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 #3646] Better Agency display and sort in Search #3686

Closed
wants to merge 20 commits into from
Closed
Changes from 1 commit
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
Prev Previous commit
Next Next commit
this seems to work with zustand
  • Loading branch information
doug-s-nava committed Jan 29, 2025
commit 8268d5f839ccb23fd44ff7817a09dedd686e4eba
38 changes: 37 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -49,7 +49,8 @@
"react-dom": "^19.0.0",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"zod": "^3.23.8"
"zod": "^3.23.8",
"zustand": "^5.0.3"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.9.0",
87 changes: 71 additions & 16 deletions frontend/src/app/[locale]/search/error.tsx
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

import QueryProvider from "src/app/[locale]/search/QueryProvider";
import { usePrevious } from "src/hooks/usePrevious";
import { useGlobalState } from "src/services/globalState/GlobalStateProvider";
import { FrontendErrorDetails } from "src/types/apiResponseTypes";
import { ServerSideSearchParams } from "src/types/searchRequestURLTypes";
import { Breakpoints, ErrorProps } from "src/types/uiTypes";
@@ -14,7 +15,14 @@ import { Alert } from "@trussworks/react-uswds";

import ContentDisplayToggle from "src/components/ContentDisplayToggle";
import SearchBar from "src/components/search/SearchBar";
import SearchFilters from "src/components/search/SearchFilters";
import { AgencyFilterAccordion } from "src/components/search/SearchFilterAccordion/AgencyFilterAccordion";
import SearchFilterAccordion from "src/components/search/SearchFilterAccordion/SearchFilterAccordion";
import {
categoryOptions,
eligibilityOptions,
fundingOptions,
} from "src/components/search/SearchFilterAccordion/SearchFilterOptions";
import SearchOpportunityStatus from "src/components/search/SearchOpportunityStatus";
import ServerErrorAlert from "src/components/ServerErrorAlert";

