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

SCC-4236: Username for My Account 2.0 #397

Merged
merged 28 commits into from
Nov 26, 2024
Merged

Conversation

7emansell
Copy link
Contributor

@7emansell 7emansell commented Nov 20, 2024

Ticket:

Resolves JIRA ticket SCC-4236.

This PR does the following:

  • Adds UsernameForm to profile header
  • Adds api/account/username route to validate new patron usernames
  • Pulls out the settings status banner into a reusable StatusBanner

NOTE: the deletion process is still up for debate. As it is right now, the user can submit an empty string which works the same as using the trash icon to officially delete

**** Out of scope things that also happened in this PR: ****

  • Removes "discovery" from src/config (it's a duplicate of "platform")
  • Updates nyplApiClient to take an api version (defaults to v0.1)
    • We need this so we can use this client to hit validations/username in v0.3

How has this been tested?

Accessibility concerns or updates

Checklist:

  • I updated the CHANGELOG with the appropriate information and JIRA ticket number (if applicable).
  • I have added relevant accessibility documentation for this pull request.
  • All new and existing tests passed.

Copy link

vercel bot commented Nov 20, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
research-catalog ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 26, 2024 3:53pm

const nyplApiClient = async ({
apiName = "platform",
version = "v0.1",
} = {}) => {
Copy link
Contributor Author

@7emansell 7emansell Nov 20, 2024

Choose a reason for hiding this comment

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

Should this be more specific/different ? this is my first time using this so I'm not super familiar with its use across the repo, this didn't seem to break anything though

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean by specific? Also, can you explain what the = {} in the function signature does?
FWIW this looks fine by me!

Copy link
Contributor

Choose a reason for hiding this comment

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

@dgcohen maybe you can confirm if this looks kosher? the username validation endpoint requires v0.3, so i suggested emma make the version be a param. at the same time, i thought this was a good opportunity to trim away the duplicated discovery/platform urls in the config.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know actually how it could be more specific, just generally looking for confirmation that this makes sense from people who wrote/used this before

Copy link
Member

Choose a reason for hiding this comment

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

I think it's clear enough when you call the function what version is being used since you pass it as an argument. All tests also continue to pass so it shouldn't break anything.

One possible option to make this clearer (maybe?) is to make this a higher-order function. So it can be something like

const baseNyplApiClient = ({
  version = "v0.1",
} = {}) => {
  return async ({
    apiName = "platform",
  } = {}) => {
      if (CACHE.clients[apiName]) {
        return CACHE.clients[apiName]
      }
    
      const baseUrl =
        appConfig.apiEndpoints[apiName][appEnvironment] + "/" + version
    //...
  })
})

export const nyplApiClientv01 = baseNyplApiClient()
export const nyplApiClientv03 = baseNyplApiClient({ version: "v0.3" })

And then call either function like they're being called now such as const platformClient = await nyplApiClientv03().

Caveat: the syntax might be off and I haven't tested this. If you like this approach, do it in another PR because of its use across the repo. But what do you think, @charmingduchess ?

Copy link
Contributor

@charmingduchess charmingduchess Nov 25, 2024

Choose a reason for hiding this comment

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

I like the look of that!
I just noticed that with this updated client factory, we will need to update the cache to include the version in the cache key

