Skip to content

Commit

Permalink
Merge pull request #2 from SalesforceCommerceCloud/feature-add-to-wis…
Browse files Browse the repository at this point in the history
…hlist-plp-new

Allow Adding to Wishlist from the Product List Page
  • Loading branch information
alexvuong authored Aug 24, 2021
2 parents 8e4462f + f9fb084 commit 78cd86f
Show file tree
Hide file tree
Showing 27 changed files with 585 additions and 119 deletions.
49 changes: 26 additions & 23 deletions packages/pwa/app/commerce-api/hooks/useCustomerProductLists.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import {useContext, useMemo, useEffect, useState} from 'react'
import {isError, useCommerceAPI, CustomerProductListsContext, noop} from '../utils'
import {isError, useCommerceAPI, CustomerProductListsContext} from '../utils'
import {noop} from '../../utils/utils'

import useCustomer from './useCustomer'
import Queue from '../../utils/queue'

// A event queue for the following use cases:
// 1. Allow user to add item to wishlist before wishlist is initialized
// 2. Allow user to add item to wishlist before logging in
// e.g. user clicks add to wishlist, push event to the queue, show login
// modal, pop the event after successfuly logged in
// modal, pop the event after successfully logged in
export class CustomerProductListEventQueue extends Queue {
static eventTypes = {
ADD: 'add',
Expand All @@ -35,12 +37,28 @@ export default function useCustomerProductLists({eventHandler = noop, errorHandl
switch (action) {
case CustomerProductListEventQueue.eventTypes.ADD: {
try {
const productItem = {
productId: item.id,
quantity: item.quantity
const productList = self.getProductListPerType(listType)
const productListItem = productList.customerProductListItems.find(
({productId}) => productId === event.item.id
)
// if the item is already in the wishlist
// only update the quantity
if (productListItem) {
await self.updateCustomerProductListItem(productList, {
...productListItem,
quantity: event.item.quantity + productListItem.quantity
})
eventHandler(event)
} else {
await self.createCustomerProductListItem(productList, {
productId: event.item.id,
priority: 1,
quantity: parseInt(event.item.quantity),
public: false,
type: 'product'
})
eventHandler(event)
}
await addItemToCustomerProductList(productItem, list?.id, listType)
eventHandler(event)
} catch (error) {
errorHandler(error)
}
Expand All @@ -59,21 +77,6 @@ export default function useCustomerProductLists({eventHandler = noop, errorHandl
})
}, [customerProductLists])

const addItemToCustomerProductList = async (item, listId, listType) => {
// Either find the list by the id or by the type
const productList = listId
? customerProductLists.data.find((list) => list.id === listId)
: customerProductLists.data.find((list) => list.type === listType)

return await self.createCustomerProductListItem(productList, {
productId: item.productId,
priority: 1,
quantity: item.quantity,
public: false,
type: 'product'
})
}

const self = useMemo(() => {
return {
...customerProductLists,
Expand All @@ -96,6 +99,7 @@ export default function useCustomerProductLists({eventHandler = noop, errorHandl

/**
* Fetches product lists for registered users or creates a new list if none exist
* due to the api limitation, we can not get the list based on type but all lists
* @param {string} type type of list to fetch or create
* @returns product lists for registered users
*/
Expand Down Expand Up @@ -199,7 +203,6 @@ export default function useCustomerProductLists({eventHandler = noop, errorHandl
/**
* Adds an item to the customer's product list.
* @param {object} list
* @param {string} list.id id of the list to add the item to.
* @param {Object} item item to be added to the list.
*/
async createCustomerProductListItem(list, item) {
Expand Down
19 changes: 9 additions & 10 deletions packages/pwa/app/commerce-api/hooks/useShopper.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,16 @@ const useShopper = () => {
useEffect(() => {
// Fetch product details for new items in product-lists
const hasCustomerProductLists = customerProductLists?.loaded
if (hasCustomerProductLists) {
customerProductLists.data.forEach((list) => {
let ids = list.customerProductListItems?.map((item) => item.productId)
if (list?._productItemsDetail) {
ids = ids.filter((id) => !list?._productItemsDetail[id])
}
if (!hasCustomerProductLists) return
customerProductLists.data.forEach((list) => {
let ids = list.customerProductListItems?.map((item) => item.productId)
if (list?._productItemsDetail) {
ids = ids.filter((id) => !list?._productItemsDetail[id])
}

customerProductLists.getProductsInList(ids?.toString(), list.id)
})
}
}, [customerProductLists])
customerProductLists.getProductsInList(ids?.toString(), list.id)
})
}, [customerProductLists.data])

return {customer, basket}
}
Expand Down
11 changes: 8 additions & 3 deletions packages/pwa/app/components/cart-item-variant/item-name.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import React from 'react'
import {Text} from '@chakra-ui/react'
import {useCartItemVariant} from '.'
import Link from '../link'

/**
* In the context of a cart product item variant, this components simply renders
Expand All @@ -18,9 +18,14 @@ const ItemName = (props) => {
const variant = useCartItemVariant()

return (
<Text fontWeight="bold" {...props}>
<Link
fontWeight="bold"
{...props}
color="black.600"
to={`/product/${variant?.master?.masterId}`}
>
{variant.productName}
</Text>
</Link>
)
}

Expand Down
15 changes: 10 additions & 5 deletions packages/pwa/app/components/confirmation-modal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,23 @@ const ConfirmationModal = ({
}) => {
const {formatMessage} = useIntl()

const handleConfirmClicked = () => {
const handleConfirmClick = () => {
onPrimaryAction()
props.onClose()
}

const handleCancelClicked = () => {
const handleAlternateActionClick = () => {
onAlternateAction()
props.onClose()
}

return (
<AlertDialog isOpen={props.isOpen} isCentered onClose={handleCancelClicked} {...props}>
<AlertDialog
isOpen={props.isOpen}
isCentered
onClose={handleAlternateActionClick}
{...props}
>
<AlertDialogOverlay />
<AlertDialogContent>
<AlertDialogHeader>{formatMessage(dialogTitle)}</AlertDialogHeader>
Expand All @@ -52,10 +57,10 @@ const ConfirmationModal = ({
</AlertDialogBody>

<AlertDialogFooter>
<Button variant="ghost" mr={3} onClick={handleCancelClicked}>
<Button variant="ghost" mr={3} onClick={handleAlternateActionClick}>
{formatMessage(alternateActionLabel)}
</Button>
<Button variant="solid" onClick={handleConfirmClicked}>
<Button variant="solid" onClick={handleConfirmClick}>
{formatMessage(primaryActionLabel)}
</Button>
</AlertDialogFooter>
Expand Down
2 changes: 1 addition & 1 deletion packages/pwa/app/components/header/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ test('shows dropdown menu when an authenticated users hover on the account icon'
await waitFor(() => {
// Look for account icon
const accountTrigger = document.querySelector('svg[aria-label="My account trigger"]')
expect(accountTrigger).toBeInTheDocument
expect(accountTrigger).toBeInTheDocument()
expect(screen.getByText('My Account')).toBeInTheDocument()
})
})
19 changes: 19 additions & 0 deletions packages/pwa/app/components/image-gallery/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ describe('Image Gallery Component', () => {
)
})
})

test('can select thumbnail image by clicking on the image', async () => {
const history = createMemoryHistory()
history.push('/en/image-gallery')

renderWithProviders(
<MockComponent imageGroups={data} selectedVariationAttributes={{}} history={history} />
)
const thumbnailImages = screen.getAllByTestId('image-gallery-thumbnails')
const lastThumbnailImage = thumbnailImages[thumbnailImages.length - 1]

fireEvent.click(lastThumbnailImage)
await waitFor(() => {
expect(screen.getByAltText(/Ruffle Front V-Neck Cardigan, , large/)).toHaveAttribute(
'src',
'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwf67d39ef/images/large/PG.10216885.JJ169XX.BZ.jpg'
)
})
})
})

const data = [
Expand Down
9 changes: 9 additions & 0 deletions packages/pwa/app/components/product-item/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ import CartItemVariantPrice from '../cart-item-variant/item-price'
import LoadingSpinner from '../loading-spinner'
import {noop} from '../../utils/utils'

/**
* Component representing a product item usually in a list with details about the product - name, variant, pricing, etc.
* @param {Object} product Product to be represented in the list item.
* @param {node} primaryAction Child component representing the most prominent action to be performed by the user.
* @param {node} secondaryActions Child component representing the other actions relevant to the product to be performed by the user.
* @param {func} onItemQuantityChange callback function to be invoked whenever item quantity changes.
* @param {boolean} showLoading Renders a loading spinner with overlay if set to true.
* @returns A JSX element representing product item in a list (eg: wishlist, cart, etc).
*/
const ProductItem = ({
product,
primaryAction,
Expand Down
57 changes: 54 additions & 3 deletions packages/pwa/app/components/product-tile/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import React from 'react'
import PropTypes from 'prop-types'
import {WishlistIcon, WishlistSolidIcon} from '../icons'

// Components
import {
Expand All @@ -16,15 +17,20 @@ import {
Skeleton as ChakraSkeleton,
Text,
Stack,
useMultiStyleConfig
useMultiStyleConfig,
IconButton
} from '@chakra-ui/react'

// Hooks
import {useIntl} from 'react-intl'

// Other
import {productUrlBuilder} from '../../utils/url'
import {noop} from '../../utils/utils'
import Link from '../link'
import withRegistration from '../../hoc/with-registration'

const IconButtonWithRegistration = withRegistration(IconButton)

// Component Skeleton
export const Skeleton = () => {
Expand All @@ -51,15 +57,19 @@ export const Skeleton = () => {
const ProductTile = (props) => {
const intl = useIntl()

const styles = useMultiStyleConfig('ProductTile')
// eslint-disable-next-line react/prop-types
const {
productSearchItem,
// eslint-disable-next-line react/prop-types
staticContext,
onAddToWishlistClick = noop,
onRemoveWishlistClick = noop,
isInWishlist,
isWishlistLoading,
...rest
} = props
const {currency, image, price, productName} = productSearchItem
const styles = useMultiStyleConfig('ProductTile', {isLoading: isWishlistLoading})

return (
<Link
Expand All @@ -72,6 +82,34 @@ const ProductTile = (props) => {
<AspectRatio {...styles.image} ratio={1}>
<Img alt={image.alt} src={image.disBaseLink} />
</AspectRatio>
{isInWishlist ? (
<IconButton
aria-label={intl.formatMessage({
defaultMessage: 'wishlist-solid'
})}
icon={<WishlistSolidIcon />}
variant="unstyled"
{...styles.iconButton}
onClick={(e) => {
e.preventDefault()
if (isWishlistLoading) return
onRemoveWishlistClick()
}}
/>
) : (
<IconButtonWithRegistration
aria-label={intl.formatMessage({
defaultMessage: 'wishlist'
})}
icon={<WishlistIcon />}
variant="unstyled"
{...styles.iconButton}
onClick={() => {
if (isWishlistLoading) return
onAddToWishlistClick()
}}
/>
)}
</Box>

{/* Title */}
Expand All @@ -94,7 +132,20 @@ ProductTile.propTypes = {
* The product search hit that will be represented in this
* component.
*/
productSearchItem: PropTypes.object.isRequired
productSearchItem: PropTypes.object.isRequired,
/**
* Types of lists the product/variant is added to. (eg: wishlist)
*/
isInWishlist: PropTypes.bool,
/**
* Callback function to be invoked when the user add item to wishlist
*/
onAddToWishlistClick: PropTypes.func,
/**
* Callback function to be invoked when the user removes item to wishlist
*/
onRemoveWishlistClick: PropTypes.func,
isWishlistLoading: PropTypes.bool
}

export default ProductTile
3 changes: 2 additions & 1 deletion packages/pwa/app/hoc/with-registration/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const withRegistration = (Component) => {
const {formatMessage} = useIntl()
const showToast = useToast()

const handleClick = () => {
const handleClick = (e) => {
e.preventDefault()
if (customer?.authType !== 'registered') {
// Do not show auth modal if users is already on the login page
if (isLoginPage) {
Expand Down
Loading

0 comments on commit 78cd86f

Please sign in to comment.