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 #1491]: Setup states to hide clear all and select all in filter accordion #1495

Merged
merged 8 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
53 changes: 28 additions & 25 deletions frontend/src/app/search/SearchForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
"use client";

import React, { useRef } from "react";

import { ConvertedSearchParams } from "../../types/requestURLTypes";
import { ConvertedSearchParams } from "../../types/searchRequestURLTypes";
import { SearchAPIResponse } from "../../types/searchTypes";
import SearchBar from "../../components/search/SearchBar";
import SearchFilterAgency from "src/components/search/SearchFilterAgency";
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
7 changes: 4 additions & 3 deletions frontend/src/app/search/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"use server";

import { SearchAPIResponse } from "../../types/searchTypes";
import { SearchFetcherProps } from "../../services/searchfetcher/SearchFetcher";
import { getSearchFetcher } from "../../services/searchfetcher/SearchFetcherUtil";
import { SearchFetcherProps } from "../../services/search/searchfetcher/SearchFetcher";
import { getSearchFetcher } from "../../services/search/searchfetcher/SearchFetcherUtil";

// Gets MockSearchFetcher or APISearchFetcher based on environment variable
const searchFetcher = getSearchFetcher();
Expand All @@ -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
4 changes: 2 additions & 2 deletions frontend/src/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
ServerSideRouteParams,
ServerSideSearchParams,
} from "../../types/requestURLTypes";
} from "../../types/searchRequestURLTypes";

import { FeatureFlagsManager } from "../../services/FeatureFlagManager";
import PageSEO from "src/components/PageSEO";
Expand All @@ -10,7 +10,7 @@ import SearchCallToAction from "../../components/search/SearchCallToAction";
import { SearchForm } from "./SearchForm";
import { convertSearchParamsToProperTypes } from "../../utils/convertSearchParamsToStrings";
import { cookies } from "next/headers";
import { getSearchFetcher } from "../../services/searchfetcher/SearchFetcherUtil";
import { getSearchFetcher } from "../../services/search/searchfetcher/SearchFetcherUtil";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also moved the SearchFetcher classes from /services/searchfetcher to services/search/searchfetcher/ so there's a bunch of updates in this PR for that

import { notFound } from "next/navigation";

const searchFetcher = getSearchFetcher();
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,16 @@ export function SearchFilterAccordion({
toggleSelectAll,
incrementTotal,
decrementTotal,
} = useSearchFilter(initialFilterOptions);
isAllSelected,
isNoneSelected,
isSectionAllSelected,
isSectionNoneSelected,
} = useSearchFilter(
initialFilterOptions,
initialQueryParams,
queryParamKey,
formRef,
);

const getAccordionTitle = () => (
<>
Expand All @@ -59,6 +74,8 @@ export function SearchFilterAccordion({
<SearchFilterToggleAll
onSelectAll={() => toggleSelectAll(true)}
onClearAll={() => toggleSelectAll(false)}
isAllSelected={isAllSelected}
isNoneSelected={isNoneSelected}
/>
<ul className="usa-list usa-list--unstyled">
{options.map((option) => (
Expand All @@ -73,6 +90,8 @@ export function SearchFilterAccordion({
mounted={mounted}
updateCheckedOption={toggleOptionChecked}
toggleSelectAll={toggleSelectAll}
isSectionAllSelected={isSectionAllSelected[option.id]}
isSectionNoneSelected={isSectionNoneSelected[option.id]}
/>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea the logic to setup those values is kind of a lot but once you have it it's very easy to use in the components

) : (
<SearchFilterCheckbox
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 All @@ -13,6 +15,8 @@ interface SearchFilterSectionProps {
mounted: boolean;
updateCheckedOption: (optionId: string, isChecked: boolean) => void;
toggleSelectAll: (isSelected: boolean, sectionId: string) => void;
isSectionAllSelected: boolean;
isSectionNoneSelected: boolean;
}

const SearchFilterSection: React.FC<SearchFilterSectionProps> = ({
Expand All @@ -22,6 +26,8 @@ const SearchFilterSection: React.FC<SearchFilterSectionProps> = ({
mounted,
updateCheckedOption,
toggleSelectAll,
isSectionAllSelected,
isSectionNoneSelected,
}) => {
const [childrenVisible, setChildrenVisible] = useState<boolean>(false);

Expand Down Expand Up @@ -67,11 +73,13 @@ const SearchFilterSection: React.FC<SearchFilterSectionProps> = ({
<SectionLinkCount sectionCount={sectionCount} />
</span>
</button>
{childrenVisible && (
{childrenVisible ? (
<div className="padding-y-1">
<SearchFilterToggleAll
onSelectAll={handleSelectAll}
onClearAll={handleClearAll}
isAllSelected={isSectionAllSelected}
isNoneSelected={isSectionNoneSelected}
/>
<ul className="usa-list usa-list--unstyled margin-left-4">
{option.children?.map((child) => (
Expand All @@ -82,11 +90,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,30 +1,48 @@
"use client";

interface SearchFilterToggleAllProps {
isAllSelected: boolean;
isNoneSelected: boolean;
onSelectAll?: () => void;
onClearAll?: () => void;
}

const SearchFilterToggleAll: React.FC<SearchFilterToggleAllProps> = ({
onSelectAll,
onClearAll,
}) => (
<div className="grid-row">
<div className="grid-col-fill">
<button
className="usa-button usa-button--unstyled font-sans-xs"
onClick={onSelectAll}
>
Select All
</button>
</div>
<div className="grid-col-fill text-right">
<button
className="usa-button usa-button--unstyled font-sans-xs"
onClick={onClearAll}
>
Clear All
</button>
isAllSelected,
isNoneSelected,
}) => {
return (
<div className="grid-row">
<div className="grid-col-fill">
<button
className="usa-button usa-button--unstyled font-sans-xs"
onClick={(event) => {
// form submission is done in useSearchFilter, so
// prevent the onClick from submitting here.
event.preventDefault();
onSelectAll?.();
}}
disabled={isAllSelected}
>
Select All
</button>
</div>
<div className="grid-col-fill text-right">
<button
className="usa-button usa-button--unstyled font-sans-xs"
onClick={(event) => {
event.preventDefault();
onClearAll?.();
}}
disabled={isNoneSelected}
>
Clear All
</button>
</div>
</div>
</div>
);
);
};

export default SearchFilterToggleAll;
Loading
Loading