Skip to content

Commit b894ac5

Browse files
authored
Merge pull request #2668 from SalesforceCommerceCloud/@W-18820722-refactor-pdp
@W-18820722 Refactor PDP for better readability and maintainability
2 parents d6be4ad + 400e93e commit b894ac5

File tree

12 files changed

+909
-606
lines changed

12 files changed

+909
-606
lines changed

packages/extension-chakra-storefront/src/pages/product-detail/index.jsx

Lines changed: 65 additions & 601 deletions
Large diffs are not rendered by default.

packages/extension-chakra-storefront/src/pages/product-detail/index.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/*
2-
* Copyright (c) 2023, Salesforce, Inc.
2+
* Copyright (c) 2025, Salesforce, Inc.
33
* All rights reserved.
44
* SPDX-License-Identifier: BSD-3-Clause
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7+
78
import React from 'react'
89
import {fireEvent, screen, waitFor, within} from '@testing-library/react'
910
import {mockCustomerBaskets, mockedCustomerProductLists} from '../../mocks/mock-data'
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import {useEffect} from 'react'
9+
import PropTypes from 'prop-types'
10+
import useEinstein from '../../hooks/use-einstein'
11+
import useDataCloud from '../../hooks/use-datacloud'
12+
import useActiveData from '../../hooks/use-active-data'
13+
import logger from '../../utils/logger-instance'
14+
15+
const PageAnalytics = ({product, category}) => {
16+
const einstein = useEinstein()
17+
const dataCloud = useDataCloud()
18+
const activeData = useActiveData()
19+
20+
useEffect(() => {
21+
if (!product || !category) {
22+
return
23+
}
24+
if (product && product.type.set) {
25+
einstein.sendViewProduct(product)
26+
dataCloud.sendViewProduct(product)
27+
const childrenProducts = product.setProducts
28+
childrenProducts.map((child) => {
29+
try {
30+
einstein.sendViewProduct(child)
31+
} catch (err) {
32+
logger.error('Einstein sendViewProduct error', {
33+
namespace: 'useProductAnalytics.useEffect',
34+
additionalProperties: {error: err, child}
35+
})
36+
}
37+
activeData.sendViewProduct(category, child, 'detail')
38+
dataCloud.sendViewProduct(child)
39+
})
40+
} else if (product) {
41+
try {
42+
einstein.sendViewProduct(product)
43+
} catch (err) {
44+
logger.error('Einstein sendViewProduct error', {
45+
namespace: 'useProductAnalytics.useEffect',
46+
additionalProperties: {error: err, product}
47+
})
48+
}
49+
activeData.sendViewProduct(category, product, 'detail')
50+
dataCloud.sendViewProduct(product)
51+
}
52+
}, [product?.id, category?.id])
53+
54+
return null
55+
}
56+
57+
PageAnalytics.propTypes = {
58+
product: PropTypes.object,
59+
category: PropTypes.object
60+
}
61+
62+
export default PageAnalytics
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import {useServerContext} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
8+
import {useExtensionConfig} from '../../hooks'
9+
/*
10+
* This component is used to set the cache headers for the page.
11+
*/
12+
export default function PageCache() {
13+
const {res} = useServerContext()
14+
const {maxCacheAge: MAX_CACHE_AGE, staleWhileRevalidate: STALE_WHILE_REVALIDATE} =
15+
useExtensionConfig()
16+
if (res) {
17+
res.set(
18+
'Cache-Control',
19+
`s-maxage=${MAX_CACHE_AGE}, stale-while-revalidate=${STALE_WHILE_REVALIDATE}`
20+
)
21+
}
22+
}

packages/extension-chakra-storefront/src/pages/product-detail/metadata.jsx renamed to packages/extension-chakra-storefront/src/pages/product-detail/page-metadata.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Seo from '../../components/seo'
1919
* @param {string} product.pageMetaTags.id - The id of the meta tag
2020
* @param {string} product.pageMetaTags.value - The value of the meta tag
2121
*/
22-
export default function Metadata({product}) {
22+
export default function PageMetadata({product}) {
2323
if (!product) {
2424
return null
2525
}
@@ -33,7 +33,7 @@ export default function Metadata({product}) {
3333
return <Seo title={title} description={description} keywords={keywords} metaTags={metaTags} />
3434
}
3535

36-
Metadata.propTypes = {
36+
PageMetadata.propTypes = {
3737
product: PropTypes.shape({
3838
pageTitle: PropTypes.string,
3939
pageDescription: PropTypes.string,
@@ -44,5 +44,5 @@ Metadata.propTypes = {
4444
value: PropTypes.string.isRequired
4545
})
4646
)
47-
}).isRequired
47+
})
4848
}

