Skip to content

Commit

Permalink
Merge branch 'main' of github.com:NYPL/research-catalog into hold-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
dgcohen committed Dec 9, 2024
2 parents 3221c1c + 8c4bf84 commit 42f1862
Show file tree
Hide file tree
Showing 45 changed files with 2,409 additions and 1,469 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ 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).

## 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

## Added
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
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)
}
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
62 changes: 48 additions & 14 deletions src/components/MyAccount/ProfileHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Banner,
Box,
List,
useNYPLBreakpoints,
Expand All @@ -10,17 +11,37 @@ import styles from "../../../styles/components/MyAccount.module.scss"
import type { Patron } from "../../types/myAccountTypes"
import type { IconListElementPropType } from "./IconListElement"
import { buildListElementsWithIcons } from "./IconListElement"
import UsernameForm from "./Settings/UsernameForm"
import { useEffect, useRef, useState } from "react"
import type { StatusType } from "./Settings/StatusBanner"
import { StatusBanner } from "./Settings/StatusBanner"

const ProfileHeader = ({ patron }: { patron: Patron }) => {
const { isLargerThanMobile } = useNYPLBreakpoints()
const [usernameStatus, setUsernameStatus] = useState<StatusType>("")
const [usernameStatusMessage, setUsernameStatusMessage] = useState<string>("")
const usernameBannerRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (usernameStatus !== "" && usernameBannerRef.current) {
usernameBannerRef.current.focus()
}
}, [usernameStatus])

const usernameState = {
setUsernameStatus,
setUsernameStatusMessage,
}

const profileData = (
[
{ icon: "actionIdentityFilled", term: "Name", description: patron.name },
{
icon: "actionIdentity",
term: "Username",
description: patron.username,
description: (
<UsernameForm patron={patron} usernameState={usernameState} />
),
},
{
icon: "actionPayment",
Expand Down Expand Up @@ -53,19 +74,32 @@ const ProfileHeader = ({ patron }: { patron: Patron }) => {
.map(buildListElementsWithIcons)

return (
<List
className={styles.myAccountList}
id="my-account-profile-header"
title="My Account"
type="dl"
sx={{
border: "none",
h2: { border: "none", paddingTop: 0 },
marginBottom: "xxl",
}}
>
{profileData}
</List>
<>
{usernameStatus !== "" && (
<div
ref={usernameBannerRef}
tabIndex={-1}
style={{ marginBottom: "32px" }}
>
<StatusBanner
status={usernameStatus}
statusMessage={usernameStatusMessage}
/>
</div>
)}
<List
className={styles.myAccountList}
id="my-account-profile-header"
title="My Account"
type="dl"
sx={{
border: "none",
h2: { border: "none", paddingTop: 0 },
}}
>
{profileData}
</List>
</>
)
}

Expand Down
4 changes: 2 additions & 2 deletions src/components/MyAccount/ProfileTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Tabs, Text } from "@nypl/design-system-react-components"
import { useRouter } from "next/router"

import AccountSettingsTab from "./Settings/AccountSettingsTab"
import CheckoutsTab from "./CheckoutsTab/CheckoutsTab"
import RequestsTab from "./RequestsTab/RequestsTab"
import FeesTab from "./FeesTab/FeesTab"
import { PatronDataContext } from "../../context/PatronDataContext"
import { useContext } from "react"
import NewAccountSettingsTab from "./Settings/NewAccountSettingsTab"

interface ProfileTabsPropsType {
activePath: string
Expand Down Expand Up @@ -49,7 +49,7 @@ const ProfileTabs = ({ activePath }: ProfileTabsPropsType) => {
: []),
{
label: "Account settings",
content: <AccountSettingsTab />,
content: <NewAccountSettingsTab />,
urlPath: "settings",
},
]
Expand Down
51 changes: 0 additions & 51 deletions src/components/MyAccount/Settings/AccountSettingsButtons.tsx

This file was deleted.

Loading

0 comments on commit 42f1862

Please sign in to comment.