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

Account's wishlist: integrate hooks #1042

Merged
merged 41 commits into from
Mar 15, 2023

Conversation

vmarta
Copy link
Contributor

@vmarta vmarta commented Mar 8, 2023

Progress so far:

  • For editing a wishlist item: tweak the cache-update logic so it also selectively updates the entire lists data
  • The wishlist page is now loading properly

What's next:

  • Continue replacing more SCAPI calls, in particular the button-click event handlers
  • Fix the failing tests

Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (could cause existing functionality to not work as expected)
  • Other changes (non-breaking changes that does not fit any of the above)

Breaking changes include:

  • Removing a public function or component or prop
  • Adding a required argument to a function
  • Changing the data type of a function parameter or return value
  • Adding a new peer dependency to package.json

Changes

  • (change1)

How to Test-Drive This PR

  • (step1)

Checklists

General

  • Changes are covered by test cases
  • CHANGELOG.md updated with a short description of changes (not required for documentation updates)

Accessibility Compliance

You must check off all items in one of the follow two lists:

  • There are no changes to UI

or...

Localization

  • Changes include a UI text update in the Retail React App (which requires translation)

Comment on lines 148 to 150
const clone: ShopperCustomersTypes.CustomerProductListResult = JSON.parse(
JSON.stringify(oldData)
)
Copy link
Collaborator

@kevinxh kevinxh Mar 8, 2023

Choose a reason for hiding this comment

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

JSON.parse has performance issues... Does the suggestion below work for typescript?

Suggested change
const clone: ShopperCustomersTypes.CustomerProductListResult = JSON.parse(
JSON.stringify(oldData)
)
const clone: ShopperCustomersTypes.CustomerProductListResult = {...oldData}

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 had {...oldData} before, but it was just a shallow clone. I wanted a deep clone, since the property to update is nested.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here's the most concise (but maybe not the most readable) way to implement this logic. General idea is "Shallow clone the object, deep clone the array, but replace the array item with matching ID" for both the list of lists and for the list of items.

