From bbfa4eb649d53f2b9902ec3fca47004839bcc665 Mon Sep 17 00:00:00 2001 From: acouch Date: Fri, 5 Jul 2024 14:00:32 -0400 Subject: [PATCH] Add search result header into suspense --- frontend/src/app/[locale]/search/page.tsx | 40 ++++++++-- .../components/search/SearchPagination.tsx | 74 +++++++++++++------ .../search/SearchPaginationFetch.tsx | 33 +++++++++ .../search/SearchPaginationItem.tsx | 61 --------------- .../search/SearchPaginationLoader.tsx | 29 -------- .../components/search/SearchResultsHeader.tsx | 23 ++++-- .../search/SearchResultsHeaderFetch.tsx | 26 +++++++ ...ultList.tsx => SearchResultsListFetch.tsx} | 2 +- .../src/components/search/SearchSortBy.tsx | 12 ++- .../components/search/SearchSortBy.test.tsx | 10 ++- 10 files changed, 181 insertions(+), 129 deletions(-) create mode 100644 frontend/src/components/search/SearchPaginationFetch.tsx delete mode 100644 frontend/src/components/search/SearchPaginationItem.tsx delete mode 100644 frontend/src/components/search/SearchPaginationLoader.tsx create mode 100644 frontend/src/components/search/SearchResultsHeaderFetch.tsx rename frontend/src/components/search/{SearchResultList.tsx => SearchResultsListFetch.tsx} (96%) diff --git a/frontend/src/app/[locale]/search/page.tsx b/frontend/src/app/[locale]/search/page.tsx index 8a614e793a..c023d09c04 100644 --- a/frontend/src/app/[locale]/search/page.tsx +++ b/frontend/src/app/[locale]/search/page.tsx @@ -2,14 +2,15 @@ import BetaAlert from "src/components/BetaAlert"; import Breadcrumbs from "src/components/Breadcrumbs"; import Loading from "src/app/[locale]/search/loading"; import PageSEO from "src/components/PageSEO"; -import SearchResultsList from "src/components/search/SearchResultList"; +import SearchResultsListFetch from "src/components/search/SearchResultsListFetch"; import QueryProvider from "./QueryProvider"; import SearchBar from "src/components/search/SearchBar"; import SearchCallToAction from "src/components/search/SearchCallToAction"; import SearchFilterAccordion from "src/components/search/SearchFilterAccordion/SearchFilterAccordion"; import SearchOpportunityStatus from "src/components/search/SearchOpportunityStatus"; import SearchPagination from "src/components/search/SearchPagination"; -import SearchPaginationLoader from "src/components/search/SearchPaginationLoader"; +import SearchPaginationFetch from "src/components/search/SearchPaginationFetch"; +import SearchResultsHeaderFetch from "src/components/search/SearchResultsHeaderFetch"; import SearchResultsHeader from "src/components/search/SearchResultsHeader"; import withFeatureFlag from "src/hoc/search/withFeatureFlag"; import { @@ -108,25 +109,48 @@ function Search({ searchParams }: { searchParams: searchParamsTypes }) { />
- + + } + > + +
} + fallback={ + + } > - }> - + } + fallback={ + + } > - diff --git a/frontend/src/components/search/SearchPagination.tsx b/frontend/src/components/search/SearchPagination.tsx index 9aad57328c..03ea044447 100644 --- a/frontend/src/components/search/SearchPagination.tsx +++ b/frontend/src/components/search/SearchPagination.tsx @@ -1,31 +1,63 @@ -"use server"; -import { getSearchFetcher } from "src/services/search/searchfetcher/SearchFetcherUtil"; -import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher"; -import SearchPaginationItem from "./SearchPaginationItem"; +"use client"; +import { Pagination } from "@trussworks/react-uswds"; +import { QueryContext } from "src/app/[locale]/search/QueryProvider"; +import { useSearchParamUpdater } from "src/hooks/useSearchParamUpdater"; +import { useContext } from "react"; + +export enum PaginationPosition { + Top = "topPagination", + Bottom = "bottomPagination", +} interface SearchPaginationProps { - searchParams: QueryParamData; - scroll: boolean; + page: number; + query: string | null | undefined; + total?: number | null; + scroll?: boolean; + totalResults?: string; + loading?: boolean; } -export default async function SearchPagination({ - searchParams, - scroll, +const MAX_SLOTS = 7; + +export default function SearchPagination({ + page, + query, + total = null, + scroll = false, + totalResults = "", + loading = false, }: SearchPaginationProps) { - const searchFetcher = getSearchFetcher(); - const searchResults = await searchFetcher.fetchOpportunities(searchParams); - const totalPages = searchResults.pagination_info?.total_pages; - const totalResults = searchResults.pagination_info?.total_records; + const { updateQueryParams } = useSearchParamUpdater(); + const { updateTotalPages, updateTotalResults } = useContext(QueryContext); + const { totalPages } = useContext(QueryContext); + // Shows total pages from the query context before it is re-fetched from the API. + const pages = total || Number(totalPages); + + const updatePage = (page: number) => { + updateTotalPages(String(total)); + updateTotalResults(totalResults); + updateQueryParams(String(page), "page", query, scroll); + }; return ( - <> - + updatePage(page + 1)} + onClickPrevious={() => updatePage(page > 1 ? page - 1 : 0)} + onClickPageNumber={(event: React.MouseEvent, page: number) => + updatePage(page) + } /> - +
); } diff --git a/frontend/src/components/search/SearchPaginationFetch.tsx b/frontend/src/components/search/SearchPaginationFetch.tsx new file mode 100644 index 0000000000..a0193d1f19 --- /dev/null +++ b/frontend/src/components/search/SearchPaginationFetch.tsx @@ -0,0 +1,33 @@ +"use server"; +import { getSearchFetcher } from "src/services/search/searchfetcher/SearchFetcherUtil"; +import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher"; +import SearchPagination from "./SearchPagination"; + +interface SearchPaginationProps { + searchParams: QueryParamData; + // Determines whether clicking on pager items causes a scroll to the top of the search + // results. Created so the bottom pager can scroll. + scroll: boolean; +} + +export default async function SearchPaginationFetch({ + searchParams, + scroll, +}: SearchPaginationProps) { + const searchFetcher = getSearchFetcher(); + const searchResults = await searchFetcher.fetchOpportunities(searchParams); + const totalPages = searchResults.pagination_info?.total_pages; + const totalResults = searchResults.pagination_info?.total_records; + + return ( + <> + + + ); +} diff --git a/frontend/src/components/search/SearchPaginationItem.tsx b/frontend/src/components/search/SearchPaginationItem.tsx deleted file mode 100644 index 763a3e5ea5..0000000000 --- a/frontend/src/components/search/SearchPaginationItem.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; - -import { Pagination } from "@trussworks/react-uswds"; -import { QueryContext } from "src/app/[locale]/search/QueryProvider"; -import { useSearchParamUpdater } from "src/hooks/useSearchParamUpdater"; -import { useDebouncedCallback } from "use-debounce"; -import { useContext } from "react"; - -export enum PaginationPosition { - Top = "topPagination", - Bottom = "bottomPagination", -} - -interface SearchPaginationProps { - total: number; - page: number; - query: string | null | undefined; - scroll: boolean; - totalResults: string; -} - -const MAX_SLOTS = 7; - -export default function SearchPaginationItem({ - total, - page, - query, - scroll, - totalResults, -}: SearchPaginationProps) { - const { updateQueryParams } = useSearchParamUpdater(); - const { updateTotalPages, updateTotalResults } = useContext(QueryContext); - - // TODO: determine better state management. The results are grabbed on the server but - // not available to the client components that aren't suspense wrapped. - updateTotalResults(totalResults); - const debouncedUpdate = useDebouncedCallback((page: string) => { - updateQueryParams(page, "page", query, scroll); - }, 50); - - const updatePage = (page: number) => { - updateTotalPages(String(total)); - debouncedUpdate(String(page)); - }; - - return ( - <> - updatePage(page + 1)} - onClickPrevious={() => updatePage(page > 1 ? page - 1 : 0)} - onClickPageNumber={(event: React.MouseEvent, page: number) => - updatePage(page) - } - /> - - ); -} diff --git a/frontend/src/components/search/SearchPaginationLoader.tsx b/frontend/src/components/search/SearchPaginationLoader.tsx deleted file mode 100644 index e754309e0e..0000000000 --- a/frontend/src/components/search/SearchPaginationLoader.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { Pagination } from "@trussworks/react-uswds"; -import { QueryContext } from "../../app/[locale]/search/QueryProvider"; -import { useContext } from "react"; - -interface SearchPaginationProps { - page: number; -} - -const MAX_SLOTS = 7; - -export default function SearchPaginationLoader({ - page, -}: SearchPaginationProps) { - const { totalPages } = useContext(QueryContext); - const total = totalPages === "na" ? MAX_SLOTS : totalPages; - - return ( -
- -
- ); -} diff --git a/frontend/src/components/search/SearchResultsHeader.tsx b/frontend/src/components/search/SearchResultsHeader.tsx index 00766b97e3..3855a05dbc 100644 --- a/frontend/src/components/search/SearchResultsHeader.tsx +++ b/frontend/src/components/search/SearchResultsHeader.tsx @@ -5,18 +5,31 @@ import { useContext } from "react"; export default function SearchResultsHeader({ sortby, + totalFetchedResults, + queryTerm, + loading = false, }: { sortby: string | null; + totalFetchedResults?: string; + queryTerm?: string | null | undefined; + loading?: boolean; }) { - const { totalResults, queryTerm } = useContext(QueryContext); - + const { totalResults } = useContext(QueryContext); + const total = totalFetchedResults || totalResults; return (
-

- {totalResults.length > 0 && <>{totalResults} Opportunities} +

+ {total && <>{total} Opportunities}

- +
); diff --git a/frontend/src/components/search/SearchResultsHeaderFetch.tsx b/frontend/src/components/search/SearchResultsHeaderFetch.tsx new file mode 100644 index 0000000000..d899124758 --- /dev/null +++ b/frontend/src/components/search/SearchResultsHeaderFetch.tsx @@ -0,0 +1,26 @@ +"use server"; +import { getSearchFetcher } from "src/services/search/searchfetcher/SearchFetcherUtil"; +import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher"; +import SearchResultsHeader from "./SearchResultsHeader"; + +export default async function SearchResultsHeaderFetch({ + searchParams, + sortby, + queryTerm, +}: { + searchParams: QueryParamData; + sortby: string | null; + queryTerm: string | null | undefined; +}) { + const searchFetcher = getSearchFetcher(); + const searchResults = await searchFetcher.fetchOpportunities(searchParams); + const totalResults = searchResults.pagination_info?.total_records; + + return ( + + ); +} diff --git a/frontend/src/components/search/SearchResultList.tsx b/frontend/src/components/search/SearchResultsListFetch.tsx similarity index 96% rename from frontend/src/components/search/SearchResultList.tsx rename to frontend/src/components/search/SearchResultsListFetch.tsx index 2d7dc973eb..2052a4629a 100644 --- a/frontend/src/components/search/SearchResultList.tsx +++ b/frontend/src/components/search/SearchResultsListFetch.tsx @@ -7,7 +7,7 @@ interface ServerPageProps { searchParams: QueryParamData; } -export default async function SearchResultsList({ +export default async function SearchResultsListFetch({ searchParams, }: ServerPageProps) { const searchFetcher = getSearchFetcher(); diff --git a/frontend/src/components/search/SearchSortBy.tsx b/frontend/src/components/search/SearchSortBy.tsx index 074da46f26..da13acd664 100644 --- a/frontend/src/components/search/SearchSortBy.tsx +++ b/frontend/src/components/search/SearchSortBy.tsx @@ -1,5 +1,8 @@ +"use client"; import { Select } from "@trussworks/react-uswds"; import { useSearchParamUpdater } from "src/hooks/useSearchParamUpdater"; +import { QueryContext } from "src/app/[locale]/search/QueryProvider"; +import { useContext } from "react"; type SortOption = { label: string; @@ -22,13 +25,20 @@ const SORT_OPTIONS: SortOption[] = [ interface SearchSortByProps { queryTerm: string | null | undefined; sortby: string | null; + totalResults: string; } -export default function SearchSortBy({ queryTerm, sortby }: SearchSortByProps) { +export default function SearchSortBy({ + queryTerm, + sortby, + totalResults, +}: SearchSortByProps) { const { updateQueryParams } = useSearchParamUpdater(); + const { updateTotalResults } = useContext(QueryContext); const handleChange = (event: React.ChangeEvent) => { const newValue = event.target.value; + updateTotalResults(totalResults); updateQueryParams(newValue, "sortby", queryTerm); }; diff --git a/frontend/tests/components/search/SearchSortBy.test.tsx b/frontend/tests/components/search/SearchSortBy.test.tsx index cd9582a636..a977cc9e00 100644 --- a/frontend/tests/components/search/SearchSortBy.test.tsx +++ b/frontend/tests/components/search/SearchSortBy.test.tsx @@ -14,7 +14,11 @@ jest.mock("src/hooks/useSearchParamUpdater", () => ({ describe("SearchSortBy", () => { it("should not have basic accessibility issues", async () => { const { container } = render( - , + , ); const results = await axe(container); @@ -22,7 +26,7 @@ describe("SearchSortBy", () => { }); it("renders correctly with initial query params", () => { - render(); + render(); expect( screen.getByDisplayValue("Posted Date (newest)"), @@ -34,7 +38,7 @@ describe("SearchSortBy", () => { const requestSubmitMock = jest.fn(); formElement.requestSubmit = requestSubmitMock; - render(); + render(); fireEvent.change(screen.getByRole("combobox"), { target: { value: "opportunityTitleDesc" },