packages/extension-chakra-storefront/src/pages/product-detail/metadata.test.js renamed to packages/extension-chakra-storefront/src/pages/product-detail/page-metadata.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import React from 'react'
99
import {render} from '@testing-library/react'
10-
import Metadata from './metadata'
10+
import Metadata from './page-metadata'
1111

1212
jest.mock('../../components/seo', () => {
1313
return function MockSeo(props) {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import React, {Fragment} from 'react'
8+
import PropTypes from 'prop-types'
9+
import {Box} from '@chakra-ui/react'
10+
import ProductView from '../../../components/product-view'
11+
import InformationAccordion from './information-accordion'
12+
13+
/**
14+
* This component is used to render the product details for a composite product.
15+
*
16+
* A composite product is a product that is made up of multiple products.
17+
*
18+
* It can be a set or a bundle.
19+
*/
20+
const CompositeProductDetails = ({
21+
product,
22+
primaryCategory,
23+
isProductASet,
24+
isProductABundle,
25+
isProductLoading,
26+
isBasketLoading,
27+
isWishlistLoading,
28+
handleAddToWishlist,
29+
handleAddToCart,
30+
handleProductSetAddToCart,
31+
handleProductBundleAddToCart,
32+
handleChildProductValidation,
33+
childProductOrderability,
34+
setSelectedBundleQuantity,
35+
comboProduct,
36+
childProductRefs,
37+
selectedBundleQuantity,
38+
setChildProductSelection,
39+
childProductSelection,
40+
setChildProductOrderability
41+
}) => {
42+
return (
43+
<Fragment>
44+
<ProductView
45+
product={product}
46+
category={primaryCategory?.parentCategoryTree || []}
47+
addToCart={isProductASet ? handleProductSetAddToCart : handleProductBundleAddToCart}
48+
addToWishlist={handleAddToWishlist}
49+
isProductLoading={isProductLoading}
50+
isBasketLoading={isBasketLoading}
51+
isWishlistLoading={isWishlistLoading}
52+
validateOrderability={handleChildProductValidation}
53+
childProductOrderability={childProductOrderability}
54+
setSelectedBundleQuantity={setSelectedBundleQuantity}
55+
/>
56+
57+
<hr />
58+
59+
{/* TODO: consider `childProduct.belongsToSet` */}
60+
{
61+
// Render the child products
62+
comboProduct.childProducts.map(
63+
({product: childProduct, quantity: childQuantity}) => (
64+
<Box key={childProduct.id} data-testid="child-product">
65+
<ProductView
66+
// Do not use an arrow function as we are manipulating the functions scope.
67+
ref={function (ref) {
68+
// Assign the "set" scope of the ref, this is how we access the internal
69+
// validation.
70+
childProductRefs.current[childProduct.id] = {
71+
ref,
72+
validateOrderability: this.validateOrderability
73+
}
74+
}}
75+
product={childProduct}
76+
isProductPartOfSet={isProductASet}
77+
isProductPartOfBundle={isProductABundle}
78+
childOfBundleQuantity={childQuantity}
79+
selectedBundleParentQuantity={selectedBundleQuantity}
80+
addToCart={
81+
isProductASet
82+
? (variant, quantity) =>
83+
handleAddToCart([
84+
{
85+
product: childProduct,
86+
variant,
87+
quantity
88+
}
89+
])
90+
: null
91+
}
92+
addToWishlist={isProductASet ? handleAddToWishlist : null}
93+
onVariantSelected={(product, variant, quantity) => {
94+
if (quantity) {
95+
setChildProductSelection((previousState) => ({
96+
...previousState,
97+
[product.id]: {
98+
product,
99+
variant,
100+
quantity: isProductABundle
101+
? childQuantity
102+
: quantity
103+
}
104+
}))
105+
} else {
106+
const selections = {...childProductSelection}
107+
delete selections[product.id]
108+
setChildProductSelection(selections)
109+
}
110+
}}
111+
isProductLoading={isProductLoading}
112+
isBasketLoading={isBasketLoading}
113+
isWishlistLoading={isWishlistLoading}
114+
setChildProductOrderability={setChildProductOrderability}
115+
/>
116+
<InformationAccordion product={childProduct} />
117+
118+
<Box display={['none', 'none', 'none', 'block']}>
119+
<hr />
120+
</Box>
121+
</Box>
122+
)
123+
)
124+
}
125+
</Fragment>
126+
)
127+
}
128+
129+
CompositeProductDetails.propTypes = {
130+
product: PropTypes.object,
131+
primaryCategory: PropTypes.object,
132+
isProductASet: PropTypes.bool,
133+
isProductABundle: PropTypes.bool,
134+
isProductLoading: PropTypes.bool,
135+
isBasketLoading: PropTypes.bool,
136+
isWishlistLoading: PropTypes.bool,
137+
handleAddToWishlist: PropTypes.func,
138+
handleAddToCart: PropTypes.func,
139+
handleProductSetAddToCart: PropTypes.func,
140+
handleProductBundleAddToCart: PropTypes.func,
141+
handleChildProductValidation: PropTypes.func,
142+
childProductOrderability: PropTypes.object,
143+
setSelectedBundleQuantity: PropTypes.func,
144+
comboProduct: PropTypes.object,
145+
childProductRefs: PropTypes.object,
146+
selectedBundleQuantity: PropTypes.number,
147+
setChildProductSelection: PropTypes.func,
148+
childProductSelection: PropTypes.object,
149+
setChildProductOrderability: PropTypes.func
150+
}
151+
152+
export default CompositeProductDetails
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import React, {Fragment} from 'react'
8+
import PropTypes from 'prop-types'
9+
import ProductView from '../../../components/product-view'
10+
import InformationAccordion from './information-accordion'
11+
12+
const SimpleProductDetails = ({
13+
product,
14+
primaryCategory,
15+
isProductLoading,
16+
isBasketLoading,
17+
isWishlistLoading,
18+
handleAddToWishlist,
19+
handleAddToCart
20+
}) => {
21+
return (
22+
<Fragment>
23+
<ProductView
24+
product={product}
25+
category={primaryCategory?.parentCategoryTree || []}
26+
addToCart={(variant, quantity) => handleAddToCart([{product, variant, quantity}])}
27+
addToWishlist={handleAddToWishlist}
28+
isProductLoading={isProductLoading}
29+
isBasketLoading={isBasketLoading}
30+
isWishlistLoading={isWishlistLoading}
31+
/>
32+
<InformationAccordion product={product} />
33+
</Fragment>
34+
)
35+
}
36+
37+
SimpleProductDetails.propTypes = {
38+
product: PropTypes.object,
39+
primaryCategory: PropTypes.object,
40+
isProductLoading: PropTypes.bool,
41+
isBasketLoading: PropTypes.bool,
42+
isWishlistLoading: PropTypes.bool,
43+
handleAddToWishlist: PropTypes.func,
44+
handleAddToCart: PropTypes.func
45+
}
46+
47+
export default SimpleProductDetails
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) 2025, Salesforce, Inc.
3+
* All rights reserved.
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import React from 'react'
8+
import PropTypes from 'prop-types'
9+
import SimpleProductDetails from './product-details-simple'
10+
import CompositeProductDetails from './product-details-composite'
11+
12+
const ProductDetails = (props) => {
13+
const {isProductASet, isProductABundle} = props
14+
15+
if (isProductASet || isProductABundle) {
16+
return <CompositeProductDetails {...props} />
17+
}
18+
19+
return <SimpleProductDetails {...props} />
20+
}
21+
22+
ProductDetails.propTypes = {
23+
product: PropTypes.object,
24+
primaryCategory: PropTypes.object,
25+
isProductASet: PropTypes.bool,
26+
isProductABundle: PropTypes.bool,
27+
isProductLoading: PropTypes.bool,
28+
isBasketLoading: PropTypes.bool,
29+
isWishlistLoading: PropTypes.bool,
30+
handleAddToWishlist: PropTypes.func,
31+
handleAddToCart: PropTypes.func,
32+
handleProductSetAddToCart: PropTypes.func,
33+
handleProductBundleAddToCart: PropTypes.func,
34+
handleChildProductValidation: PropTypes.func,
35+
childProductOrderability: PropTypes.object,
36+
setSelectedBundleQuantity: PropTypes.func,
37+
comboProduct: PropTypes.object,
38+
childProductRefs: PropTypes.object,
39+
selectedBundleQuantity: PropTypes.number,
40+
setChildProductSelection: PropTypes.func,
41+
childProductSelection: PropTypes.object,
42+
setChildProductOrderability: PropTypes.func
43+
}
44+
45+
export default ProductDetails

0 commit comments

Comments
 (0)