diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml
index 3aff7def1..d1cbad4bd 100644
--- a/api/openapi.generated.yml
+++ b/api/openapi.generated.yml
@@ -1522,6 +1522,8 @@ components:
- post_date
- close_date
- agency_code
+ - agency_name
+ - top_level_agency_name
description: The field to sort the response by
sort_direction:
description: Whether to sort the response ascending or descending
diff --git a/api/src/api/opportunities_v1/opportunity_schemas.py b/api/src/api/opportunities_v1/opportunity_schemas.py
index ab90b28e8..f383e4506 100644
--- a/api/src/api/opportunities_v1/opportunity_schemas.py
+++ b/api/src/api/opportunities_v1/opportunity_schemas.py
@@ -489,6 +489,8 @@ class OpportunitySearchRequestV1Schema(Schema):
"post_date",
"close_date",
"agency_code",
+ "agency_name",
+ "top_level_agency_name",
],
default_sort_order=[{"order_by": "opportunity_id", "sort_direction": "descending"}],
),
diff --git a/api/src/db/models/agency_models.py b/api/src/db/models/agency_models.py
index fb65539e5..8d2fb9f71 100644
--- a/api/src/db/models/agency_models.py
+++ b/api/src/db/models/agency_models.py
@@ -98,7 +98,7 @@ class Agency(ApiSchemaTable, TimestampMixin):
ForeignKey(agency_id),
nullable=True,
)
- top_level_agency: Mapped["Agency"] = relationship(
+ top_level_agency: Mapped["Agency | None"] = relationship(
lambda: Agency,
remote_side=[agency_id],
)
diff --git a/api/src/db/models/opportunity_models.py b/api/src/db/models/opportunity_models.py
index 225e2c1d6..7acd484a6 100644
--- a/api/src/db/models/opportunity_models.py
+++ b/api/src/db/models/opportunity_models.py
@@ -98,7 +98,7 @@ def top_level_agency_name(self) -> str | None:
if self.agency_record is not None and self.agency_record.top_level_agency is not None:
return self.agency_record.top_level_agency.agency_name
- return None
+ return self.agency_name
@property
def agency_name(self) -> str | None:
diff --git a/api/src/services/opportunities_v1/search_opportunities.py b/api/src/services/opportunities_v1/search_opportunities.py
index e560751cb..3ce34e296 100644
--- a/api/src/services/opportunities_v1/search_opportunities.py
+++ b/api/src/services/opportunities_v1/search_opportunities.py
@@ -42,6 +42,8 @@
"close_date": "summary.close_date",
"agency_code": "agency_code.keyword",
"agency": "agency_code.keyword",
+ "agency_name": "agency_name.keyword",
+ "top_level_agency_name": "top_level_agency_name.keyword",
"opportunity_status": "opportunity_status.keyword",
"funding_instrument": "summary.funding_instruments.keyword",
"funding_category": "summary.funding_categories.keyword",
diff --git a/frontend/src/components/search/SearchResultsListItem.tsx b/frontend/src/components/search/SearchResultsListItem.tsx
index 315605063..5c8e5ffe8 100644
--- a/frontend/src/components/search/SearchResultsListItem.tsx
+++ b/frontend/src/components/search/SearchResultsListItem.tsx
@@ -97,17 +97,21 @@ export default function SearchResultsListItem({
: "--"}
-
+
{t("resultsListItem.summary.agency")}
- {opportunity?.agency ||
- (opportunity?.summary?.agency_name &&
- opportunity?.summary?.agency_code &&
- agencyNameLookup
- ? // Use same exact label we're using for the agency filter list
- agencyNameLookup[opportunity?.summary?.agency_code]
- : "--")}
+ {opportunity?.top_level_agency_name &&
+ opportunity?.agency_name &&
+ opportunity?.top_level_agency_name !== opportunity?.agency_name
+ ? `${opportunity?.top_level_agency_name} - ${opportunity?.agency_name}`
+ : opportunity?.agency_name ||
+ (agencyNameLookup && opportunity?.summary?.agency_code
+ ? // Use same exact label we're using for the agency filter list
+ agencyNameLookup[opportunity?.summary?.agency_code]
+ : "--")}
+
+
{t("resultsListItem.opportunity_number")}
{opportunity?.opportunity_number}
diff --git a/frontend/src/services/fetch/fetchers/searchFetcher.ts b/frontend/src/services/fetch/fetchers/searchFetcher.ts
index a1f1a1801..4995790dd 100644
--- a/frontend/src/services/fetch/fetchers/searchFetcher.ts
+++ b/frontend/src/services/fetch/fetchers/searchFetcher.ts
@@ -4,6 +4,7 @@ import { fetchOpportunitySearch } from "src/services/fetch/fetchers/fetchers";
import {
PaginationOrderBy,
PaginationRequestBody,
+ PaginationSortOrder,
QueryParamData,
SearchFetcherActionType,
SearchFilterRequestBody,
@@ -12,12 +13,12 @@ import {
import { SearchAPIResponse } from "src/types/search/searchResponseTypes";
const orderByFieldLookup = {
- relevancy: "relevancy",
- opportunityNumber: "opportunity_number",
- opportunityTitle: "opportunity_title",
- agency: "agency_code",
- postedDate: "post_date",
- closeDate: "close_date",
+ relevancy: ["relevancy"],
+ opportunityNumber: ["opportunity_number"],
+ opportunityTitle: ["opportunity_title"],
+ agency: ["top_level_agency_name", "agency_name"],
+ postedDate: ["post_date"],
+ closeDate: ["close_date"],
};
type FrontendFilterNames =
@@ -134,24 +135,38 @@ export const buildPagination = (
? 1
: page;
- let order_by: PaginationOrderBy = "relevancy";
+ let sort_order: PaginationSortOrder = [
+ { order_by: "relevancy", sort_direction: "descending" },
+ { order_by: "post_date", sort_direction: "descending" },
+ ];
+
if (sortby) {
+ sort_order = [];
+ // this will need to change in a future where we allow the user to set an ordered set of sort columns.
+ // for now we're just using the multiple internally behind a single column picker drop down so this is fine.
for (const [key, value] of Object.entries(orderByFieldLookup)) {
if (sortby.startsWith(key)) {
- order_by = value as PaginationOrderBy;
- break; // Stop searching after the first match is found
+ const sort_direction =
+ sortby && sortby.endsWith("Asc") ? "ascending" : "descending";
+ value.forEach((item) => {
+ sort_order.push({
+ order_by: item,
+ sort_direction,
+ });
+ if (item === "relevancy") {
+ sort_order.push({
+ order_by: "post_date",
+ sort_direction,
+ });
+ }
+ });
}
}
}
- // sort relevancy descending without suffix
- const sort_direction =
- sortby && sortby.endsWith("Asc") ? "ascending" : "descending";
-
return {
- order_by,
page_offset,
page_size: 25,
- sort_direction,
+ sort_order,
};
};
diff --git a/frontend/src/types/search/searchRequestTypes.ts b/frontend/src/types/search/searchRequestTypes.ts
index eab0172a8..461769053 100644
--- a/frontend/src/types/search/searchRequestTypes.ts
+++ b/frontend/src/types/search/searchRequestTypes.ts
@@ -11,15 +11,19 @@ export type PaginationOrderBy =
| "opportunity_id"
| "opportunity_number"
| "opportunity_title"
- | "agency_code"
+ | "agency_name"
+ | "top_level_agency_code"
| "post_date"
| "close_date";
export type PaginationSortDirection = "ascending" | "descending";
-export interface PaginationRequestBody {
+export type PaginationSortOrder = {
order_by: PaginationOrderBy;
+ sort_direction: PaginationSortDirection;
+}[];
+export interface PaginationRequestBody {
page_offset: number;
page_size: number;
- sort_direction: PaginationSortDirection;
+ sort_order: PaginationSortOrder;
}
export type SearchRequestBody = {
diff --git a/frontend/src/types/search/searchResponseTypes.ts b/frontend/src/types/search/searchResponseTypes.ts
index 4afb2866f..719ae2428 100644
--- a/frontend/src/types/search/searchResponseTypes.ts
+++ b/frontend/src/types/search/searchResponseTypes.ts
@@ -43,6 +43,7 @@ export interface Summary {
export interface Opportunity {
agency: string | null;
+ agency_name: string | null;
category: string | null;
category_explanation: string | null;
created_at: string;
@@ -52,6 +53,7 @@ export interface Opportunity {
opportunity_status: OpportunityStatus;
opportunity_title: string;
summary: Summary;
+ top_level_agency_name: string | null;
updated_at: string;
}
diff --git a/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts b/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts
index dca102a56..d79efd691 100644
--- a/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts
+++ b/frontend/tests/services/fetch/fetchers/SearchFetcher.test.ts
@@ -51,10 +51,14 @@ describe("searchForOpportunities", () => {
expect(mockfetchOpportunitySearch).toHaveBeenCalledWith({
body: {
pagination: {
- order_by: "opportunity_number", // This should be the actual value being used in the API method
+ sort_order: [
+ {
+ order_by: "opportunity_number",
+ sort_direction: "ascending",
+ },
+ ], // This should be the actual value being used in the API method
page_offset: 1,
page_size: 25,
- sort_direction: "ascending", // or "descending" based on your sortby parameter
},
query: "research",
filters: {
@@ -86,10 +90,14 @@ describe("downloadOpportunities", () => {
expect(mockfetchOpportunitySearch).toHaveBeenCalledWith({
body: {
pagination: {
- order_by: "opportunity_number", // This should be the actual value being used in the API method
+ sort_order: [
+ {
+ order_by: "opportunity_number", // This should be the actual value being used in the API method
+ sort_direction: "ascending", // or "descending" based on your sortby parameter
+ },
+ ],
page_offset: 1,
page_size: 5000,
- sort_direction: "ascending", // or "descending" based on your sortby parameter
},
query: "research",
filters: {
@@ -157,9 +165,9 @@ describe("buildPagination", () => {
...{ page: 5, sortby: null },
});
- expect(pagination.order_by).toEqual("relevancy");
+ expect(pagination.sort_order[0].order_by).toEqual("relevancy");
expect(pagination.page_offset).toEqual(5);
- expect(pagination.sort_direction).toEqual("descending");
+ expect(pagination.sort_order[0].sort_direction).toEqual("descending");
});
it("builds correct offset based on action type and field changed", () => {
@@ -206,14 +214,14 @@ describe("buildPagination", () => {
...{ sortby: "closeDateAsc" },
});
- expect(pagination.order_by).toEqual("close_date");
+ expect(pagination.sort_order[0].order_by).toEqual("close_date");
const secondPagination = buildPagination({
...searchProps,
...{ sortby: "postedDateAsc" },
});
- expect(secondPagination.order_by).toEqual("post_date");
+ expect(secondPagination.sort_order[0].order_by).toEqual("post_date");
});
it("builds correct sort_direction based on sortby", () => {
@@ -222,20 +230,20 @@ describe("buildPagination", () => {
...{ sortby: "opportunityNumberDesc" },
});
- expect(pagination.sort_direction).toEqual("descending");
+ expect(pagination.sort_order[0].sort_direction).toEqual("descending");
const secondPagination = buildPagination({
...searchProps,
...{ sortby: "postedDateAsc" },
});
- expect(secondPagination.sort_direction).toEqual("ascending");
+ expect(secondPagination.sort_order[0].sort_direction).toEqual("ascending");
const thirdPagination = buildPagination({
...searchProps,
...{ sortby: null },
});
- expect(thirdPagination.sort_direction).toEqual("descending");
+ expect(thirdPagination.sort_order[0].sort_direction).toEqual("descending");
});
});