Skip to content

Commit

Permalink
Agencies error boundary (#3878)
Browse files Browse the repository at this point in the history
* removes global state
* removes search error page
* instead of a boundary based approach, which is limited to client
components, handles API request based search errors where they occur
  • Loading branch information
doug-s-nava authored Feb 13, 2025
1 parent 040057d commit d3936ca
Show file tree
Hide file tree
Showing 15 changed files with 81 additions and 316 deletions.
3 changes: 1 addition & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@
"react-dom": "^19.0.0",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"zod": "^3.23.8",
"zustand": "^5.0.3"
"zod": "^3.23.8"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.9.0",
Expand Down
136 changes: 0 additions & 136 deletions frontend/src/app/[locale]/search/error.tsx

This file was deleted.

25 changes: 11 additions & 14 deletions frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import UserProvider from "src/services/auth/UserProvider";
import { GlobalStateProvider } from "src/services/globalState/GlobalStateProvider";

import { useTranslations } from "next-intl";
import { setRequestLocale } from "next-intl/server";
Expand All @@ -21,19 +20,17 @@ export default function Layout({ children, locale }: Props) {
return (
// Stick the footer to the bottom of the page
<UserProvider>
<GlobalStateProvider>
<div className="display-flex flex-column minh-viewport">
<a className="usa-skipnav" href="#main-content">
{t("Layout.skip_to_main")}
</a>
<Header locale={locale} />
<main id="main-content" className="border-top-0">
{children}
</main>
<Footer />
<GrantsIdentifier />
</div>
</GlobalStateProvider>
<div className="display-flex flex-column minh-viewport">
<a className="usa-skipnav" href="#main-content">
{t("Layout.skip_to_main")}
</a>
<Header locale={locale} />
<main id="main-content" className="border-top-0">
{children}
</main>
<Footer />
<GrantsIdentifier />
</div>
</UserProvider>
);
}
39 changes: 39 additions & 0 deletions frontend/src/components/search/SearchError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { ParsedError } from "src/types/generalTypes";
import { ErrorProps } from "src/types/uiTypes";

import { useTranslations } from "next-intl";
import { Alert } from "@trussworks/react-uswds";

import ServerErrorAlert from "src/components/ServerErrorAlert";

function isValidJSON(str: string) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false; // String is not valid JSON
}
}

export function SearchError({ error }: ErrorProps) {
const t = useTranslations("Search");

const parsedErrorData = isValidJSON(error.message)
? (JSON.parse(error.message) as ParsedError)
: {};

// note that the validation error will contain untranslated strings
// and will only appear in development, prod builds will not include user facing error details
const ErrorAlert =
parsedErrorData.details && parsedErrorData.type === "ValidationError" ? (
<Alert type="error" heading={t("validationError")} headingLevel="h4">
{`Error in ${parsedErrorData.details.field || "a search field"}: ${parsedErrorData.details.message || "adjust your search and try again"}`}
</Alert>
) : (
<ServerErrorAlert callToAction={t("generic_error_cta")} />
);

return <div className="tablet:grid-col-8">{ErrorAlert}</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ async function AgencyFilterAccordionWithFetchedOptions({
agenciesPromise: Promise<FilterOption[]>;
title: string;
}) {
const agencies = await agenciesPromise;
let agencies: FilterOption[];
try {
agencies = await agenciesPromise;
} catch (e) {
// Come back to this to show the user an error
console.error("Unable to fetch agencies for filter list", e);
agencies = [];
}
return (
<SearchFilterAccordion
filterOptions={agencies}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
import { camelCase } from "lodash";
import { QueryContext } from "src/app/[locale]/search/QueryProvider";
import { useSearchParamUpdater } from "src/hooks/useSearchParamUpdater";
import { useGlobalState } from "src/services/globalState/GlobalStateProvider";
import { QueryParamKey } from "src/types/search/searchResponseTypes";

import { useContext, useEffect } from "react";
import { useContext } from "react";
import { Accordion } from "@trussworks/react-uswds";

import SearchFilterCheckbox from "src/components/search/SearchFilterAccordion/SearchFilterCheckbox";
Expand Down Expand Up @@ -67,15 +66,6 @@ export function SearchFilterAccordion({
}: SearchFilterAccordionProps) {
const { queryTerm } = useContext(QueryContext);
const { updateQueryParams, searchParams } = useSearchParamUpdater();
const { setAgencyOptions } = useGlobalState(({ setAgencyOptions }) => ({
setAgencyOptions,
}));

useEffect(() => {
if (queryParamKey === "agency" && filterOptions && setAgencyOptions) {
setAgencyOptions(filterOptions);
}
}, [queryParamKey, filterOptions, setAgencyOptions]);

const totalCheckedCount = query.size;
// all top level selectable filter options
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Suspense } from "react";
import { ClientSideUrlUpdater } from "src/components/ClientSideUrlUpdater";
import Loading from "src/components/Loading";
import { ExportSearchResultsButton } from "./ExportSearchResultsButton";
import { SearchError } from "./SearchError";
import SearchPagination from "./SearchPagination";
import SearchResultsHeader from "./SearchResultsHeader";
import SearchResultsList from "./SearchResultsList";
Expand Down Expand Up @@ -47,7 +48,14 @@ const ResolvedSearchResults = async ({
query?: string | null;
searchResultsPromise: Promise<SearchAPIResponse>;
}) => {
const searchResults = await searchResultsPromise;
let searchResults: SearchAPIResponse;

try {
searchResults = await searchResultsPromise;
} catch (e) {
const error = e as Error;
return <SearchError error={error} />;
}

// if there are no results because we've requested a page beyond the number of total pages
// update page to the last page to trigger a new search
Expand Down
71 changes: 0 additions & 71 deletions frontend/src/services/globalState/GlobalStateProvider.tsx

This file was deleted.

10 changes: 10 additions & 0 deletions frontend/src/types/generalTypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FrontendErrorDetails } from "src/types/apiResponseTypes";

export interface LayoutProps {
children: React.ReactNode;
params: Promise<{
Expand All @@ -8,3 +10,11 @@ export interface LayoutProps {
export interface OptionalStringDict {
[key: string]: string | undefined;
}

export interface ParsedError {
message?: string;
searchInputs?: OptionalStringDict;
status?: number;
type?: string;
details?: FrontendErrorDetails;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ jest.mock("src/hooks/useSearchParamUpdater", () => ({
}),
}));

jest.mock("src/services/globalState/GlobalStateProvider", () => ({
useGlobalState: () => ({
setAgencyOptions: () => undefined,
}),
}));

jest.mock("react", () => ({
...jest.requireActual<typeof import("react")>("react"),
Suspense: ({ fallback }: { fallback: React.Component }) => fallback,
Expand Down
Loading

0 comments on commit d3936ca

Please sign in to comment.