return {
  ...oldData,
  data: oldData.data.map((list) =>
    list.id !== listId
      ? list
      : {
        ...list,
        customerProductListItems:
          list.customerProductListItems?.map((item) =>
            item.id !== itemId ? item : response
          )
        }
  )
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

❤️ Thanks Will! Not only it's more concise, but I think it's more readable too.

Comment on lines 148 to 150
const clone: ShopperCustomersTypes.CustomerProductListResult = JSON.parse(
JSON.stringify(oldData)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here's the most concise (but maybe not the most readable) way to implement this logic. General idea is "Shallow clone the object, deep clone the array, but replace the array item with matching ID" for both the list of lists and for the list of items.

return {
  ...oldData,
  data: oldData.data.map((list) =>
    list.id !== listId
      ? list
      : {
        ...list,
        customerProductListItems:
          list.customerProductListItems?.map((item) =>
            item.id !== itemId ? item : response
          )
        }
  )
}

// TODO: make separate PR that merges into develop
{
// Also selectively update the lists
queryKey: getCustomerProductLists.queryKey(parameters),
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't forget to remove the query from the invalidate list; invalidate takes precedence over update. (Technically, it will update then invalidate, which negates the point of updating.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, after the list item is updated, the product lists become somewhat stale. For example, the lastModified time is no longer true.

So I prefer to also invalidate the product lists. Yes, most of the data can be redundant, but the refetch is going to be done in the background. In some way, I think of it as similar to stale-while-revalidate strategy.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we update and invalidate at the same time, does react query ever show the updated data? I thought that it just keeps the old data until the refetch completes. (I could very easily be wrong.)

Also, I just noticed that we update/invalidate the lists endpoint, but we just invalidate the list endpoint. We should follow the same pattern for both.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You brought up good points there. To make things simpler, I've reverted the cache-update logic.

This article mentions that it's safer and simpler to just invalidate, rather than update. And I can see that here in our case.

So what now? There's actually another way to achieve what I wanted: via local state update in the quantity picker component. Take a look at this commit for details: 487c775

@vmarta vmarta marked this pull request as ready for review March 14, 2023 01:03
@vmarta vmarta requested a review from a team as a code owner March 14, 2023 01:03

const addToCartButton = getByRole('button', {
const addToCartButton = await screen.findByRole('button', {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nice await!

toast({title: formatMessage(API_ERROR_MESSAGE), status: 'error'})
},
onSettled: () => {
onClick('')
Copy link
Collaborator

Choose a reason for hiding this comment

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

why? I see that we previously do this but don't understand why, maybe it's worth adding a comment on this.

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 so the callee can know when something has ended (and give you a chance to remove the loading spinner, for example). I've updated the code so it's clearer now.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not entirely sure if i understand, it's really weird to the caller that when user clicks the button, onClick actually fires twice!

Copy link
Contributor Author

@vmarta vmarta Mar 15, 2023

Choose a reason for hiding this comment

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

🤔 maybe instead of onClick prop, we have 2 props?

  • onActionStart
  • onActionEnd

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kevinxh All right, I've converted the onClick into those 2 events to be clearer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

sorry, i just saw this now... You could actually make onClick a promise.

const removeItemMock = jest.fn()
renderWithProviders(<MockedComponent onClick={removeItemMock} />)

const removeButton = await screen.findByRole('button', {
Copy link
Collaborator

Choose a reason for hiding this comment

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

more awaits! good stuff!

onClick('')
}
const handleItemRemove = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

async with no await?


const handleActionClicked = (itemId) => {
setWishlistItemLoading(!!itemId)
setWishlistItemLoading(Boolean(itemId))
Copy link
Contributor

Choose a reason for hiding this comment

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

👏


const {data: productsData, isLoading: isProductsLoading} = useProducts(
{parameters: {ids: productIds?.join(','), allImages: true}},
{enabled: Boolean(productIds)}
Copy link
Contributor

Choose a reason for hiding this comment

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

This will send a request if productIds is an empty array. Is that desired behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I'll update it to check for an array and that it's not empty.

// TODO: make separate PR that merges into develop
{
// Also selectively update the lists
queryKey: getCustomerProductLists.queryKey(parameters),
Copy link
Contributor

Choose a reason for hiding this comment

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

If we update and invalidate at the same time, does react query ever show the updated data? I thought that it just keeps the old data until the refetch completes. (I could very easily be wrong.)

Also, I just noticed that we update/invalidate the lists endpoint, but we just invalidate the list endpoint. We should follow the same pattern for both.

quantity: localQuantity[item.productId]
? localQuantity[item.productId]
: item.quantity
quantity: item.quantity
Copy link
Collaborator

Choose a reason for hiding this comment

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

The code in develop removes the item from the wishlist when the shopper decrements the product quantity < 1, when taking either of these two actions in the QuantityPicker:

  1. Typing the number '0' in the quantity Input field.
  2. Pressing the '-' button when the quantity is 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, the smart deletion logic originally comes from:

updateListItem: async (listId, item) => {
const {id, quantity} = item
if (quantity === 0) {
await removeListItem(listId, id)
actions.removeListItem(listId, id)
return
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI I've implemented that.

import {useToast} from '../../../hooks/use-toast'
import {useWishList} from '../../../hooks/use-wish-list'
Copy link
Collaborator

Choose a reason for hiding this comment

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

On develop when the shopper clicks the "Add to Wishlist" button on one of the items of a product set, the individual item is added to the wishlist, whereas now the entire set is added to the wishlist instead of the individual product.

Example product set:
https://pwa-kit.mobify-storefront.com/global/en-GB/product/fall-lookM

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm strange, that might be a regression after resolving merge conflicts.. let me investigate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok I've fixed this issue.

vmarta added 4 commits March 14, 2023 16:44
But still achieve the same result via local state update within the component.
Bring back this original feature.
You can add the parent or child items to the wishlist.
Comment on lines +83 to +84
? updateCustomerProductListItem.mutateAsync({body, parameters})
: deleteCustomerProductListItem.mutateAsync({parameters})
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if you do update with 0? Feels like the back end should be capable of handling the deletion logic when quantity is 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What would happen is the back end would accept 0 as the minimum quantity, but it does not remove the item from the list.

Comment on lines 48 to 49
onActionStart = noop,
onActionEnd = noop
Copy link
Collaborator

@kevinxh kevinxh Mar 15, 2023

Choose a reason for hiding this comment

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

nit, if onClick is a promise, then the consumer will know when the action ends. An example in our code base is the action card component's onRemove prop

@@ -129,7 +129,7 @@ const ProductDetail = () => {
body: {
// NOTE: APi does not respect quantity, it always adds 1
quantity,
productId: urlParams.get('pid') || productId,
productId: variant?.productId || product?.id,
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 a bug fix for making sure that adding to wishlist works correctly for a product set: you can add the parent item to the wishlist or the individual child items.

@vmarta vmarta merged commit 1e655f1 into feature/integrate-commerce-sdk-react Mar 15, 2023
@vmarta vmarta deleted the hooks-account-wishlist branch March 15, 2023 23:32
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.

4 participants