Skip to content

Commit

Permalink
Merge branch 'dec-24-release' into SCC-4334/replace-owning-institution
Browse files Browse the repository at this point in the history
  • Loading branch information
charmingduchess authored Dec 3, 2024
2 parents cd844d7 + 3b52f1f commit df63da5
Show file tree
Hide file tree
Showing 50 changed files with 2,482 additions and 1,539 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Updated
## Prerelease

### Updated

- Pull in Owning institution from items to populate bib details ([SCC-4334](https://newyorkpubliclibrary.atlassian.net/browse/SCC-4334))
- Refactor bib details model so most non-url properties return value, not object with value and label
- add call number and standard numbers to advanced search ([SCC-4326](https://newyorkpubliclibrary.atlassian.net/browse/SCC-4326))

## Unreleased

### Updated

- Updated phone, email, notification preference and home library to be individually editable in Account Settings (SCC-4337, SCC-4254, SCC-4253)
- Updated username to be editable in My Account header (SCC-4236)

## [1.3.6] 2024-11-6

Expand Down
16 changes: 0 additions & 16 deletions __test__/pages/account/account.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,22 +231,6 @@ describe("MyAccount page", () => {
const result = await getServerSideProps({ req: req, res: mockRes })
expect(result.props.tabsPath).toBe("overdues")
})
it("can handle no username", () => {
render(
<MyAccount
isAuthenticated={true}
accountData={{
checkouts: processedCheckouts,
holds: processedHolds,
patron: { ...processedPatron, username: undefined },
fines: processedFines,
pickupLocations: filteredPickupLocations,
}}
/>
)
const username = screen.queryByText("Username")
expect(username).toBeNull()
})
it("renders notification banner if user has fines", () => {
render(
<MyAccount
Expand Down
43 changes: 23 additions & 20 deletions __test__/pages/search/advancedSearchForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { fireEvent, render, screen } from "../../../src/utils/testUtils"
import mockRouter from "next-router-mock"
import userEvent from "@testing-library/user-event"

import { textInputFields } from "../../../src/utils/advancedSearchUtils"
import AdvancedSearch, {
defaultEmptySearchErrorMessage,
} from "../../../pages/search/advanced"
Expand All @@ -12,6 +13,9 @@ import { searchAggregations } from "../../../src/config/aggregations"
jest.mock("next/router", () => jest.requireActual("next-router-mock"))

describe("Advanced Search Form", () => {
beforeEach(async () => {
render(<AdvancedSearch isAuthenticated={true} />)
})
const submit = () => {
fireEvent(
screen.getByTestId("submit-advanced-search-button"),
Expand All @@ -22,8 +26,6 @@ describe("Advanced Search Form", () => {
await userEvent.click(screen.getByText("Clear fields"))
})
it("displays alert when no fields are submitted", () => {
render(<AdvancedSearch isAuthenticated={true} />)

submit()
screen.getByText(defaultEmptySearchErrorMessage)
})
Expand All @@ -32,32 +34,36 @@ describe("Advanced Search Form", () => {
// final input in output string in test. the broken test is
// commented out below.
it.todo("can set keyword, contributor, title, subject")
// async () => {
// render(<AdvancedSearch isAuthenticated={true}/>)
// , async () => {
//

// const [keywordInput, contributorInput, titleInput, subjectInput] = [
// "Keywords",
// "Keyword",
// "Title",
// "Author",
// "Subject",
// "Call number",
// "Unique identifier",
// ].map((field) => screen.getByLabelText(field))
// await act(async () => {
// await userEvent.type(subjectInput, "italian food")
// await userEvent.type(keywordInput, "spaghetti")
// await userEvent.type(contributorInput, "strega nonna")
// await userEvent.type(titleInput, "il amore di pasta")
// // this set stimeout is to ad
// // eslint-disable-next-line @typescript-eslint/no-empty-function
// setTimeout(() => {}, 300)
// submit()
// fireEvent.change(subjectInput, { target: { value: "italian food" } })
// fireEvent.change(keywordInput, { target: { value: "spaghetti" } })
// fireEvent.change(contributorInput, { target: { value: "strega nonna" } })
// fireEvent.change(titleInput, { target: { value: "il amore di pasta" } })
// submit()
// await waitFor(() =>
// expect(mockRouter.asPath).toBe(
// "/search?q=spaghetti&contributor=il+amore+di+pasta&title=strega+nonna&subject=italian+food"
// )
// })
// )
// })
it("can select languages", async () => {
render(<AdvancedSearch isAuthenticated={true} />)
it("renders inputs for all text input fields", () => {
textInputFields.map(({ label }) => {
const input = screen.getByLabelText(label)
expect(input).toBeInTheDocument()
})
})

it("can select languages", async () => {
const languageSelect = screen.getByLabelText("Language")
await userEvent.selectOptions(languageSelect, "Azerbaijani")
submit()
Expand All @@ -67,7 +73,6 @@ describe("Advanced Search Form", () => {
)
})
it("can check material checkboxes", async () => {
render(<AdvancedSearch isAuthenticated={true} />)
await userEvent.click(screen.getByLabelText("Notated music"))
await userEvent.click(screen.getByLabelText("Cartographic"))
submit()
Expand All @@ -78,7 +83,6 @@ describe("Advanced Search Form", () => {
)
})
it("can check location checkboxes", async () => {
render(<AdvancedSearch isAuthenticated={true} />)
const location = searchAggregations.buildingLocation[0]
await userEvent.click(screen.getByLabelText(location.label as string))
submit()
Expand All @@ -88,7 +92,6 @@ describe("Advanced Search Form", () => {
})

it("can clear the form", async () => {
render(<AdvancedSearch isAuthenticated={true} />)
const notatedMusic = screen.getByLabelText("Notated music")
await userEvent.click(notatedMusic)
const cartographic = screen.getByLabelText("Cartographic")
Expand Down
3 changes: 2 additions & 1 deletion accountREADME.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ Route parameter is the patron ID. Request body can include any fields on the pat
exampleBody: {
emails: ['[email protected]'],
phones: [6466600432]
phones: [12345678],
homeLibraryCode: 'sn'
},
```
Expand Down
9 changes: 6 additions & 3 deletions pages/account/[[...index]].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default function MyAccount({
assistance.
</Text>
)

useEffect(() => {
resetCountdown()
// to avoid a reference error on document in the modal, wait to render it
Expand Down Expand Up @@ -111,16 +112,17 @@ export async function getServerSideProps({ req, res }) {
},
}
}
// Parsing path from url to pass to ProfileTabs.

// Parsing path from URL
const tabsPathRegex = /\/account\/(.+)/
const match = req.url.match(tabsPathRegex)
const tabsPath = match ? match[1] : null
const id = patronTokenResponse.decodedPatron.sub

try {
const { checkouts, holds, patron, fines, pickupLocations } =
await getPatronData(id)
/* Redirecting invalid paths (including /overdues if user has none) and
// cleaning extra parts off valid paths. */
// Redirecting invalid paths and cleaning extra parts off valid paths.
if (tabsPath) {
const allowedPaths = ["items", "requests", "overdues", "settings"]
if (
Expand All @@ -147,6 +149,7 @@ export async function getServerSideProps({ req, res }) {
}
}
}

return {
props: {
accountData: { checkouts, holds, patron, fines, pickupLocations },
Expand Down
47 changes: 47 additions & 0 deletions pages/api/account/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sierraClient from "../../../src/server/sierraClient"
import type { HTTPResponse } from "../../../src/types/appTypes"
import nyplApiClient from "../../../src/server/nyplApiClient"

/**
* PUT request to Sierra to update patron PIN, first validating with previous PIN.
Expand Down Expand Up @@ -27,6 +28,52 @@ export async function updatePin(
}
}

/**
* PUT request to Sierra to update patron username, first validating that it's available.
* Returns status and message about request.
*/
export async function updateUsername(
patronId: string,
newUsername: string
): Promise<HTTPResponse> {
try {
// If the new username is an empty string, skips validation and directly updates in Sierra.
const client = await sierraClient()
if (newUsername === "") {
const client = await sierraClient()
await client.put(`patrons/${patronId}`, {
varFields: [{ fieldTag: "u", content: newUsername }],
})
return { status: 200, message: "Username removed successfully" }
} else {
const platformClient = await nyplApiClient({ version: "v0.3" })
const response = await platformClient.post("/validations/username", {
username: newUsername,
})

if (response?.type === "available-username") {
await client.put(`patrons/${patronId}`, {
varFields: [{ fieldTag: "u", content: newUsername }],
})
return { status: 200, message: `Username updated to ${newUsername}` }
} else if (response?.type === "unavailable-username") {
// Username taken but not an error, returns a message.
return { status: 200, message: "Username taken" }
} else {
throw new Error("Username update failed")
}
}
} catch (error) {
return {
status: error?.status || 500,
message:
error?.message ||
error.response?.data?.description ||
"An error occurred",
}
}
}

/**
* PUT request to Sierra to update patron settings. Returns status and message about request.
*/
Expand Down
1 change: 1 addition & 0 deletions pages/api/account/settings/[id].ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default async function handler(
if (req.method == "GET") {
responseMessage = "Please make a PUT request to this endpoint."
}

if (req.method == "PUT") {
/** We get the patron id from the request: */
const patronId = req.query.id as string
Expand Down
40 changes: 40 additions & 0 deletions pages/api/account/username/[id].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { NextApiResponse, NextApiRequest } from "next"
import initializePatronTokenAuth from "../../../../src/server/auth"
import { updateUsername } from "../helpers"

/**
* API route handler for /api/account/username/{patronId}
*/
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
let responseMessage = "Request error"
let responseStatus = 400
const patronTokenResponse = await initializePatronTokenAuth(req.cookies)
const cookiePatronId = patronTokenResponse.decodedPatron?.sub
if (!cookiePatronId) {
responseStatus = 403
responseMessage = "No authenticated patron"
return res.status(responseStatus).json(responseMessage)
}
if (req.method == "GET") {
responseMessage = "Please make a PUT request to this endpoint."
}
if (req.method == "PUT") {
/** We get the patron id from the request: */
const patronId = req.query.id as string
const { username } = req.body
/** We check that the patron cookie matches the patron id in the request,
* i.e.,the logged in user is updating their own username. */
if (patronId == cookiePatronId) {
const response = await updateUsername(patronId, username)
responseStatus = response.status
responseMessage = response.message
} else {
responseStatus = 403
responseMessage = "Authenticated patron does not match request"
}
}
res.status(responseStatus).json(responseMessage)
}
6 changes: 2 additions & 4 deletions pages/search/advanced.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ export default function AdvancedSearch({
e.preventDefault()
alert && setAlert(false)
const target = e.target as HTMLInputElement

dispatch({
type: type,
field: target.name,
Expand All @@ -106,7 +105,6 @@ export default function AdvancedSearch({
e.preventDefault()
if (!validateDateRange()) return
const queryString = getSearchQuery(searchFormState as SearchParams)

if (!queryString.length) {
setErrorMessage(defaultEmptySearchErrorMessage)
setAlert(true)
Expand Down Expand Up @@ -164,7 +162,7 @@ export default function AdvancedSearch({
onSubmit={handleSubmit}
>
<Flex flexDirection={{ base: "column", md: "row" }}>
<Flex id="advancedSearchLeft" gap="s" direction="column">
<Flex id="advancedSearchLeft" gap="s" direction="column" grow="1">
{textInputFields.map(({ name, label }) => {
return (
<FormField key={name}>
Expand Down Expand Up @@ -201,7 +199,7 @@ export default function AdvancedSearch({
</FormField>
<FormField>{<DateForm {...dateFormProps} />}</FormField>
</Flex>
<Flex direction="column" gap="l">
<Flex direction="column" gap="l" grow="1">
<SearchFilterCheckboxField
options={searchAggregations.buildingLocation}
name="location"
Expand Down
2 changes: 1 addition & 1 deletion src/components/MyAccount/IconListElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface IconListElementPropType {

// This component is designed to centralize common styling patterns for a
// description type List with icons
const IconListElement = ({
export const IconListElement = ({
icon,
term,
description,
Expand Down
Loading

0 comments on commit df63da5

Please sign in to comment.