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 #3518] download search results #3689

Merged
merged 10 commits into from
Feb 3, 2025

Conversation

doug-s-nava
Copy link
Collaborator

@doug-s-nava doug-s-nava commented Jan 30, 2025

Summary

Fixes #3158

Time to review: 30 mins

Changes proposed

The goal here is adding a button to the search page that will allow users to download a csv file containing all (well, the first 5000) search results.

To accomplish this a few larger refactors were made:

  • fetch function service and helpers updated to be more flexible in order to allow a non-json response
  • some flaky e2e tests updated

Context for reviewers

Test steps

  1. seed your database with a bunch more records. Update seed_local_db.py to up the size of each type by a bunch. Try updating size=5 to size=50. This is important for testing with a full width pagination. Then run make db-seed-local populate-search-opportunities
  2. start a server on this branch with npm run dev
  3. visit http://localhost:3000/search
  4. VERIFY: you see an "Export results" button that matches designs
  5. resize window to table and mobile widths
  6. VERIFY: button placement and appearance matches expectations at each viewport width
  7. click the button
  8. VERIFY: a csv file downloads with filename grants-search-<timestamp>.csv)
  9. open the file
  10. VERIFY: the file contains the expected search result details in csv form

Additional information

Screenshot 2025-01-31 at 4 25 59 PM

Screenshot 2025-01-31 at 4 25 30 PM

@@ -0,0 +1,11 @@
import { OpportunityApiResponse } from "src/types/opportunity/opportunityResponseTypes";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

since the bare fetcher no longer returns the json result, we'll create a basic wrapper here.

@@ -19,7 +19,7 @@ import ServerErrorAlert from "src/components/ServerErrorAlert";

export interface ParsedError {
message: string;
searchInputs: ServerSideSearchParams;
searchInputs: OptionalStringDict;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

just a rename to reflect that this type is generic rather than specific in any way


*/

export async function GET(request: NextRequest) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

making API request here in order to avoid exposing the API key

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like a shame we have to do this. Maybe at some point we can open up some of the APIs for the general public.

Noting the API is a POST while we're using a GET, I think GET is the correct interpretation for search since a search request isn't meaningfully creating or updating server entities.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agree it's not great, but the best option we have for now. We should maybe talk as a team about how we want to handle this sort of thing.

As far as the GET v POST, I'd be fine to make the first request as POST as well, but since it's largely internal and not sending a request body, a GET seemed fine. The search itself being a post makes sense to me since we're sending data in the body of the request.

* Send a request and handle the response
* @param queryParamData: note that this is only used in error handling in order to help restore original page state
*/
export async function sendRequest<ResponseType extends APIResponse>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

since this function wasn't doing much, and what it was doing (reading a json response body) was making our implementation less flexible, I got rid of it and moved all this functionality either into the fetcher generator or endpoint specific wrappers

// note that this will pass along filter inputs in order to maintain the state
// of the page when relaying an error, but anything passed in the body of the request,
// such as keyword search query will not be included
function handleNotOkResponse(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

similar to the above, this wasn't doing much, moved the behavior into the fetcher generator


return response;
};
}

export const fetchOpportunity = cache(
requesterForEndpoint<OpportunityApiResponse>(fetchOpportunityEndpoint),
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

these all return Response now, so the generic isn't necessary any more

@@ -7,7 +7,7 @@ import {

test.describe("Search page tests", () => {
// Loadiing indicator resolves too quickly to reliably test in e2e.
skip("should show and hide loading state", async () => {
test.skip("should show and hide loading state", async () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the other implementation didn't seem to actually skip the test?

@@ -156,12 +156,12 @@ test.describe("Search page tests", () => {
page,
}: PageProps) => {
await page.goto("/search");
await waitForSearchResultsInitialLoad(page);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not sure why, but these tests got flaky. Adding the wait on initial page load seems to solve the problem

@doug-s-nava doug-s-nava marked this pull request as ready for review January 31, 2025 21:26
@doug-s-nava doug-s-nava changed the title Dschrashun/3518 download search results [Issue #3518] download search results Jan 31, 2025
@@ -16,9 +14,6 @@ interface OpportunityDocumentsProps {
documents: OpportunityDocument[];
}

dayjs.extend(advancedFormat);
dayjs.extend(timezone);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

moved this into a central location

@@ -23,3 +30,5 @@ export function formatDate(dateStr: string | null): string {
};
return date.toLocaleDateString("en-US", options);
}

export const getConfiguredDayJs = () => dayjs;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

to support the timestamp in the filename

@@ -11,6 +11,8 @@ interface IconProps {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const sprite_uri = SpriteSVG.src as string;

// height prop doesn't seem to work
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like this could be discussion rather than a code comment. The USWDS component doesn't work with SVGs as far as I recall. We could revisit that, was a while ago this was adopted.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

true, I'll remove the comment. Ticket for follow up here: #3748

acouch
acouch previously approved these changes Feb 3, 2025
Copy link
Collaborator

@acouch acouch left a comment

Choose a reason for hiding this comment

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

Looks great.

@doug-s-nava doug-s-nava merged commit e475e7c into main Feb 3, 2025
10 checks passed
@doug-s-nava doug-s-nava deleted the dschrashun/3518-download-search-results branch February 3, 2025 18:17
DavidDudas-Intuitial pushed a commit that referenced this pull request Feb 7, 2025
* adds a button to the search page that downloads search results in csv format
* refactors fetch function service and helpers to be more flexible in order to allow a non-json response
* updates some flaky e2e tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants