diff --git a/__test__/pages/search/searchResults.test.tsx b/__test__/pages/search/searchResults.test.tsx index 13580ad7a..83492f875 100644 --- a/__test__/pages/search/searchResults.test.tsx +++ b/__test__/pages/search/searchResults.test.tsx @@ -1,4 +1,5 @@ import React from "react" +import userEvent from "@testing-library/user-event" import { render, screen } from "@testing-library/react" import mockRouter from "next-router-mock" @@ -11,11 +12,14 @@ jest.mock("next/router", () => jest.requireActual("next-router-mock")) describe("Search Results page", () => { describe("More than 50 bibs", () => { - it("displays many bibs", () => { - const query = "spaghetti" + let query: string + + beforeEach(() => { + query = "spaghetti" mockRouter.push(`/search?q=${query}`) render() - + }) + it("displays many bibs", () => { const displayingText = screen.getByRole("heading", { level: 2 }) expect(displayingText).toHaveTextContent( `Displaying 1-50 of ${results.results.totalResults} results for keyword "${query}"` @@ -23,6 +27,25 @@ describe("Search Results page", () => { const cards = screen.getAllByRole("heading", { level: 3 }) expect(cards).toHaveLength(50) }) + it("renders the sort select field and updates the query string in the url on changes", async () => { + const sortSelect = screen.getByLabelText("Sort by") + expect(sortSelect).toHaveValue("relevance") + await userEvent.selectOptions(sortSelect, "Title (A - Z)") + expect(sortSelect).toHaveValue("title_asc") + + expect(mockRouter.asPath).toBe( + "/?q=spaghetti&sort=title&sort_direction=asc" + ) + }) + it("returns the user to the first page on sorting changes", async () => { + await mockRouter.push(`/search?q=${query}&page=2`) + const sortSelect = screen.getByLabelText("Sort by") + await userEvent.selectOptions(sortSelect, "Title (Z - A)") + + expect(mockRouter.asPath).toBe( + "/?q=spaghetti&sort=title&sort_direction=desc" + ) + }) }) describe("No bibs", () => { it("displays No results message", () => { diff --git a/pages/search/index.tsx b/pages/search/index.tsx index d87514405..f1189d6e3 100644 --- a/pages/search/index.tsx +++ b/pages/search/index.tsx @@ -1,5 +1,10 @@ import Head from "next/head" -import { Heading, SimpleGrid } from "@nypl/design-system-react-components" +import { + Heading, + SimpleGrid, + Select, +} from "@nypl/design-system-react-components" +import type { ChangeEvent } from "react" import { useRouter } from "next/router" import { parse } from "qs" @@ -11,7 +16,10 @@ import { fetchResults } from "../api/search" import { mapQueryToSearchParams, mapElementsToSearchResultsBibs, + getQueryString, + sortOptions, } from "../../src/utils/searchUtils" +import type { SortKey, SortOrder } from "../../src/types/searchTypes" import { mapWorksToDRBResults } from "../../src/utils/drbUtils" import { SITE_NAME } from "../../src/config/constants" import type SearchResultsBib from "../../src/models/SearchResultsBib" @@ -21,7 +29,7 @@ import type SearchResultsBib from "../../src/models/SearchResultsBib" * as well as displaying and controlling pagination and search filters. */ export default function Search({ results }) { - const { query } = useRouter() + const { push, query } = useRouter() const { itemListElement: searchResultsElements, totalResults } = results.results @@ -37,6 +45,19 @@ export default function Search({ results }) { // Map DRB Works from response to DRBResult objects const drbResults = mapWorksToDRBResults(drbWorks) + const handleSortChange = async (e: ChangeEvent) => { + const selectedSortOption = e.target.value + // Extract sort key and order from selected sort option using "_" delineator + const [sortBy, order] = selectedSortOption.split("_") as [ + SortKey, + SortOrder | undefined + ] + // Push the new query values, removing the page number if set. + await push( + getQueryString({ ...searchParams, sortBy, order, page: undefined }) + ) + } + return ( <> @@ -45,13 +66,35 @@ export default function Search({ results }) { - ) + <> + {totalResults && ( + + )} + {drbResponse?.totalWorks && ( + + )} + } > {totalResults ? ( diff --git a/src/models/Item.ts b/src/models/Item.ts index 70b3824dd..df25ef229 100644 --- a/src/models/Item.ts +++ b/src/models/Item.ts @@ -40,7 +40,7 @@ export default class Item { this.accessMessage = item.accessMessage?.length ? item.accessMessage[0]?.prefLabel : "" - this.callNumber = item.shelfMark.length ? item.shelfMark[0] : null + this.callNumber = item.shelfMark?.length ? item.shelfMark[0] : null this.volume = item.enumerationChronology?.length ? item.enumerationChronology[0] : null diff --git a/src/types/searchTypes.ts b/src/types/searchTypes.ts index e431130e5..a6692b173 100644 --- a/src/types/searchTypes.ts +++ b/src/types/searchTypes.ts @@ -33,8 +33,8 @@ export interface Identifiers { export interface SearchParams { q?: string field?: string - sortBy?: string - order?: string + sortBy?: SortKey + order?: SortOrder filters?: SearchFilters contributor?: string title?: string @@ -43,6 +43,9 @@ export interface SearchParams { identifiers?: Identifiers } +export type SortKey = "relevance" | "title" | "date" +export type SortOrder = "asc" | "desc" + type SearchFormField = { value: string } export interface SearchResultsResponse { @@ -86,8 +89,8 @@ export interface SearchQueryParams extends Identifiers { title?: string subject?: string filters?: SearchFilters - sort?: string - sort_direction?: string + sort?: SortKey + sort_direction?: SortOrder sort_scope?: string search_scope?: string page?: number diff --git a/src/utils/searchUtils.ts b/src/utils/searchUtils.ts index 2f3bd76ad..e1710c3d0 100644 --- a/src/utils/searchUtils.ts +++ b/src/utils/searchUtils.ts @@ -165,6 +165,18 @@ export function mapElementsToSearchResultsBibs( /* eslint-disable @typescript-eslint/naming-convention */ +/** + * sortOptions + * The allowed keys for the sort field and their respective labels + */ +export const sortOptions: Record = { + relevance: "Relevance", + title_asc: "Title (A - Z)", + title_desc: "Title (Z - A)", + date_asc: "Date (Old to New)", + date_desc: "Date (New to Old)", +} + /** * mapQueryToSearchParams * Maps the SearchQueryParams structure from the request to a SearchParams object, which is expected by fetchResults diff --git a/src/utils/utilsTests/searchUtils.test.ts b/src/utils/utilsTests/searchUtils.test.ts index 6e6c59b8a..8b1b99358 100644 --- a/src/utils/utilsTests/searchUtils.test.ts +++ b/src/utils/utilsTests/searchUtils.test.ts @@ -30,7 +30,7 @@ describe("searchUtils", () => { isbn: "456", search_scope: "contributor", sort_direction: "asc", - sort: "spaghetti", + sort: "relevance", }) ).toEqual({ identifiers: { @@ -39,7 +39,7 @@ describe("searchUtils", () => { }, field: "contributor", order: "asc", - sortBy: "spaghetti", + sortBy: "relevance", }) }) })