export interface ParsedError {
@@ -25,6 +33,15 @@ export interface ParsedError {
details?: FrontendErrorDetails;
}

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

/*
- expand the layout to ensure that server side rendering can happen on the agency filter
- - problem is that then we would not be able to repopulate the form state on error
@@ -36,11 +53,38 @@ export interface ParsedError {
- - we'll also need to restructure the filters to support pulling from local storage instead of the API (since we just pass a promise containing the agencies down, maybe it's enough to make how we create that promise more flexible)
*/

// note that the SearchFilters component is not used since it is a server component
// we work around that by including the rendered components from SearchFilters, but manually
// passing through the agency options as received from global state rather than fetching from API
export default function SearchError({ error, reset }: ErrorProps) {
const t = useTranslations("Search");
const searchParams = useSearchParams();
const previousSearchParams =
usePrevious<ReadonlyURLSearchParams>(searchParams);
useEffect(() => {
console.error(error);
}, [error]);

// const { agencyOptions } = useMemo(
// () =>
// useGlobalState(({ agencyOptions }) => ({
// agencyOptions,
// })),
// [],
// );

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

const { agencyOptions } = useGlobalState(({ agencyOptions }) => ({
agencyOptions,
}));

// const agencyOptions = [];
const convertedSearchParams = convertSearchParamsToProperTypes(
Object.fromEntries(searchParams.entries().toArray()),
);

useEffect(() => {
if (
@@ -52,13 +96,8 @@ export default function SearchError({ error, reset }: ErrorProps) {
}
}, [searchParams, reset]);

useEffect(() => {
console.error(error);
}, [error]);

const convertedSearchParams = convertSearchParamsToProperTypes(
Object.fromEntries(searchParams.entries().toArray()),
);
const { agency, category, eligibility, fundingInstrument, query, status } =
convertedSearchParams;
// note that the validation error will contain untranslated strings
const ErrorAlert =
parsedErrorData.details && parsedErrorData.type === "ValidationError" ? (
@@ -77,19 +116,35 @@ export default function SearchError({ error, reset }: ErrorProps) {
</div>
<div className="grid-row grid-gap">
<div className="tablet:grid-col-4">
{/* <ContentDisplayToggle
<ContentDisplayToggle
showCallToAction={t("filterDisplayToggle.showFilters")}
hideCallToAction={t("filterDisplayToggle.hideFilters")}
breakpoint={Breakpoints.TABLET}
>
<SearchFilters
opportunityStatus={status}
eligibility={eligibility}
category={category}
fundingInstrument={fundingInstrument}
agency={agency}
<SearchOpportunityStatus query={status} />
<SearchFilterAccordion
filterOptions={fundingOptions}
query={fundingInstrument}
queryParamKey="fundingInstrument"
title={t("accordion.titles.funding")}
/>
<SearchFilterAccordion
filterOptions={eligibilityOptions}
query={eligibility}
queryParamKey={"eligibility"}
title={t("accordion.titles.eligibility")}
/>
<AgencyFilterAccordion
query={agency}
agencyOptions={agencyOptions}
/>
<SearchFilterAccordion
filterOptions={categoryOptions}
query={category}
queryParamKey={"category"}
title={t("accordion.titles.category")}
/>
</ContentDisplayToggle> */}
</ContentDisplayToggle>
</div>
<div className="tablet:grid-col-8">{ErrorAlert}</div>
</div>
25 changes: 14 additions & 11 deletions frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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";
@@ -20,17 +21,19 @@ export default function Layout({ children, locale }: Props) {
return (
// Stick the footer to the bottom of the page
<UserProvider>
<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>
</GlobalStateProvider>
</UserProvider>
);
}
Original file line number Diff line number Diff line change
@@ -6,15 +6,15 @@ import SearchFilterAccordion, {
FilterOption,
} from "src/components/search/SearchFilterAccordion/SearchFilterAccordion";

// this could be abstracted if we ever want to do this again
export async function AgencyFilterAccordion({
async function AgencyFilterAccordionWithFetchedOptions({
query,
agenciesPromise,
title,
}: {
query: Set<string>;
agenciesPromise: Promise<FilterOption[]>;
title: string;
}) {
const t = useTranslations("Search");
const agencies = await agenciesPromise;
return (
<Suspense
@@ -23,7 +23,7 @@ export async function AgencyFilterAccordion({
bordered={true}
items={[
{
title: `${t("accordion.titles.agency")}`,
title,
content: [],
expanded: false,
id: `opportunity-filter-agency-disabled`,
@@ -39,8 +39,44 @@ export async function AgencyFilterAccordion({
filterOptions={agencies}
query={query}
queryParamKey={"agency"}
title={t("accordion.titles.agency")}
title={title}
/>
</Suspense>
);
}

// this could be abstracted if we ever want to do this again
export function AgencyFilterAccordion({
query,
agencyOptions,
agencyOptionsPromise,
}: {
query: Set<string>;
agencyOptions?: FilterOption[];
agencyOptionsPromise?: Promise<FilterOption[]>;
}) {
const t = useTranslations("Search");
const title = t("accordion.titles.agency");
if (agencyOptions) {
return (
<SearchFilterAccordion
filterOptions={agencyOptions}
query={query}
queryParamKey={"agency"}
title={title}
/>
);
}
if (agencyOptionsPromise) {
return (
<AgencyFilterAccordionWithFetchedOptions
agenciesPromise={agencyOptionsPromise}
query={query}
title={title}
/>
);
}
throw new Error(
"AgencyFilterAccordion must have either agencyOptions or agencyOptionsPromise prop",
);
}
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
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 } from "react";
@@ -66,6 +67,13 @@ export function SearchFilterAccordion({
}: SearchFilterAccordionProps) {
const { queryTerm } = useContext(QueryContext);
const { updateQueryParams, searchParams } = useSearchParamUpdater();
const { setAgencyOptions } = useGlobalState(({ setAgencyOptions }) => ({
setAgencyOptions,
}));

if (queryParamKey === "agency" && filterOptions) {
setAgencyOptions(filterOptions);
}

const totalCheckedCount = query.size;
// all top level selectable filter options
5 changes: 4 additions & 1 deletion frontend/src/components/search/SearchFilters.tsx
Original file line number Diff line number Diff line change
@@ -42,7 +42,10 @@ export default function SearchFilters({
queryParamKey={"eligibility"}
title={t("accordion.titles.eligibility")}
/>
<AgencyFilterAccordion query={agency} agenciesPromise={agenciesPromise} />
<AgencyFilterAccordion
query={agency}
agencyOptionsPromise={agenciesPromise}
/>
<SearchFilterAccordion
filterOptions={categoryOptions}
query={category}
9 changes: 6 additions & 3 deletions frontend/src/services/fetch/fetchers/agenciesFetcher.ts
Original file line number Diff line number Diff line change
@@ -66,10 +66,13 @@ export const agenciesToFilterOptions = (
}, [] as FilterOption[]);
};

export const getAgenciesForFilterOptions = async (): Promise<
FilterOption[]
> => {
export const getAgenciesForFilterOptions = async (
prefetchedOptions?: FilterOption[],
): Promise<FilterOption[]> => {
try {
if (prefetchedOptions) {
return Promise.resolve(prefetchedOptions);
}
const agencies = await obtainAgencies();
const filterOptions = agenciesToFilterOptions(agencies);
return sortFilterOptions(filterOptions);
Loading