Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/*
* Copyright (c) 2023, Salesforce, Inc.
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* 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 from 'react'
import {fireEvent, screen, waitFor, within} from '@testing-library/react'
import {mockCustomerBaskets, mockedCustomerProductLists} from '../../mocks/mock-data'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* 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 {useEffect} from 'react'
import PropTypes from 'prop-types'
import useEinstein from '../../hooks/use-einstein'
import useDataCloud from '../../hooks/use-datacloud'
import useActiveData from '../../hooks/use-active-data'
import logger from '../../utils/logger-instance'

const PageAnalytics = ({product, category}) => {
const einstein = useEinstein()
const dataCloud = useDataCloud()
const activeData = useActiveData()

useEffect(() => {
if (product && product.type.set) {
einstein.sendViewProduct(product)
dataCloud.sendViewProduct(product)
const childrenProducts = product.setProducts
childrenProducts.map((child) => {
try {
einstein.sendViewProduct(child)
} catch (err) {
logger.error('Einstein sendViewProduct error', {
namespace: 'useProductAnalytics.useEffect',
additionalProperties: {error: err, child}
})
}
activeData.sendViewProduct(category, child, 'detail')
dataCloud.sendViewProduct(child)
})
} else if (product) {
try {
einstein.sendViewProduct(product)
} catch (err) {
logger.error('Einstein sendViewProduct error', {
namespace: 'useProductAnalytics.useEffect',
additionalProperties: {error: err, product}
})
}
activeData.sendViewProduct(category, product, 'detail')
dataCloud.sendViewProduct(product)
}
}, [product, category])

return null
}

PageAnalytics.propTypes = {
product: PropTypes.object,
category: PropTypes.object
}

export default PageAnalytics
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* 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 {useServerContext} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
import {useExtensionConfig} from '../../hooks'
/*
* This component is used to set the cache headers for the page.
*/
export default function PageCache() {
const {res} = useServerContext()
const {maxCacheAge: MAX_CACHE_AGE, staleWhileRevalidate: STALE_WHILE_REVALIDATE} =
useExtensionConfig()
if (res) {
res.set(
'Cache-Control',
`s-maxage=${MAX_CACHE_AGE}, stale-while-revalidate=${STALE_WHILE_REVALIDATE}`
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Seo from '../../components/seo'
* @param {string} product.pageMetaTags.id - The id of the meta tag
* @param {string} product.pageMetaTags.value - The value of the meta tag
*/
export default function Metadata({product}) {
export default function PageMetadata({product}) {
if (!product) {
return null
}
Expand All @@ -33,7 +33,7 @@ export default function Metadata({product}) {
return <Seo title={title} description={description} keywords={keywords} metaTags={metaTags} />
}

Metadata.propTypes = {
PageMetadata.propTypes = {
product: PropTypes.shape({
pageTitle: PropTypes.string,
pageDescription: PropTypes.string,
Expand All @@ -44,5 +44,5 @@ Metadata.propTypes = {
value: PropTypes.string.isRequired
})
)
}).isRequired
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React from 'react'
import {render} from '@testing-library/react'
import Metadata from './metadata'
import Metadata from './page-metadata'

jest.mock('../../components/seo', () => {
return function MockSeo(props) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* 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, {Fragment} from 'react'
import PropTypes from 'prop-types'
import {Box} from '@chakra-ui/react'
import ProductView from '../../../components/product-view'
import InformationAccordion from './information-accordion'

/**
* This component is used to render the product details for a composite product.
*
* A composite product is a product that is made up of multiple products.
*
* It can be a set or a bundle.
*/
const CompositeProductDetails = ({
product,
primaryCategory,
isProductASet,
isProductABundle,
isProductLoading,
isBasketLoading,
isWishlistLoading,
handleAddToWishlist,
handleAddToCart,
handleProductSetAddToCart,
handleProductBundleAddToCart,
handleChildProductValidation,
childProductOrderability,
setSelectedBundleQuantity,
comboProduct,
childProductRefs,
selectedBundleQuantity,
setChildProductSelection,
childProductSelection,
setChildProductOrderability
}) => {
return (
<Fragment>
<ProductView
product={product}
category={primaryCategory?.parentCategoryTree || []}
addToCart={isProductASet ? handleProductSetAddToCart : handleProductBundleAddToCart}
addToWishlist={handleAddToWishlist}
isProductLoading={isProductLoading}
isBasketLoading={isBasketLoading}
isWishlistLoading={isWishlistLoading}
validateOrderability={handleChildProductValidation}
childProductOrderability={childProductOrderability}
setSelectedBundleQuantity={setSelectedBundleQuantity}
/>

<hr />

{/* TODO: consider `childProduct.belongsToSet` */}
{
// Render the child products
comboProduct.childProducts.map(
({product: childProduct, quantity: childQuantity}) => (
<Box key={childProduct.id} data-testid="child-product">
<ProductView
// Do not use an arrow function as we are manipulating the functions scope.
ref={function (productViewRef) {
// The ref callback will be called with `null` when the component unmounts.
// We need to guard against that to prevent a runtime error.
if (productViewRef) {
// Assign the "set" scope of the ref, this is how we access the internal
// validation.
childProductRefs.current[childProduct.id] = {
ref: productViewRef,
validateOrderability:
productViewRef.validateOrderability
}
}
}}
product={childProduct}
isProductPartOfSet={isProductASet}
isProductPartOfBundle={isProductABundle}
childOfBundleQuantity={childQuantity}
selectedBundleParentQuantity={selectedBundleQuantity}
addToCart={
isProductASet
? (variant, quantity) =>
handleAddToCart([
{
product: childProduct,
variant,
quantity
}
])
: null
}
addToWishlist={isProductASet ? handleAddToWishlist : null}
onVariantSelected={(product, variant, quantity) => {
if (quantity) {
setChildProductSelection((previousState) => ({
...previousState,
[product.id]: {
product,
variant,
quantity: isProductABundle
? childQuantity
: quantity
}
}))
} else {
const selections = {...childProductSelection}
delete selections[product.id]
setChildProductSelection(selections)
}
}}
isProductLoading={isProductLoading}
isBasketLoading={isBasketLoading}
isWishlistLoading={isWishlistLoading}
setChildProductOrderability={setChildProductOrderability}
/>
<InformationAccordion product={childProduct} />

<Box display={['none', 'none', 'none', 'block']}>
<hr />
</Box>
</Box>
)
)
}
</Fragment>
)
}

CompositeProductDetails.propTypes = {
product: PropTypes.object,
primaryCategory: PropTypes.object,
isProductASet: PropTypes.bool,
isProductABundle: PropTypes.bool,
isProductLoading: PropTypes.bool,
isBasketLoading: PropTypes.bool,
isWishlistLoading: PropTypes.bool,
handleAddToWishlist: PropTypes.func,
handleAddToCart: PropTypes.func,
handleProductSetAddToCart: PropTypes.func,
handleProductBundleAddToCart: PropTypes.func,
handleChildProductValidation: PropTypes.func,
childProductOrderability: PropTypes.object,
setSelectedBundleQuantity: PropTypes.func,
comboProduct: PropTypes.object,
childProductRefs: PropTypes.object,
selectedBundleQuantity: PropTypes.number,
setChildProductSelection: PropTypes.func,
childProductSelection: PropTypes.object,
setChildProductOrderability: PropTypes.func
}

export default CompositeProductDetails
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* 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, {Fragment} from 'react'
import PropTypes from 'prop-types'
import ProductView from '../../../components/product-view'
import InformationAccordion from './information-accordion'

const SimpleProductDetails = ({
product,
primaryCategory,
isProductLoading,
isBasketLoading,
isWishlistLoading,
handleAddToWishlist,
handleAddToCart
}) => {
return (
<Fragment>
<ProductView
product={product}
category={primaryCategory?.parentCategoryTree || []}
addToCart={(variant, quantity) => handleAddToCart([{product, variant, quantity}])}
addToWishlist={handleAddToWishlist}
isProductLoading={isProductLoading}
isBasketLoading={isBasketLoading}
isWishlistLoading={isWishlistLoading}
/>
<InformationAccordion product={product} />
</Fragment>
)
}

SimpleProductDetails.propTypes = {
product: PropTypes.object,
primaryCategory: PropTypes.object,
isProductLoading: PropTypes.bool,
isBasketLoading: PropTypes.bool,
isWishlistLoading: PropTypes.bool,
handleAddToWishlist: PropTypes.func,
handleAddToCart: PropTypes.func
}

export default SimpleProductDetails
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025, Salesforce, Inc.
* All rights reserved.
* 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 from 'react'
import PropTypes from 'prop-types'
import SimpleProductDetails from './product-details-simple'
import CompositeProductDetails from './product-details-composite'

const ProductDetails = (props) => {
const {isProductASet, isProductABundle} = props

if (isProductASet || isProductABundle) {
return <CompositeProductDetails {...props} />
}

return <SimpleProductDetails {...props} />
}
Comment on lines +12 to +20
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: how about merging this back into the, um, parent product details component? This isn't much code, so I think it doesn't need to be in its own component.


ProductDetails.propTypes = {
product: PropTypes.object,
primaryCategory: PropTypes.object,
isProductASet: PropTypes.bool,
isProductABundle: PropTypes.bool,
isProductLoading: PropTypes.bool,
isBasketLoading: PropTypes.bool,
isWishlistLoading: PropTypes.bool,
handleAddToWishlist: PropTypes.func,
handleAddToCart: PropTypes.func,
handleProductSetAddToCart: PropTypes.func,
handleProductBundleAddToCart: PropTypes.func,
handleChildProductValidation: PropTypes.func,
childProductOrderability: PropTypes.object,
setSelectedBundleQuantity: PropTypes.func,
comboProduct: PropTypes.object,
childProductRefs: PropTypes.object,
selectedBundleQuantity: PropTypes.number,
setChildProductSelection: PropTypes.func,
childProductSelection: PropTypes.object,
setChildProductOrderability: PropTypes.func
}

export default ProductDetails
Loading
Loading