export const StatusBanner = ({ status, statusMessage }: StatusBannerProps) => {
const bannerContent = (
<div style={{ alignItems: "center" }}>
{status === "failure" ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you pull out this conditional logic so it's not in nested ternaries, and illustrates the possibilities more clearly? Something along the lines of...

let statusComponent
if (status === 'success'){
  statusComponent = ...
} if (status === 'failure && statusMessage){
statusComponent = ...
else {
statusComponent = }
etc

potentially also with some descriptive variable names that point to when a statusMessage might be there or not

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lmk if this is better– I do think, since the parent component is responsible for passing the statusMessage, and there's several potential parents and reasons for passing a specific message, any explanation or indication of what that message will be should also live on the parent component if that makes sense

</Text>
</Flex>

{isLoading ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar request here. Can you replace this nested ternary with a conditional outside of the return statement so it's a easier to scan which components get rendered when?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Again lmk if this is better/ more readable

Copy link
Contributor

Choose a reason for hiding this comment

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

Same note here - the extraction is good, but the nested ternary itself is hard to read. Can you replace it?

src/components/MyAccount/ProfileHeader.tsx Show resolved Hide resolved
const nyplApiClient = async ({
apiName = "platform",
version = "v0.1",
} = {}) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean by specific? Also, can you explain what the = {} in the function signature does?
FWIW this looks fine by me!

const nyplApiClient = async ({
apiName = "platform",
version = "v0.1",
} = {}) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

@dgcohen maybe you can confirm if this looks kosher? the username validation endpoint requires v0.3, so i suggested emma make the version be a param. at the same time, i thought this was a good opportunity to trim away the duplicated discovery/platform urls in the config.

src/components/MyAccount/NewSettings/UsernameForm.test.tsx Outdated Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this extraction 🌝

Your changes were saved.
</Text>
)}
{status === "failure"
Copy link
Contributor

Choose a reason for hiding this comment

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

This is definitely better, but I'd really like to have this nested ternary replaced. Its ok if you end up with a conditional that seems redundant or repetitive, ie if failure && statusMessage, if failure && !statusMessage and if !failure. the conditional logic doesn't have to be inline of the return statement. in fact, it might have to come out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay ternary gone but I'd like to register my dissent... I think this makes it less readable both here and in the username form

</Text>
</Flex>

{isLoading ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

Same note here - the extraction is good, but the nested ternary itself is hard to read. Can you replace it?

Copy link
Member

@EdwinGuzman EdwinGuzman left a comment

Choose a reason for hiding this comment

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

NOTE: the deletion process is still up for debate. As it is right now, the user can submit an empty string which works the same as using the trash icon to officially delete

What's the debate? That there are two approaches for the same outcome?

This looks really good. There's the alignment issue in the description property when the username edits and I also left other comments.

Comment on lines +55 to +57
await client.put(`patrons/${patronId}`, {
varFields: [{ fieldTag: "u", content: newUsername }],
})
Copy link
Member

Choose a reason for hiding this comment

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

This is a general problem but I don't think it's a problem we'll encounter. This risks having race conditions if the username is available and then a separate request is made to update the patron with that username. If another patron is in the process of creating an account or updating their own username to the same one, then we'll have this race condition and error. This will be caught regardless on line 66 and also, there's really not much we can do anyway.
What you have here is fine, but wanted to point it out as possible error as an FYI in case it ever does come up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay good to note

// Username taken but not an error, returns a message.
return { status: 200, message: "Username taken" }
} else {
return { status: 500, message: "Username update failed" }
Copy link
Member

Choose a reason for hiding this comment

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

Why not throw an error here and return the error object on line 67?

src/components/MyAccount/NewSettings/StatusBanner.tsx Outdated Show resolved Hide resolved
Comment on lines 28 to 29
color: "ui.link.primary !important",
textDecorationColor: "ui.link.primary !important",
Copy link
Member

Choose a reason for hiding this comment

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

Two things:

  • I'm not sure this works, or rather, this should not work. I have not successfully added a design token value with !important. So this should be updated to "var(--nypl-colors-ui-link-primary) !important".
  • The above point should be resolved if you use the default style. Links in the negative Banner are meant to be red:
Screenshot 2024-11-22 at 5 43 52 PM

Are they blue in the designs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are blue in the designs and this does work but i'll change it

src/server/api/bib.ts Show resolved Hide resolved
const nyplApiClient = async ({
apiName = "platform",
version = "v0.1",
} = {}) => {
Copy link
Member

Choose a reason for hiding this comment

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

I think it's clear enough when you call the function what version is being used since you pass it as an argument. All tests also continue to pass so it shouldn't break anything.

One possible option to make this clearer (maybe?) is to make this a higher-order function. So it can be something like

const baseNyplApiClient = ({
  version = "v0.1",
} = {}) => {
  return async ({
    apiName = "platform",
  } = {}) => {
      if (CACHE.clients[apiName]) {
        return CACHE.clients[apiName]
      }
    
      const baseUrl =
        appConfig.apiEndpoints[apiName][appEnvironment] + "/" + version
    //...
  })
})

export const nyplApiClientv01 = baseNyplApiClient()
export const nyplApiClientv03 = baseNyplApiClient({ version: "v0.3" })

And then call either function like they're being called now such as const platformClient = await nyplApiClientv03().

Caveat: the syntax might be off and I haven't tested this. If you like this approach, do it in another PR because of its use across the repo. But what do you think, @charmingduchess ?

</List>
<>
{usernameStatus !== "" && (
<div
Copy link
Member

Choose a reason for hiding this comment

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

I think we talked about this in a different PR, check with Clare if the banner message is read. In the Dynamic Banners we say that this wrapper div should always be rendered and have an aria-live attribute. If Clare signs off on this, let me know so we can update the docs.

}}
value={tempInput || ""}
id="username-input"
labelText="Must be 5-15 characters and use only letters (a-z) and numbers (0-9)"
Copy link
Member

Choose a reason for hiding this comment

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

The space between the input and label seems too small, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, j added more styling

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's like a mix of the label and helper text on this component I'm not sure how design arrived at this

Copy link
Contributor

Choose a reason for hiding this comment

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

If there is a default that makes sense functionally (like the helper text) coming from reservoir that deviates from the designs in a small way, can you ask Apoorva if it's alright to use the default instead of overriding?

Copy link
Member

Choose a reason for hiding this comment

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

Okay, I see what's going on. I thought this was the helperErrorText because it was below the input field but you're reversing the order on line 117. I don't understand why this was done.

You can try target the helperErrorText for styling purposes. Can you try having the text value in the helper text prop and invalid text prop? Then you can update the color with the isInvalid prop.

My concern is that a screenreader would read the "Must be 5-15 characters and use only letters (a-z) and numbers (0-9)" text but that's not necessarily clear that it's for the username. So a hidden "username" label, I think, is better. Let's chat about this.

flexDirection: "column-reverse",
label: {
fontWeight: "400",
color: error ? "ui.error.primary" : "ui.black",
Copy link
Member

Choose a reason for hiding this comment

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

This should happen automatically when isInvalid is true. Or is this because the additional !validateUsername check below affects the text color?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is because the label and the invalid text are the same copy, and render in the same place (below the field). Since that's not how this component expects to work, I'm manually making the label text become the invalid text when needed

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you use helperText here instead of labelText? It looks like the helper text is automatically with the correct styling for this.

* Within this form, the user NOT having a username is represented by: username = null, so the
* empty string is an invalid username.
*/
const [input, setInput] = useState(
Copy link
Contributor

@charmingduchess charmingduchess Nov 25, 2024

Choose a reason for hiding this comment

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

Can you rename this variable to usernameInSierra? It seems like it's not actually being updated as part of the controlled text input.

const client = await sierraClient()
if (newUsername === "") {
const client = await sierraClient()
await client.put(`patrons/${patronId}`, {
Copy link
Member

Choose a reason for hiding this comment

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

Does this return a response to verify it was successful? Or is it a quiet "there was no error so it's good" validation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes just 200 if successful, no body

}

const successContent = (
<Text marginBottom={0} color="ui.black !important">
Copy link
Member

Choose a reason for hiding this comment

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

Just to verify, the design token with the !important flag does work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes

Comment on lines 28 to 29
color: "ui.link.primary !important",
textDecorationColor: "var(--nypl-colors-ui-link-primary) !important",
Copy link
Member

Choose a reason for hiding this comment

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

If both works, great, but pick one for consistency, either the design token or the CSS var.


fireEvent.click(screen.getByRole("button", { name: /save changes/i }))

await waitFor(() => expect(fetch).toHaveBeenCalledTimes(2))
Copy link
Member

Choose a reason for hiding this comment

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

The second expect is getMostUpdatedSierraAccountData, correct?

}}
value={tempInput || ""}
id="username-input"
labelText="Must be 5-15 characters and use only letters (a-z) and numbers (0-9)"
Copy link
Member

Choose a reason for hiding this comment

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

Okay, I see what's going on. I thought this was the helperErrorText because it was below the input field but you're reversing the order on line 117. I don't understand why this was done.

You can try target the helperErrorText for styling purposes. Can you try having the text value in the helper text prop and invalid text prop? Then you can update the color with the isInvalid prop.

My concern is that a screenreader would read the "Must be 5-15 characters and use only letters (a-z) and numbers (0-9)" text but that's not necessarily clear that it's for the username. So a hidden "username" label, I think, is better. Let's chat about this.

)

const notEditingView = (
<Flex alignItems="center" marginTop={{ base: "unset", md: "-xs" }}>
Copy link
Member

Choose a reason for hiding this comment

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

Did not know or think that -xs would work.

const [isEditing, setIsEditing] = useState(false)
const [error, setError] = useState(false)
/**
* In Sierra, the user NOT having a username is represented by the empty string: username = "".
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for adding this comment here.

I'd like to also see this elsewhere but I don't know where yet...

@7emansell 7emansell merged commit 865b452 into my-account-2.0 Nov 26, 2024
3 checks passed
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.

3 participants