-
Notifications
You must be signed in to change notification settings - Fork 142
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
Changes from 39 commits
11b6261
c862ffe
3923dc8
0b444d5
d72419c
a82ae6d
fbf6cba
428c3a3
6d8d9bb
a35170d
4dd1661
7fe8af0
dff593a
e51439c
5524928
da6129e
7900f52
907401a
c6ad3fc
3de2b35
49eb095
fa90831
20d0c2e
1f307c0
20c2186
a7fad37
1bd4a5c
f5159e2
6bd9593
3c47cf8
487c775
438a45c
df6e823
3792643
67c339f
1643906
60aecdf
b5c1db5
9fd05e0
c73b2e5
7c6e45e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -4,15 +4,15 @@ | |||||||||||||||
* SPDX-License-Identifier: BSD-3-Clause | ||||||||||||||||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||||||||||||||||
*/ | ||||||||||||||||
import React, {useEffect, useState} from 'react' | ||||||||||||||||
import React, {useState} from 'react' | ||||||||||||||||
import {Stack, Heading} from '@chakra-ui/layout' | ||||||||||||||||
import {FormattedMessage, useIntl} from 'react-intl' | ||||||||||||||||
import {Box, Flex, Skeleton} from '@chakra-ui/react' | ||||||||||||||||
import {useProducts, useShopperCustomersMutation} from 'commerce-sdk-react-preview' | ||||||||||||||||
|
||||||||||||||||
import useCustomer from '../../../commerce-api/hooks/useCustomer' | ||||||||||||||||
import useNavigation from '../../../hooks/use-navigation' | ||||||||||||||||
import useWishlist from '../../../hooks/use-wishlist' | ||||||||||||||||
import {useToast} from '../../../hooks/use-toast' | ||||||||||||||||
import {useWishList} from '../../../hooks/use-wish-list' | ||||||||||||||||
|
||||||||||||||||
import PageActionPlaceHolder from '../../../components/page-action-placeholder' | ||||||||||||||||
import {HeartIcon} from '../../../components/icons' | ||||||||||||||||
|
@@ -21,69 +21,101 @@ import WishlistPrimaryAction from './partials/wishlist-primary-action' | |||||||||||||||
import WishlistSecondaryButtonGroup from './partials/wishlist-secondary-button-group' | ||||||||||||||||
|
||||||||||||||||
import {API_ERROR_MESSAGE} from '../../../constants' | ||||||||||||||||
import {useCurrentCustomer} from '../../../hooks/use-current-customer' | ||||||||||||||||
|
||||||||||||||||
const numberOfSkeletonItems = 3 | ||||||||||||||||
|
||||||||||||||||
const AccountWishlist = () => { | ||||||||||||||||
const customer = useCustomer() | ||||||||||||||||
const navigate = useNavigation() | ||||||||||||||||
const {formatMessage} = useIntl() | ||||||||||||||||
const toast = useToast() | ||||||||||||||||
|
||||||||||||||||
const [selectedItem, setSelectedItem] = useState(undefined) | ||||||||||||||||
const [localQuantity, setLocalQuantity] = useState({}) | ||||||||||||||||
const [isWishlistItemLoading, setWishlistItemLoading] = useState(false) | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still need this state? can we replace this by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. That state is still necessary in 1 case (removing an item), but for another case (updating quantity), I've updated it so it used the mutation's loading state instead. (The one case where it's still necessary is because the mutation lives in a different file/component) |
||||||||||||||||
const wishlist = useWishlist() | ||||||||||||||||
|
||||||||||||||||
const handleActionClicked = (itemId) => { | ||||||||||||||||
setWishlistItemLoading(!!itemId) | ||||||||||||||||
const {data: wishListData, isLoading: isWishListLoading} = useWishList() | ||||||||||||||||
const productIds = wishListData?.customerProductListItems?.map((item) => item.productId) | ||||||||||||||||
|
||||||||||||||||
const {data: productsData, isLoading: isProductsLoading} = useProducts( | ||||||||||||||||
{parameters: {ids: productIds?.join(','), allImages: true}}, | ||||||||||||||||
{enabled: productIds?.length > 0} | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
const wishListItems = wishListData?.customerProductListItems?.map((item, i) => { | ||||||||||||||||
return { | ||||||||||||||||
...item, | ||||||||||||||||
product: productsData?.data?.[i] | ||||||||||||||||
} | ||||||||||||||||
}) | ||||||||||||||||
|
||||||||||||||||
const updateCustomerProductListItem = useShopperCustomersMutation( | ||||||||||||||||
'updateCustomerProductListItem' | ||||||||||||||||
) | ||||||||||||||||
const deleteCustomerProductListItem = useShopperCustomersMutation( | ||||||||||||||||
'deleteCustomerProductListItem' | ||||||||||||||||
) | ||||||||||||||||
const {data: customer} = useCurrentCustomer() | ||||||||||||||||
|
||||||||||||||||
const handleSecondaryAction = async (itemId, promise) => { | ||||||||||||||||
setWishlistItemLoading(true) | ||||||||||||||||
setSelectedItem(itemId) | ||||||||||||||||
|
||||||||||||||||
try { | ||||||||||||||||
await promise | ||||||||||||||||
// No need to handle error here, as the inner component will take care of it | ||||||||||||||||
} finally { | ||||||||||||||||
setWishlistItemLoading(false) | ||||||||||||||||
setSelectedItem(undefined) | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
const handleItemQuantityChanged = async (quantity, item) => { | ||||||||||||||||
// This local state allows the dropdown to show the desired quantity | ||||||||||||||||
// while the API call to update it is happening. | ||||||||||||||||
setLocalQuantity({...localQuantity, [item.productId]: quantity}) | ||||||||||||||||
setWishlistItemLoading(true) | ||||||||||||||||
let isValidChange = false | ||||||||||||||||
setSelectedItem(item.productId) | ||||||||||||||||
|
||||||||||||||||
const body = { | ||||||||||||||||
...item, | ||||||||||||||||
quantity: parseInt(quantity) | ||||||||||||||||
} | ||||||||||||||||
// To meet expected schema, remove the custom `product` we added | ||||||||||||||||
delete body.product | ||||||||||||||||
|
||||||||||||||||
const parameters = { | ||||||||||||||||
customerId: customer.customerId, | ||||||||||||||||
itemId: item.id, | ||||||||||||||||
listId: wishListData?.id | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
const mutation = | ||||||||||||||||
parseInt(quantity) > 0 | ||||||||||||||||
? updateCustomerProductListItem.mutateAsync({body, parameters}) | ||||||||||||||||
: deleteCustomerProductListItem.mutateAsync({parameters}) | ||||||||||||||||
Comment on lines
+91
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if you do There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||||||||
|
||||||||||||||||
try { | ||||||||||||||||
await wishlist.updateListItem({ | ||||||||||||||||
...item, | ||||||||||||||||
quantity: parseInt(quantity) | ||||||||||||||||
}) | ||||||||||||||||
} catch { | ||||||||||||||||
await mutation | ||||||||||||||||
isValidChange = true | ||||||||||||||||
setSelectedItem(undefined) | ||||||||||||||||
} catch (err) { | ||||||||||||||||
toast({ | ||||||||||||||||
title: formatMessage(API_ERROR_MESSAGE), | ||||||||||||||||
status: 'error' | ||||||||||||||||
}) | ||||||||||||||||
} | ||||||||||||||||
setWishlistItemLoading(false) | ||||||||||||||||
setSelectedItem(undefined) | ||||||||||||||||
setLocalQuantity({...localQuantity, [item.productId]: undefined}) | ||||||||||||||||
|
||||||||||||||||
// If true, the quantity picker would immediately update its number | ||||||||||||||||
// without waiting for the invalidated lists data to finish refetching | ||||||||||||||||
return isValidChange | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
useEffect(() => { | ||||||||||||||||
if (customer.isRegistered) { | ||||||||||||||||
// We want to reset the wishlist here | ||||||||||||||||
// because it is possible that a user | ||||||||||||||||
// adds an item to the wishlist on another page | ||||||||||||||||
// and the wishlist page may not have enough | ||||||||||||||||
// data to render the page. | ||||||||||||||||
// Reset the wishlist will make sure the | ||||||||||||||||
// initialization state is correct. | ||||||||||||||||
if (wishlist.isInitialized) { | ||||||||||||||||
wishlist.reset() | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
wishlist.init({detail: true}) | ||||||||||||||||
} | ||||||||||||||||
}, [customer.isRegistered]) | ||||||||||||||||
const isPageLoading = wishListItems ? isProductsLoading : isWishListLoading | ||||||||||||||||
|
||||||||||||||||
return ( | ||||||||||||||||
<Stack spacing={4} data-testid="account-wishlist-page"> | ||||||||||||||||
<Heading as="h1" fontSize="2xl"> | ||||||||||||||||
<FormattedMessage defaultMessage="Wishlist" id="account_wishlist.title.wishlist" /> | ||||||||||||||||
</Heading> | ||||||||||||||||
{!wishlist.hasDetail && ( | ||||||||||||||||
|
||||||||||||||||
{isPageLoading && ( | ||||||||||||||||
<Box data-testid="sf-wishlist-skeleton"> | ||||||||||||||||
{new Array(numberOfSkeletonItems).fill(0).map((i, idx) => ( | ||||||||||||||||
<Box | ||||||||||||||||
|
@@ -108,7 +140,7 @@ const AccountWishlist = () => { | |||||||||||||||
</Box> | ||||||||||||||||
)} | ||||||||||||||||
|
||||||||||||||||
{wishlist.hasDetail && wishlist.isEmpty && ( | ||||||||||||||||
{!isPageLoading && !wishListItems && ( | ||||||||||||||||
<PageActionPlaceHolder | ||||||||||||||||
data-testid="empty-wishlist" | ||||||||||||||||
icon={<HeartIcon boxSize={8} />} | ||||||||||||||||
|
@@ -129,26 +161,29 @@ const AccountWishlist = () => { | |||||||||||||||
/> | ||||||||||||||||
)} | ||||||||||||||||
|
||||||||||||||||
{wishlist.hasDetail && | ||||||||||||||||
!wishlist.isEmpty && | ||||||||||||||||
wishlist.items.map((item) => ( | ||||||||||||||||
{!isPageLoading && | ||||||||||||||||
wishListItems && | ||||||||||||||||
wishListItems.map((item) => ( | ||||||||||||||||
<ProductItem | ||||||||||||||||
key={item.id} | ||||||||||||||||
product={{ | ||||||||||||||||
...item.product, | ||||||||||||||||
quantity: localQuantity[item.productId] | ||||||||||||||||
? localQuantity[item.productId] | ||||||||||||||||
: item.quantity | ||||||||||||||||
quantity: item.quantity | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code in
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, the smart deletion logic originally comes from: pwa-kit/packages/template-retail-react-app/app/commerce-api/hooks/useCustomerProductLists.js Lines 204 to 210 in 487c775
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI I've implemented that. |
||||||||||||||||
}} | ||||||||||||||||
showLoading={isWishlistItemLoading && selectedItem === item.productId} | ||||||||||||||||
showLoading={ | ||||||||||||||||
(updateCustomerProductListItem.isLoading || | ||||||||||||||||
deleteCustomerProductListItem.isLoading || | ||||||||||||||||
isWishlistItemLoading) && | ||||||||||||||||
selectedItem === item.productId | ||||||||||||||||
} | ||||||||||||||||
primaryAction={<WishlistPrimaryAction />} | ||||||||||||||||
onItemQuantityChange={(quantity) => | ||||||||||||||||
handleItemQuantityChanged(quantity, item) | ||||||||||||||||
} | ||||||||||||||||
secondaryActions={ | ||||||||||||||||
<WishlistSecondaryButtonGroup | ||||||||||||||||
productListItemId={item.id} | ||||||||||||||||
onClick={handleActionClicked} | ||||||||||||||||
onClick={handleSecondaryAction} | ||||||||||||||||
/> | ||||||||||||||||
} | ||||||||||||||||
/> | ||||||||||||||||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.