From f66b61a592805830c67d6cacac776b3129d95689 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 23 Apr 2024 17:51:17 -0700 Subject: [PATCH 01/45] strikethrough for PLP --- .../js/overrides/app/pages/home/index.jsx.hbs | 2 + .../app/components/_app/index.jsx | 1 - .../app/components/display-price/index.jsx | 96 +++-- .../components/display-price/index.test.js | 20 +- .../app/components/product-tile/index.jsx | 91 ++-- .../app/components/product-tile/index.test.js | 402 +++++++++++++++--- .../app/components/product-view/index.jsx | 4 +- .../app/hooks/use-current-basket.test.js | 7 +- .../app/pages/checkout/index.jsx | 7 +- .../app/pages/home/index.jsx | 2 + .../app/pages/product-list/index.jsx | 2 + .../static/translations/compiled/en-GB.json | 16 +- .../static/translations/compiled/en-US.json | 16 +- .../static/translations/compiled/en-XA.json | 24 +- .../translations/en-GB.json | 7 +- .../translations/en-US.json | 7 +- 16 files changed, 550 insertions(+), 154 deletions(-) diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs index a269d332e0..25e3bb496f 100644 --- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs +++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs @@ -57,6 +57,8 @@ const Home = () => { const {data: productSearchResult, isLoading} = useProductSearch({ parameters: { refine: [`cgid=${HOME_SHOP_PRODUCTS_CATEGORY_ID}`, 'htype=master'], + perPricebook: true, + allVariationProperties: true, limit: HOME_SHOP_PRODUCTS_LIMIT } }) diff --git a/packages/template-retail-react-app/app/components/_app/index.jsx b/packages/template-retail-react-app/app/components/_app/index.jsx index 07d5c14efb..708e707057 100644 --- a/packages/template-retail-react-app/app/components/_app/index.jsx +++ b/packages/template-retail-react-app/app/components/_app/index.jsx @@ -16,7 +16,6 @@ import {useQuery} from '@tanstack/react-query' import { useAccessToken, useCategory, - useCustomerBaskets, useShopperBasketsMutation } from '@salesforce/commerce-sdk-react' diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index d3eef28aad..576e629336 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -12,55 +12,95 @@ import {useIntl} from 'react-intl' import {useCurrency} from '@salesforce/retail-react-app/app/hooks' const DisplayPrice = ({ - basePrice, - discountPrice, + strikethroughPrice, + currentPrice, isProductASet = false, currency, - discountPriceProps, - basePriceProps, + currentPriceProps, + strikethroughPriceProps, skeletonProps }) => { const intl = useIntl() const {currency: activeCurrency} = useCurrency() + const currentPriceText = + currentPrice && + intl.formatNumber(currentPrice, { + style: 'currency', + currency: currency || activeCurrency + }) + const strikethroughPriceText = + strikethroughPrice && + intl.formatNumber(strikethroughPrice, { + style: 'currency', + currency: currency || activeCurrency + }) + const strikethroughPriceAriaLabel = + strikethroughPrice && + intl.formatMessage( + { + id: 'product_tile.assistive_msg.original_price', + defaultMessage: 'strikethrough price {strikethroughPrice}' + }, + { + strikethroughPrice: strikethroughPriceText + } + ) + const currentPriceAriaLabel = + currentPrice && + intl.formatMessage( + { + id: 'product_tile.assistive_msg.sale_price', + defaultMessage: 'current price {currentPrice}' + }, + { + currentPrice: currentPriceText + } + ) return ( - - - {isProductASet && - `${intl.formatMessage({ + + {isProductASet && ( + + {intl.formatMessage({ id: 'product_view.label.starting_at_price', defaultMessage: 'Starting at' - })} `} - - {typeof discountPrice === 'number' && ( - - {intl.formatNumber(discountPrice, { + })} + + )} + {/*Allowing display price of 0*/} + {typeof currentPrice === 'number' && ( + + {intl.formatNumber(currentPrice, { + style: 'currency', + currency: currency || activeCurrency + })} + + )} + {/*Allowing display price of 0*/} + {typeof strikethroughPrice === 'number' && ( + + {intl.formatNumber(strikethroughPrice, { style: 'currency', currency: currency || activeCurrency })} )} - - {intl.formatNumber(basePrice, { - style: 'currency', - currency: currency || activeCurrency - })} - ) } DisplayPrice.propTypes = { - basePrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - discountPrice: PropTypes.number, + strikethroughPrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + currentPrice: PropTypes.number, currency: PropTypes.string, isProductASet: PropTypes.bool, - discountPriceProps: PropTypes.object, - basePriceProps: PropTypes.object, + strikethroughPriceProps: PropTypes.object, + currentPriceProps: PropTypes.object, skeletonProps: PropTypes.object } diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index f492cbbf94..ba213e73d1 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -11,25 +11,27 @@ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-u describe('DisplayPrice', function () { test('should render without error', () => { - renderWithProviders() + renderWithProviders( + + ) expect(screen.getByText(/£90\.00/i)).toBeInTheDocument() expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() }) test('should render according html tag for prices', () => { const {container} = renderWithProviders( - + ) - const discountPriceTag = container.querySelectorAll('b') - const basePriceTag = container.querySelectorAll('s') - expect(within(discountPriceTag[0]).getByText(/£90\.00/i)).toBeDefined() - expect(within(basePriceTag[0]).getByText(/£100\.00/i)).toBeDefined() - expect(discountPriceTag).toHaveLength(1) - expect(basePriceTag).toHaveLength(1) + const currentPriceTag = container.querySelectorAll('b') + const strikethroughPriceTag = container.querySelectorAll('s') + expect(within(currentPriceTag[0]).getByText(/£90\.00/i)).toBeDefined() + expect(within(strikethroughPriceTag[0]).getByText(/£100\.00/i)).toBeDefined() + expect(currentPriceTag).toHaveLength(1) + expect(strikethroughPriceTag).toHaveLength(1) }) test('should not render discount price if not available', () => { - renderWithProviders() + renderWithProviders() expect(screen.queryByText(/£90\.00/i)).not.toBeInTheDocument() expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() }) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.jsx b/packages/template-retail-react-app/app/components/product-tile/index.jsx index 4580e178e2..0069665530 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.jsx +++ b/packages/template-retail-react-app/app/components/product-tile/index.jsx @@ -5,9 +5,11 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React, {useRef} from 'react' +import React, {useMemo, useRef} from 'react' import PropTypes from 'prop-types' import {HeartIcon, HeartSolidIcon} from '@salesforce/retail-react-app/app/components/icons' +import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price' +import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' // Components import { @@ -28,7 +30,6 @@ import {useIntl} from 'react-intl' import {productUrlBuilder} from '@salesforce/retail-react-app/app/utils/url' import Link from '@salesforce/retail-react-app/app/components/link' import withRegistration from '@salesforce/retail-react-app/app/components/with-registration' -import {useCurrency} from '@salesforce/retail-react-app/app/hooks' const IconButtonWithRegistration = withRegistration(IconButton) @@ -66,7 +67,7 @@ const ProductTile = (props) => { ...rest } = props - const {currency, image, price, productId, hitType} = product + const {image, productId, hitType} = product // ProductTile is used by two components, RecommendedProducts and ProductList. // RecommendedProducts provides a localized product name as `name` and non-localized product @@ -74,10 +75,48 @@ const ProductTile = (props) => { // use the `name` property. const localizedProductName = product.name ?? product.productName - const {currency: activeCurrency} = useCurrency() const isFavouriteLoading = useRef(false) const styles = useMultiStyleConfig('ProductTile') + // NOTE: swatches will implement later to set variant accordingly, + // On first load, get the variant that is the represent product + // this is for variant product, standard/set/bundles does not have variants + // Also, product tile can be used in RecommendedProducts where it calls getProducts which does not have representedProduct + // in that case we use the first variant that has price book to set up discount price + // Not all variants has set in a priceBook, meaning not having tieredPrices. + const variant = useMemo(() => { + return product?.variants?.find( + (i) => i?.productId === product?.representedProduct?.id || !!i?.tieredPrices + ) + }, [product]) + // prioritize variant promotionalPrice over standard price + let currentPrice = variant + ? getDisplayPrice(variant)?.discountPrice || variant?.price + : product?.price + + // check for both data returned from getProducts and productSearch + const isProductASet = hitType === 'set' || !!product?.type?.set + const isProductABundle = hitType === 'bundle' || !!product?.type?.bundle + const isProductAStandard = hitType === 'product' || !!product?.type?.item + let tieredPrices + if (variant) { + tieredPrices = variant?.tieredPrices + } else if (isProductABundle || isProductAStandard) { + tieredPrices = product?.tieredPrices + } else { + // if none applies, we assume this is a product set + // product sets do not have tierPieces, we go with priceRanges + tieredPrices = product?.priceRanges + } + + let listPrice = useMemo(() => { + const maxPriceTier = tieredPrices + ? Math.max(...(tieredPrices || []).map((item) => item.price || item.maxPrice)) + : 0 + return tieredPrices?.find( + (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier + ) + }, [tieredPrices]) return ( { {localizedProductName} {/* Price */} - - {hitType === 'set' - ? intl.formatMessage( - { - id: 'product_tile.label.starting_at_price', - defaultMessage: 'Starting at {price}' - }, - { - price: intl.formatNumber(price, { - style: 'currency', - currency: currency || activeCurrency - }) - } - ) - : intl.formatNumber(price, { - style: 'currency', - currency: currency || activeCurrency - })} - + currentPrice || listPrice?.price > currentPrice + ? listPrice?.price || listPrice?.maxPrice + : null + } + isProductASet={isProductASet} + currentPriceProps={ + listPrice?.maxPrice > currentPrice || listPrice?.price > currentPrice + ? {as: 'b'} + : {as: 'span'} + } + currentPrice={currentPrice} + /> {enableFavourite && ( { + const {getAllByRole} = renderWithProviders() + + const link = getAllByRole('link') + const img = getAllByRole('img') + + expect(link).toBeDefined() + expect(img).toBeDefined() +}) + +test('Renders Skeleton', () => { + const {getAllByTestId} = renderWithProviders() + + const skeleton = getAllByTestId('sf-product-tile-skeleton') + + expect(skeleton).toBeDefined() +}) + +test('Product set - renders the appropriate price label', async () => { + const {getByText} = renderWithProviders() + + expect(getByText(/starting at/i)).toBeInTheDocument() +}) + +test('Remove from wishlist cannot be muti-clicked', () => { + const onClick = jest.fn() + + const {getByTestId} = renderWithProviders( + + ) + const wishlistButton = getByTestId('wishlist-button') + + fireEvent.click(wishlistButton) + fireEvent.click(wishlistButton) + expect(onClick).toHaveBeenCalledTimes(1) +}) + +test('renders strike through price with variants product', () => { + const {getByText, container} = renderWithProviders( + + ) + expect(getByText(/black flat front wool suit/i)).toBeInTheDocument() + expect(getByText(/£191\.99/i)).toBeInTheDocument() + expect(getByText(/£320\.00/i)).toBeInTheDocument() + + const currentPriceTag = container.querySelectorAll('b') + const strikethroughPriceTag = container.querySelectorAll('s') + expect(within(currentPriceTag[0]).getByText(/£191\.99/i)).toBeDefined() + expect(within(strikethroughPriceTag[0]).getByText(/£320\.00/i)).toBeDefined() + expect(currentPriceTag).toHaveLength(1) + expect(strikethroughPriceTag).toHaveLength(1) +}) + +test('renders strike through price with set product', () => { + const {getByText, container} = renderWithProviders( + + ) + expect(getByText(/Winter Look/i)).toBeInTheDocument() + expect(getByText(/Starting at/i)).toBeInTheDocument() + expect(getByText(/£44\.16/i)).toBeInTheDocument() + expect(getByText(/£101\.76/i)).toBeInTheDocument() + + const currentPriceTag = container.querySelectorAll('b') + const strikethroughPriceTag = container.querySelectorAll('s') + expect(within(currentPriceTag[0]).getByText(/£44\.16/i)).toBeDefined() + expect(within(strikethroughPriceTag[0]).getByText(/£101\.76/i)).toBeDefined() + expect(currentPriceTag).toHaveLength(1) + expect(strikethroughPriceTag).toHaveLength(1) +}) + +test('renders strike through price with standard product', () => { + const {getByText, container} = renderWithProviders( + + ) + expect(getByText(/Laptop Briefcase with wheels \(37L\)/i)).toBeInTheDocument() + expect(getByText(/£63\.99/i)).toBeInTheDocument() + const currentPriceTag = container.querySelectorAll('b') + const strikethroughPriceTag = container.querySelectorAll('s') + expect(within(currentPriceTag[0]).getByText(/£63\.99/i)).toBeDefined() + expect(within(strikethroughPriceTag[0]).getByText(/£67\.99/i)).toBeDefined() + expect(currentPriceTag).toHaveLength(1) + expect(strikethroughPriceTag).toHaveLength(1) +}) const mockProductSearchItem = { currency: 'USD', @@ -19,8 +107,7 @@ const mockProductSearchItem = { price: 299.99, productName: 'Charcoal Single Pleat Wool Suit' } - -const mockProductSet = { +const mockProductSearchItemSet = { currency: 'GBP', hitType: 'set', image: { @@ -35,76 +122,259 @@ const mockProductSet = { priceMax: 71.03, pricePerUnit: 44.16, pricePerUnitMax: 71.03, + priceRanges: [ + { + maxPrice: 101.76, + minPrice: 44.16, + pricebook: 'gbp-m-list-prices' + }, + { + maxPrice: 71.03, + minPrice: 44.16, + pricebook: 'gbp-m-sale-prices' + } + ], productId: 'winter-lookM', productName: 'Winter Look', productType: { set: true }, representedProduct: { - id: '701642853695M' + id: '740357357531M', + c_color: 'BLACKLE', + c_refinementColor: 'black', + c_size: '065', + c_width: 'M' + } +} +const mockStandardProduct = { + currency: 'GBP', + hitType: 'product', + image: { + alt: 'Laptop Briefcase with wheels (37L), , large', + disBaseLink: + 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw7cb2d401/images/large/P0048_001.jpg', + link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dw7cb2d401/images/large/P0048_001.jpg', + title: 'Laptop Briefcase with wheels (37L), ' + }, + orderable: true, + price: 63.99, + pricePerUnit: 63.99, + productId: 'P0048M', + productName: 'Laptop Briefcase with wheels (37L)', + productType: { + item: true + }, + representedProduct: { + id: 'P0048M', + c_styleNumber: 'P0048', + c_tabDescription: + 'Perfect for business travel, this briefcase is ultra practical with plenty of space for your laptop and all its extras, as well as storage for documents, paperwork and all your essential items. The wheeled system allows you to travel comfortably with your work and when you reach your destination, you can remove the laptop compartment and carry over your shoulder to meetings. It’s the business.', + c_tabDetails: + '1682 ballistic nylon and genuine leather inserts| Spacious main storage compartment for documents and binders|Removable, padded laptop sleeve with D-rings for carrying with shoulder strap|Change handle system and cantilever wheels|Zip pull in gunmetal with black rubber insert Leather “comfort” insert detailed handle|Internal storage pockets for CD-Rom and peripherals|Real leather inserts' }, representedProducts: [ - {id: '701642853695M'}, - {id: '701642853718M'}, - {id: '701642853725M'}, - {id: '701642853701M'}, - {id: '740357357531M'}, - {id: '740357358095M'}, - {id: '740357357623M'}, - {id: '740357357609M'}, - {id: '740357358156M'}, - {id: '740357358132M'}, - {id: '740357358101M'}, - {id: '740357357562M'}, - {id: '740357357548M'}, - {id: '740357358187M'}, - {id: '740357357593M'}, - {id: '740357357555M'}, - {id: '740357357524M'}, - {id: '740357358149M'}, - {id: '740357358088M'}, - {id: '701642867098M'}, - {id: '701642867111M'}, - {id: '701642867104M'}, - {id: '701642867128M'}, - {id: '701642867135M'} + { + id: 'P0048M' + } + ], + tieredPrices: [ + { + price: 67.99, + pricebook: 'gbp-m-list-prices', + quantity: 1 + } + ] +} +const mockProductWithVariants = { + currency: 'GBP', + hitType: 'master', + image: { + alt: 'Black Flat Front Wool Suit, , large', + disBaseLink: + 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw3d8972fe/images/large/PG.52001DAN84Q.BLACKWL.PZ.jpg', + link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dw3d8972fe/images/large/PG.52001DAN84Q.BLACKWL.PZ.jpg', + title: 'Black Flat Front Wool Suit, ' + }, + orderable: true, + price: 191.99, + pricePerUnit: 191.99, + priceRanges: [ + { + maxPrice: 320, + minPrice: 320, + pricebook: 'gbp-m-list-prices' + }, + { + maxPrice: 191.99, + minPrice: 191.99, + pricebook: 'gbp-m-sale-prices' + } + ], + productId: '25686544M', + productName: 'Black Flat Front Wool Suit', + productType: { + master: true + }, + representedProduct: { + id: '750518703077M', + c_color: 'BLACKWL', + c_refinementColor: 'black', + c_size: '048', + c_width: 'V' + }, + representedProducts: [ + { + id: '750518703077M' + }, + { + id: '750518703060M' + }, + { + id: '750518703039M' + }, + { + id: '750518703046M' + } + ], + variants: [ + { + orderable: true, + price: 191.99, + productId: '750518703077M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKWL', + size: '048', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518703060M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKWL', + size: '046', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518703039M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKWL', + size: '042', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518703046M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKWL', + size: '043', + width: 'V' + } + } + ], + variationAttributes: [ + { + id: 'color', + name: 'Colour', + values: [ + { + name: 'Black', + orderable: true, + value: 'BLACKWL' + } + ] + }, + { + id: 'size', + name: 'Size', + values: [ + { + name: '42', + orderable: true, + value: '042' + }, + { + name: '43', + orderable: true, + value: '043' + }, + { + name: '46', + orderable: true, + value: '046' + }, + { + name: '48', + orderable: true, + value: '048' + } + ] + }, + { + id: 'width', + name: 'Width', + values: [ + { + name: 'Regular', + orderable: true, + value: 'V' + } + ] + } ] } - -test('Renders links and images', () => { - const {getAllByRole} = renderWithProviders() - - const link = getAllByRole('link') - const img = getAllByRole('img') - - expect(link).toBeDefined() - expect(img).toBeDefined() -}) - -test('Renders Skeleton', () => { - const {getAllByTestId} = renderWithProviders() - - const skeleton = getAllByTestId('sf-product-tile-skeleton') - - expect(skeleton).toBeDefined() -}) - -test('Product set - renders the appropriate price label', async () => { - const {getByTestId} = renderWithProviders() - - const container = getByTestId('product-tile-price') - expect(container).toHaveTextContent(/starting at/i) -}) - -test('Remove from wishlist cannot be muti-clicked', () => { - const onClick = jest.fn() - - const {getByTestId} = renderWithProviders( - - ) - const wishlistButton = getByTestId('wishlist-button') - - fireEvent.click(wishlistButton) - fireEvent.click(wishlistButton) - expect(onClick).toHaveBeenCalledTimes(1) -}) diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index 3a68dd2e97..31d2f7a81a 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -56,8 +56,8 @@ const ProductViewHeader = ({name, basePrice, discountPrice, currency, category, diff --git a/packages/template-retail-react-app/app/hooks/use-current-basket.test.js b/packages/template-retail-react-app/app/hooks/use-current-basket.test.js index 88795854e0..faa7bd23ca 100644 --- a/packages/template-retail-react-app/app/hooks/use-current-basket.test.js +++ b/packages/template-retail-react-app/app/hooks/use-current-basket.test.js @@ -6,14 +6,11 @@ */ import React from 'react' -import {screen, waitFor} from '@testing-library/react' +import {screen} from '@testing-library/react' import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket' import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' import {useCustomerBaskets} from '@salesforce/commerce-sdk-react' -import { - mockCustomerBaskets, - mockEmptyBasket -} from '@salesforce/retail-react-app/app/mocks/mock-data' +import {mockCustomerBaskets} from '@salesforce/retail-react-app/app/mocks/mock-data' const MOCK_USE_QUERY_RESULT = { data: undefined, diff --git a/packages/template-retail-react-app/app/pages/checkout/index.jsx b/packages/template-retail-react-app/app/pages/checkout/index.jsx index da384db3c7..db9d610c6d 100644 --- a/packages/template-retail-react-app/app/pages/checkout/index.jsx +++ b/packages/template-retail-react-app/app/pages/checkout/index.jsx @@ -29,11 +29,7 @@ import OrderSummary from '@salesforce/retail-react-app/app/components/order-summ import {useCurrentCustomer} from '@salesforce/retail-react-app/app/hooks/use-current-customer' import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-current-basket' import CheckoutSkeleton from '@salesforce/retail-react-app/app/pages/checkout/partials/checkout-skeleton' -import { - useUsid, - useShopperOrdersMutation, - useShopperBasketsMutation -} from '@salesforce/commerce-sdk-react' +import {useShopperOrdersMutation, useShopperBasketsMutation} from '@salesforce/commerce-sdk-react' import UnavailableProductConfirmationModal from '@salesforce/retail-react-app/app/components/unavailable-product-confirmation-modal' import { API_ERROR_MESSAGE, @@ -45,7 +41,6 @@ import LoadingSpinner from '@salesforce/retail-react-app/app/components/loading- const Checkout = () => { const {formatMessage} = useIntl() const navigate = useNavigation() - const {usid} = useUsid() const {step} = useCheckout() const [error, setError] = useState() const {data: basket} = useCurrentBasket() diff --git a/packages/template-retail-react-app/app/pages/home/index.jsx b/packages/template-retail-react-app/app/pages/home/index.jsx index 34f0ddbccf..19252f0d54 100644 --- a/packages/template-retail-react-app/app/pages/home/index.jsx +++ b/packages/template-retail-react-app/app/pages/home/index.jsx @@ -64,6 +64,8 @@ const Home = () => { const {data: productSearchResult, isLoading} = useProductSearch({ parameters: { refine: [`cgid=${HOME_SHOP_PRODUCTS_CATEGORY_ID}`, 'htype=master'], + perPricebook: true, + allVariationProperties: true, limit: HOME_SHOP_PRODUCTS_LIMIT } }) diff --git a/packages/template-retail-react-app/app/pages/product-list/index.jsx b/packages/template-retail-react-app/app/pages/product-list/index.jsx index 86fdadfb42..a88288bd7a 100644 --- a/packages/template-retail-react-app/app/pages/product-list/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-list/index.jsx @@ -149,6 +149,8 @@ const ProductList = (props) => { { parameters: { ...restOfParams, + perPricebook: true, + allVariationProperties: true, refine: _refine } }, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 20ebb0d5d5..f0c724d02b 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -2481,6 +2481,16 @@ "value": " to wishlist" } ], + "product_tile.assistive_msg.original_price": [ + { + "type": 0, + "value": "strikethrough price " + }, + { + "type": 1, + "value": "strikethroughPrice" + } + ], "product_tile.assistive_msg.remove_from_wishlist": [ { "type": 0, @@ -2495,14 +2505,14 @@ "value": " from wishlist" } ], - "product_tile.label.starting_at_price": [ + "product_tile.assistive_msg.sale_price": [ { "type": 0, - "value": "Starting at " + "value": "current price " }, { "type": 1, - "value": "price" + "value": "currentPrice" } ], "product_view.button.add_set_to_cart": [ diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 20ebb0d5d5..f0c724d02b 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -2481,6 +2481,16 @@ "value": " to wishlist" } ], + "product_tile.assistive_msg.original_price": [ + { + "type": 0, + "value": "strikethrough price " + }, + { + "type": 1, + "value": "strikethroughPrice" + } + ], "product_tile.assistive_msg.remove_from_wishlist": [ { "type": 0, @@ -2495,14 +2505,14 @@ "value": " from wishlist" } ], - "product_tile.label.starting_at_price": [ + "product_tile.assistive_msg.sale_price": [ { "type": 0, - "value": "Starting at " + "value": "current price " }, { "type": 1, - "value": "price" + "value": "currentPrice" } ], "product_view.button.add_set_to_cart": [ diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index 8e15e0cea6..1ee7d2f973 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -5281,6 +5281,24 @@ "value": "]" } ], + "product_tile.assistive_msg.original_price": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "şŧřīķḗḗŧħřǿǿŭŭɠħ ƥřīƈḗḗ " + }, + { + "type": 1, + "value": "strikethroughPrice" + }, + { + "type": 0, + "value": "]" + } + ], "product_tile.assistive_msg.remove_from_wishlist": [ { "type": 0, @@ -5303,18 +5321,18 @@ "value": "]" } ], - "product_tile.label.starting_at_price": [ + "product_tile.assistive_msg.sale_price": [ { "type": 0, "value": "[" }, { "type": 0, - "value": "Şŧȧȧřŧīƞɠ ȧȧŧ " + "value": "ƈŭŭřřḗḗƞŧ ƥřīƈḗḗ " }, { "type": 1, - "value": "price" + "value": "currentPrice" }, { "type": 0, diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index ab3fc69b9b..8b80bb74d8 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -1063,11 +1063,14 @@ "product_tile.assistive_msg.add_to_wishlist": { "defaultMessage": "Add {product} to wishlist" }, + "product_tile.assistive_msg.original_price": { + "defaultMessage": "strikethrough price {strikethroughPrice}" + }, "product_tile.assistive_msg.remove_from_wishlist": { "defaultMessage": "Remove {product} from wishlist" }, - "product_tile.label.starting_at_price": { - "defaultMessage": "Starting at {price}" + "product_tile.assistive_msg.sale_price": { + "defaultMessage": "current price {currentPrice}" }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index ab3fc69b9b..8b80bb74d8 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -1063,11 +1063,14 @@ "product_tile.assistive_msg.add_to_wishlist": { "defaultMessage": "Add {product} to wishlist" }, + "product_tile.assistive_msg.original_price": { + "defaultMessage": "strikethrough price {strikethroughPrice}" + }, "product_tile.assistive_msg.remove_from_wishlist": { "defaultMessage": "Remove {product} from wishlist" }, - "product_tile.label.starting_at_price": { - "defaultMessage": "Starting at {price}" + "product_tile.assistive_msg.sale_price": { + "defaultMessage": "current price {currentPrice}" }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" From 50b3bce3d2955b6a5acfc4ee50bd4794bcc08749 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 23 Apr 2024 17:58:17 -0700 Subject: [PATCH 02/45] add param for getProducts in useEinstein --- packages/template-retail-react-app/app/hooks/use-einstein.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template-retail-react-app/app/hooks/use-einstein.js b/packages/template-retail-react-app/app/hooks/use-einstein.js index fca064ea46..dd51956d64 100644 --- a/packages/template-retail-react-app/app/hooks/use-einstein.js +++ b/packages/template-retail-react-app/app/hooks/use-einstein.js @@ -405,7 +405,7 @@ const useEinstein = () => { const token = await getTokenWhenReady() // Fetch the product details for the recommendations const products = await api.shopperProducts.getProducts({ - parameters: {ids: ids.join(',')}, + parameters: {ids: ids.join(','), perPricebook: true, allVariationProperties: true}, headers: { Authorization: `Bearer ${token}` } From 249785e49b96135f6a5065a9f4e36e5eed212dc5 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 25 Apr 2024 11:26:25 -0700 Subject: [PATCH 03/45] PR feedback --- .../app/components/display-price/index.jsx | 97 ++++++++----------- .../app/components/product-tile/index.jsx | 12 ++- .../app/components/product-view/index.jsx | 1 - .../app/hooks/use-einstein.js | 6 +- .../app/pages/product-list/index.jsx | 1 + 5 files changed, 56 insertions(+), 61 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 576e629336..e74193c3ac 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -7,101 +7,84 @@ import React from 'react' import PropTypes from 'prop-types' -import {Skeleton, Text} from '@salesforce/retail-react-app/app/components/shared/ui' +import {Box, Text} from '@salesforce/retail-react-app/app/components/shared/ui' import {useIntl} from 'react-intl' import {useCurrency} from '@salesforce/retail-react-app/app/hooks' const DisplayPrice = ({ strikethroughPrice, currentPrice, - isProductASet = false, + prefixLabel, currency, currentPriceProps, - strikethroughPriceProps, - skeletonProps + strikethroughPriceProps }) => { const intl = useIntl() const {currency: activeCurrency} = useCurrency() - const currentPriceText = - currentPrice && - intl.formatNumber(currentPrice, { - style: 'currency', - currency: currency || activeCurrency - }) + const currentPriceText = intl.formatNumber(currentPrice, { + style: 'currency', + currency: currency || activeCurrency + }) const strikethroughPriceText = strikethroughPrice && intl.formatNumber(strikethroughPrice, { style: 'currency', currency: currency || activeCurrency }) - const strikethroughPriceAriaLabel = - strikethroughPrice && - intl.formatMessage( - { - id: 'product_tile.assistive_msg.original_price', - defaultMessage: 'strikethrough price {strikethroughPrice}' - }, - { - strikethroughPrice: strikethroughPriceText - } - ) - const currentPriceAriaLabel = - currentPrice && - intl.formatMessage( - { - id: 'product_tile.assistive_msg.sale_price', - defaultMessage: 'current price {currentPrice}' - }, - { - currentPrice: currentPriceText - } - ) return ( - - {isProductASet && ( + + {prefixLabel && ( - {intl.formatMessage({ - id: 'product_view.label.starting_at_price', - defaultMessage: 'Starting at' - })} - - )} - {/*Allowing display price of 0*/} - {typeof currentPrice === 'number' && ( - - {intl.formatNumber(currentPrice, { - style: 'currency', - currency: currency || activeCurrency - })} + {prefixLabel} )} + + {currentPriceText} + {/*Allowing display price of 0*/} {typeof strikethroughPrice === 'number' && ( - {intl.formatNumber(strikethroughPrice, { - style: 'currency', - currency: currency || activeCurrency - })} + {strikethroughPriceText} )} - + ) } DisplayPrice.propTypes = { strikethroughPrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - currentPrice: PropTypes.number, + currentPrice: PropTypes.number.isRequired, currency: PropTypes.string, - isProductASet: PropTypes.bool, + prefixLabel: PropTypes.string, strikethroughPriceProps: PropTypes.object, - currentPriceProps: PropTypes.object, - skeletonProps: PropTypes.object + currentPriceProps: PropTypes.object } export default DisplayPrice diff --git a/packages/template-retail-react-app/app/components/product-tile/index.jsx b/packages/template-retail-react-app/app/components/product-tile/index.jsx index 0069665530..ac86f7d6d5 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.jsx +++ b/packages/template-retail-react-app/app/components/product-tile/index.jsx @@ -68,7 +68,6 @@ const ProductTile = (props) => { } = props const {image, productId, hitType} = product - // ProductTile is used by two components, RecommendedProducts and ProductList. // RecommendedProducts provides a localized product name as `name` and non-localized product // name as `productName`. ProductList provides a localized name as `productName` and does not @@ -117,6 +116,7 @@ const ProductTile = (props) => { (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier ) }, [tieredPrices]) + return ( { ? listPrice?.price || listPrice?.maxPrice : null } - isProductASet={isProductASet} + prefixLabel={ + isProductASet + ? intl.formatMessage({ + id: 'product_view.label.starting_at_price', + defaultMessage: 'Starting at' + }) + : null + } currentPriceProps={ listPrice?.maxPrice > currentPrice || listPrice?.price > currentPrice ? {as: 'b'} @@ -238,6 +245,7 @@ ProductTile.propTypes = { variants: PropTypes.array, type: PropTypes.shape({ set: PropTypes.bool, + bundle: PropTypes.bool, item: PropTypes.bool }) diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index 31d2f7a81a..24e451ee08 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -59,7 +59,6 @@ const ProductViewHeader = ({name, basePrice, discountPrice, currency, category, strikethroughPrice={basePrice} currentPrice={discountPrice} currency={currency} - isProductASet={isProductASet} /> ) diff --git a/packages/template-retail-react-app/app/hooks/use-einstein.js b/packages/template-retail-react-app/app/hooks/use-einstein.js index dd51956d64..554c4505a8 100644 --- a/packages/template-retail-react-app/app/hooks/use-einstein.js +++ b/packages/template-retail-react-app/app/hooks/use-einstein.js @@ -405,7 +405,11 @@ const useEinstein = () => { const token = await getTokenWhenReady() // Fetch the product details for the recommendations const products = await api.shopperProducts.getProducts({ - parameters: {ids: ids.join(','), perPricebook: true, allVariationProperties: true}, + parameters: { + ids: ids.join(','), + perPricebook: true, + allVariationProperties: true + }, headers: { Authorization: `Bearer ${token}` } diff --git a/packages/template-retail-react-app/app/pages/product-list/index.jsx b/packages/template-retail-react-app/app/pages/product-list/index.jsx index a88288bd7a..5d0600fe56 100644 --- a/packages/template-retail-react-app/app/pages/product-list/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-list/index.jsx @@ -151,6 +151,7 @@ const ProductList = (props) => { ...restOfParams, perPricebook: true, allVariationProperties: true, + expand: ['promotions', 'variations', 'prices', 'images'], refine: _refine } }, From ceddadfc91fb74dc57b0283793602fe7f5c6c749 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 25 Apr 2024 16:01:00 -0700 Subject: [PATCH 04/45] Refactoring getDisplayPrice --- .../app/components/product-tile/index.jsx | 63 +++++-------------- .../app/components/product-view/index.jsx | 38 ++++++----- .../app/hooks/use-einstein.js | 10 ++- .../app/pages/home/index.jsx | 1 - .../app/pages/product-detail/index.jsx | 1 + .../app/utils/product-utils.js | 26 +++++--- .../app/utils/product-utils.test.js | 62 ++++++++++++++---- 7 files changed, 118 insertions(+), 83 deletions(-) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.jsx b/packages/template-retail-react-app/app/components/product-tile/index.jsx index ac86f7d6d5..9cd3356c44 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.jsx +++ b/packages/template-retail-react-app/app/components/product-tile/index.jsx @@ -9,7 +9,6 @@ import React, {useMemo, useRef} from 'react' import PropTypes from 'prop-types' import {HeartIcon, HeartSolidIcon} from '@salesforce/retail-react-app/app/components/icons' import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price' -import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' // Components import { @@ -30,6 +29,7 @@ import {useIntl} from 'react-intl' import {productUrlBuilder} from '@salesforce/retail-react-app/app/utils/url' import Link from '@salesforce/retail-react-app/app/components/link' import withRegistration from '@salesforce/retail-react-app/app/components/with-registration' +import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' const IconButtonWithRegistration = withRegistration(IconButton) @@ -67,7 +67,7 @@ const ProductTile = (props) => { ...rest } = props - const {image, productId, hitType} = product + const {image, productId} = product // ProductTile is used by two components, RecommendedProducts and ProductList. // RecommendedProducts provides a localized product name as `name` and non-localized product // name as `productName`. ProductList provides a localized name as `productName` and does not @@ -76,46 +76,21 @@ const ProductTile = (props) => { const isFavouriteLoading = useRef(false) const styles = useMultiStyleConfig('ProductTile') - // NOTE: swatches will implement later to set variant accordingly, - // On first load, get the variant that is the represent product - // this is for variant product, standard/set/bundles does not have variants + // On first load, for variant product, get the variant that is the represent product // Also, product tile can be used in RecommendedProducts where it calls getProducts which does not have representedProduct // in that case we use the first variant that has price book to set up discount price // Not all variants has set in a priceBook, meaning not having tieredPrices. - const variant = useMemo(() => { - return product?.variants?.find( - (i) => i?.productId === product?.representedProduct?.id || !!i?.tieredPrices - ) - }, [product]) - // prioritize variant promotionalPrice over standard price - let currentPrice = variant - ? getDisplayPrice(variant)?.discountPrice || variant?.price - : product?.price - - // check for both data returned from getProducts and productSearch - const isProductASet = hitType === 'set' || !!product?.type?.set - const isProductABundle = hitType === 'bundle' || !!product?.type?.bundle - const isProductAStandard = hitType === 'product' || !!product?.type?.item - let tieredPrices - if (variant) { - tieredPrices = variant?.tieredPrices - } else if (isProductABundle || isProductAStandard) { - tieredPrices = product?.tieredPrices - } else { - // if none applies, we assume this is a product set - // product sets do not have tierPieces, we go with priceRanges - tieredPrices = product?.priceRanges - } + const variant = useMemo( + () => + product?.variants?.find( + (i) => i?.productId === product?.representedProduct?.id || !!i?.tieredPrices + ), + [product] + ) - let listPrice = useMemo(() => { - const maxPriceTier = tieredPrices - ? Math.max(...(tieredPrices || []).map((item) => item.price || item.maxPrice)) - : 0 - return tieredPrices?.find( - (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier - ) - }, [tieredPrices]) + const {listPrice, currentPrice} = getDisplayPrice({...product, ...variant}) + const isASet = product?.hitType === 'set' || !!product?.type?.set return ( @@ -145,24 +120,16 @@ const ProductTile = (props) => { {/* Price */} currentPrice || listPrice?.price > currentPrice - ? listPrice?.price || listPrice?.maxPrice - : null - } + strikethroughPrice={listPrice > currentPrice ? listPrice : null} prefixLabel={ - isProductASet + isASet ? intl.formatMessage({ id: 'product_view.label.starting_at_price', defaultMessage: 'Starting at' }) : null } - currentPriceProps={ - listPrice?.maxPrice > currentPrice || listPrice?.price > currentPrice - ? {as: 'b'} - : {as: 'span'} - } + currentPriceProps={listPrice > currentPrice ? {as: 'b'} : {as: 'span'}} currentPrice={currentPrice} /> diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index 24e451ee08..10cea71104 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -39,9 +39,9 @@ import Swatch from '@salesforce/retail-react-app/app/components/swatch-group/swa import SwatchGroup from '@salesforce/retail-react-app/app/components/swatch-group' import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' -const ProductViewHeader = ({name, basePrice, discountPrice, currency, category, productType}) => { +const ProductViewHeader = ({name, currentPrice, listPrice, currency, category, productType}) => { const isProductASet = productType?.set - + const intl = useIntl() return ( {category && ( @@ -55,19 +55,29 @@ const ProductViewHeader = ({name, basePrice, discountPrice, currency, category, {`${name}`} - + + currentPrice ? listPrice : null} + currentPrice={currentPrice} + currency={currency} + prefixLabel={ + isProductASet + ? intl.formatMessage({ + id: 'product_view.label.starting_at_price', + defaultMessage: 'Starting at' + }) + : null + } + /> + ) } ProductViewHeader.propTypes = { name: PropTypes.string, - basePrice: PropTypes.number, - discountPrice: PropTypes.number, + currentPrice: PropTypes.number, + listPrice: PropTypes.number, currency: PropTypes.string, category: PropTypes.array, productType: PropTypes.object @@ -124,7 +134,7 @@ const ProductView = forwardRef( stockLevel, stepQuantity } = useDerivedProduct(product, isProductPartOfSet) - const {basePrice, discountPrice} = getDisplayPrice(product) + const {listPrice, currentPrice} = getDisplayPrice(product) const canAddToWishlist = !isProductLoading const isProductASet = product?.type.set const errorContainerRef = useRef(null) @@ -290,8 +300,8 @@ const ProductView = forwardRef( { parameters: { ids: ids.join(','), perPricebook: true, - allVariationProperties: true + expand: [ + 'availability', + 'links', + 'promotions', + 'options', + 'images', + 'prices', + 'variations' + ] }, headers: { Authorization: `Bearer ${token}` diff --git a/packages/template-retail-react-app/app/pages/home/index.jsx b/packages/template-retail-react-app/app/pages/home/index.jsx index 19252f0d54..27e891a1c0 100644 --- a/packages/template-retail-react-app/app/pages/home/index.jsx +++ b/packages/template-retail-react-app/app/pages/home/index.jsx @@ -65,7 +65,6 @@ const Home = () => { parameters: { refine: [`cgid=${HOME_SHOP_PRODUCTS_CATEGORY_ID}`, 'htype=master'], perPricebook: true, - allVariationProperties: true, limit: HOME_SHOP_PRODUCTS_LIMIT } }) diff --git a/packages/template-retail-react-app/app/pages/product-detail/index.jsx b/packages/template-retail-react-app/app/pages/product-detail/index.jsx index f8cddb61c7..8aeba431cf 100644 --- a/packages/template-retail-react-app/app/pages/product-detail/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-detail/index.jsx @@ -79,6 +79,7 @@ const ProductDetail = () => { { parameters: { id: urlParams.get('pid') || productId, + perPricebook: true, allImages: true } }, diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index d999ce3cdb..59c38a8272 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -35,19 +35,31 @@ export const getDisplayVariationValues = (variationAttributes, values = {}) => { } /** - * This function extract the promotional price from a product. If there are more than one price, the smallest price will be picked + * This function extract the list price and current price of a product + * If a product has promotional price, it will prioritize that value for current price + * List price will take the highest value among the price book prices * @param {object} product - product detail object - * @returns {{discountPrice: number, basePrice: number | string}} + * @returns {{listPrice: number, currentPrice: number}} */ export const getDisplayPrice = (product) => { - const basePrice = product?.pricePerUnit || product?.price const promotionalPriceList = product?.productPromotions ?.map((promo) => promo.promotionalPrice) .filter((i) => i !== null && i !== undefined) - // choose the smallest price among the promotionalPrice - const discountPrice = promotionalPriceList?.length ? Math.min(...promotionalPriceList) : null + const promotionalPrice = promotionalPriceList?.length ? Math.min(...promotionalPriceList) : null + // prioritize variant promotionalPrice over standard price + let currentPrice = promotionalPrice || product?.price + + let tieredPrices = product?.tieredPrices || product?.priceRanges + const maxPriceTier = tieredPrices + ? Math.max(...(tieredPrices || []).map((item) => item.price || item.maxPrice)) + : 0 + + // find the tieredPrice with has the highest value price + const highestTieredPrice = tieredPrices?.find( + (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier + ) return { - basePrice, - discountPrice + listPrice: highestTieredPrice?.price || highestTieredPrice?.maxPrice, + currentPrice } } diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index 772bc65aa0..1f4ed66ee7 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -55,23 +55,61 @@ test('getDisplayVariationValues', () => { }) describe('getDisplayPrice', function () { - test('returns basePrice and discountPrice', () => { - const {basePrice, discountPrice} = getDisplayPrice( - mockedCustomerProductListsDetails.data[0] - ) + test('returns listPrice and currentPrice for product that has only priceRanges', () => { + const data = { + name: 'product name', + price: 37.76, + priceRanges: [ + { + maxPrice: 40.76, + minPrice: 30.76, + pricebook: 'gbp-m-list-prices' + }, + { + maxPrice: 37.76, + minPrice: 37.76, + pricebook: 'gbp-m-sale-prices' + } + ] + } + const {listPrice, currentPrice} = getDisplayPrice(data) - expect(basePrice).toBe(199.0) - expect(discountPrice).toBe(189.0) + expect(listPrice).toBe(40.76) + expect(currentPrice).toBe(37.76) }) - test('returns null if there is not discount promotion', () => { + test('returns listPrice and currentPrice for product that has only tieredPrices', () => { const data = { - ...mockedCustomerProductListsDetails.data[0], - productPromotions: [] + name: 'product name', + price: 25.6, + priceRanges: [ + { + maxPrice: 25.6, + minPrice: 25.6, + pricebook: 'gbp-m-list-prices' + }, + { + maxPrice: 25.6, + minPrice: 25.6, + pricebook: 'gbp-m-sale-prices' + } + ], + tieredPrices: [ + { + price: 30.6, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 25.6, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ] } - const {basePrice, discountPrice} = getDisplayPrice(data) + const {listPrice, currentPrice} = getDisplayPrice(data) - expect(basePrice).toBe(199.0) - expect(discountPrice).toBeNull() + expect(listPrice).toBe(30.6) + expect(currentPrice).toBe(25.6) }) }) From 56f0736bfc9e2700a34dee5962b1ea0790d971f8 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 25 Apr 2024 16:01:40 -0700 Subject: [PATCH 05/45] remove unused variables --- .../template-retail-react-app/app/utils/product-utils.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index 1f4ed66ee7..6b54c760fd 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -9,7 +9,6 @@ import { getDisplayPrice, getDisplayVariationValues } from '@salesforce/retail-react-app/app/utils/product-utils' -import {mockedCustomerProductListsDetails} from '@salesforce/retail-react-app/app/mocks/mock-data' const variationAttributes = [ { From ae1da4794916b83caf9459ab28636a581f4a5998 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 25 Apr 2024 16:05:40 -0700 Subject: [PATCH 06/45] remove unvalid param --- .../assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs index 25e3bb496f..93366bcdce 100644 --- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs +++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs @@ -58,7 +58,6 @@ const Home = () => { parameters: { refine: [`cgid=${HOME_SHOP_PRODUCTS_CATEGORY_ID}`, 'htype=master'], perPricebook: true, - allVariationProperties: true, limit: HOME_SHOP_PRODUCTS_LIMIT } }) From 5fc66c88dcb6dff9ef3350c5eb22492dcc32339a Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 26 Apr 2024 15:40:24 -0700 Subject: [PATCH 07/45] take closest tiered price into account --- .../app/components/product-view/index.jsx | 6 +- .../app/hooks/use-add-to-cart-modal.js | 12 +-- .../app/pages/product-detail/index.jsx | 93 ++++++++++--------- .../app/utils/product-utils.js | 36 +++++-- 4 files changed, 87 insertions(+), 60 deletions(-) diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index 10cea71104..65ac969597 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React, {forwardRef, useEffect, useRef, useState} from 'react' +import React, {forwardRef, useEffect, useMemo, useRef, useState} from 'react' import PropTypes from 'prop-types' import {useLocation} from 'react-router-dom' import {useIntl, FormattedMessage} from 'react-intl' @@ -134,7 +134,9 @@ const ProductView = forwardRef( stockLevel, stepQuantity } = useDerivedProduct(product, isProductPartOfSet) - const {listPrice, currentPrice} = getDisplayPrice(product) + const {listPrice, currentPrice} = useMemo(() => { + return getDisplayPrice(product, {quantity}) + }, [product, quantity]) const canAddToWishlist = !isProductLoading const isProductASet = product?.type.set const errorContainerRef = useRef(null) diff --git a/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js b/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js index a559679ac6..ecbdf2618d 100644 --- a/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js +++ b/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js @@ -115,8 +115,8 @@ export const AddToCartModal = () => { selectedVariationAttributes: variant.variationValues })?.images?.[0] const { - basePrice: lineItemBasePrice, - discountPrice: lineItemDiscountPrice + currentPrice: lineItemCurrentPrice, + listPrice: lineItemListPrice } = getDisplayPrice(product) const variationAttributeValues = getDisplayVariationValues( product.variationAttributes, @@ -171,12 +171,12 @@ export const AddToCartModal = () => { lineItemCurrentPrice + ? lineItemListPrice * quantity : null } + currentPrice={lineItemCurrentPrice * quantity} currency={currency} /> diff --git a/packages/template-retail-react-app/app/pages/product-detail/index.jsx b/packages/template-retail-react-app/app/pages/product-detail/index.jsx index 8aeba431cf..d7e5bf5923 100644 --- a/packages/template-retail-react-app/app/pages/product-detail/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-detail/index.jsx @@ -80,6 +80,15 @@ const ProductDetail = () => { parameters: { id: urlParams.get('pid') || productId, perPricebook: true, + expand: [ + 'availability', + 'promotions', + 'options', + 'images', + 'prices', + 'variations', + 'set_products' + ], allImages: true } }, @@ -407,48 +416,48 @@ const ProductDetail = () => { )} {/* Product Recommendations */} - - {!isProductASet && ( - - } - recommender={EINSTEIN_RECOMMENDERS.PDP_COMPLETE_SET} - products={[product]} - mx={{base: -4, md: -8, lg: 0}} - shouldFetch={() => product?.id} - /> - )} - - } - recommender={EINSTEIN_RECOMMENDERS.PDP_MIGHT_ALSO_LIKE} - products={[product]} - mx={{base: -4, md: -8, lg: 0}} - shouldFetch={() => product?.id} - /> - - - } - recommender={EINSTEIN_RECOMMENDERS.PDP_RECENTLY_VIEWED} - mx={{base: -4, md: -8, lg: 0}} - /> - + {/**/} + {/* {!isProductASet && (*/} + {/* */} + {/* }*/} + {/* recommender={EINSTEIN_RECOMMENDERS.PDP_COMPLETE_SET}*/} + {/* products={[product]}*/} + {/* mx={{base: -4, md: -8, lg: 0}}*/} + {/* shouldFetch={() => product?.id}*/} + {/* />*/} + {/* )}*/} + {/* */} + {/* }*/} + {/* recommender={EINSTEIN_RECOMMENDERS.PDP_MIGHT_ALSO_LIKE}*/} + {/* products={[product]}*/} + {/* mx={{base: -4, md: -8, lg: 0}}*/} + {/* shouldFetch={() => product?.id}*/} + {/* />*/} + + {/* */} + {/* }*/} + {/* recommender={EINSTEIN_RECOMMENDERS.PDP_RECENTLY_VIEWED}*/} + {/* mx={{base: -4, md: -8, lg: 0}}*/} + {/* />*/} + {/**/} ) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 59c38a8272..dac9639697 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -39,27 +39,43 @@ export const getDisplayVariationValues = (variationAttributes, values = {}) => { * If a product has promotional price, it will prioritize that value for current price * List price will take the highest value among the price book prices * @param {object} product - product detail object + * @param {object} opts - product detail object * @returns {{listPrice: number, currentPrice: number}} */ -export const getDisplayPrice = (product) => { +export const getDisplayPrice = (product, opts = {}) => { + const {quantity = 1} = opts const promotionalPriceList = product?.productPromotions ?.map((promo) => promo.promotionalPrice) .filter((i) => i !== null && i !== undefined) const promotionalPrice = promotionalPriceList?.length ? Math.min(...promotionalPriceList) : null - // prioritize variant promotionalPrice over standard price - let currentPrice = promotionalPrice || product?.price - let tieredPrices = product?.tieredPrices || product?.priceRanges - const maxPriceTier = tieredPrices - ? Math.max(...(tieredPrices || []).map((item) => item.price || item.maxPrice)) + // if a product has tieredPrices, get the tiered that has the closest quantity to current quantity + const closestTieredPrice = product?.tieredPrices?.reduce((prev, curr) => { + return Math.abs(curr.quantity - quantity) < Math.abs(prev.quantity - quantity) ? curr : prev + }) + let currentPrice = Math.min( + ...[closestTieredPrice?.price, promotionalPrice, product?.price].filter(Boolean) + ) + + // pick the price range that has the highest maxPrice + const maxPriceRange = product?.priceRanges + ? Math.max(...(product?.priceRanges || []).map((item) => item.maxPrice)) : 0 + const priceRange = product?.priceRanges?.find((range) => range.maxPrice === maxPriceRange) + // TODO: how to deal with variant that does not have priceRanges, but tiredPrices + // tieredPrices is not suitable to use as list price + // let tieredPrices = product?.tieredPrices || product?.priceRanges + // const maxPriceTier = tieredPrices + // ? Math.max(...(tieredPrices || []).map((item) => item.price || item.maxPrice)) + // : 0 + // // find the tieredPrice with has the highest value price - const highestTieredPrice = tieredPrices?.find( - (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier - ) + // const highestTieredPrice = tieredPrices?.find( + // (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier + // ) return { - listPrice: highestTieredPrice?.price || highestTieredPrice?.maxPrice, + listPrice: priceRange?.maxPrice, currentPrice } } From b2bbabb6161432faae047fc6a0c64139ea589dfb Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 26 Apr 2024 16:10:52 -0700 Subject: [PATCH 08/45] fix test --- .../app/components/product-view/index.jsx | 1 + .../app/utils/product-utils.js | 28 ++--- .../app/utils/product-utils.test.js | 114 ++++++++++-------- 3 files changed, 77 insertions(+), 66 deletions(-) diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index 65ac969597..c843b12fea 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -42,6 +42,7 @@ import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-ut const ProductViewHeader = ({name, currentPrice, listPrice, currency, category, productType}) => { const isProductASet = productType?.set const intl = useIntl() + console.log('currentPrice cccc', currentPrice) return ( {category && ( diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index dac9639697..698871e294 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -53,29 +53,21 @@ export const getDisplayPrice = (product, opts = {}) => { const closestTieredPrice = product?.tieredPrices?.reduce((prev, curr) => { return Math.abs(curr.quantity - quantity) < Math.abs(prev.quantity - quantity) ? curr : prev }) - let currentPrice = Math.min( - ...[closestTieredPrice?.price, promotionalPrice, product?.price].filter(Boolean) - ) + const salePrices = [closestTieredPrice?.price, promotionalPrice, product?.price].filter(Boolean) + // pick the smallest price among these price for the "current" price + let currentPrice = salePrices?.length ? Math.min(...salePrices) : 0 - // pick the price range that has the highest maxPrice - const maxPriceRange = product?.priceRanges - ? Math.max(...(product?.priceRanges || []).map((item) => item.maxPrice)) + let tieredPrices = product?.priceRanges || product?.tieredPrices || [] + const maxPriceTier = tieredPrices + ? Math.max(...tieredPrices.map((item) => item.price || item.maxPrice)) : 0 - const priceRange = product?.priceRanges?.find((range) => range.maxPrice === maxPriceRange) - // TODO: how to deal with variant that does not have priceRanges, but tiredPrices - // tieredPrices is not suitable to use as list price - // let tieredPrices = product?.tieredPrices || product?.priceRanges - // const maxPriceTier = tieredPrices - // ? Math.max(...(tieredPrices || []).map((item) => item.price || item.maxPrice)) - // : 0 - // // find the tieredPrice with has the highest value price - // const highestTieredPrice = tieredPrices?.find( - // (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier - // ) + const highestTieredPrice = tieredPrices?.find( + (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier + ) return { - listPrice: priceRange?.maxPrice, + listPrice: highestTieredPrice?.price || highestTieredPrice?.maxPrice, currentPrice } } diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index 6b54c760fd..a49a759d9f 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -10,46 +10,48 @@ import { getDisplayVariationValues } from '@salesforce/retail-react-app/app/utils/product-utils' -const variationAttributes = [ - { - id: 'color', - name: 'Colour', - values: [ - {name: 'Black', orderable: true, value: 'BLACKLE'}, - {name: 'Taupe', orderable: true, value: 'TAUPETX'} - ] - }, - { - id: 'size', - name: 'Size', - values: [ - {name: '6', orderable: true, value: '060'}, - {name: '6.5', orderable: true, value: '065'}, - {name: '7', orderable: true, value: '070'}, - {name: '7.5', orderable: true, value: '075'}, - {name: '8', orderable: true, value: '080'}, - {name: '8.5', orderable: true, value: '085'}, - {name: '9', orderable: true, value: '090'}, - {name: '9.5', orderable: true, value: '095'}, - {name: '10', orderable: true, value: '100'}, - {name: '11', orderable: true, value: '110'} - ] - }, - {id: 'width', name: 'Width', values: [{name: 'M', orderable: true, value: 'M'}]} -] +describe('getDisplayVariationValues', function () { + const variationAttributes = [ + { + id: 'color', + name: 'Colour', + values: [ + {name: 'Black', orderable: true, value: 'BLACKLE'}, + {name: 'Taupe', orderable: true, value: 'TAUPETX'} + ] + }, + { + id: 'size', + name: 'Size', + values: [ + {name: '6', orderable: true, value: '060'}, + {name: '6.5', orderable: true, value: '065'}, + {name: '7', orderable: true, value: '070'}, + {name: '7.5', orderable: true, value: '075'}, + {name: '8', orderable: true, value: '080'}, + {name: '8.5', orderable: true, value: '085'}, + {name: '9', orderable: true, value: '090'}, + {name: '9.5', orderable: true, value: '095'}, + {name: '10', orderable: true, value: '100'}, + {name: '11', orderable: true, value: '110'} + ] + }, + {id: 'width', name: 'Width', values: [{name: 'M', orderable: true, value: 'M'}]} + ] -test('getDisplayVariationValues', () => { - const selectedValues = { - color: 'TAUPETX', - size: '065', - width: 'M' - } - const result = getDisplayVariationValues(variationAttributes, selectedValues) + test('returned selected values', () => { + const selectedValues = { + color: 'TAUPETX', + size: '065', + width: 'M' + } + const result = getDisplayVariationValues(variationAttributes, selectedValues) - expect(result).toEqual({ - Colour: 'Taupe', - Size: '6.5', - Width: 'M' + expect(result).toEqual({ + Colour: 'Taupe', + Size: '6.5', + Width: 'M' + }) }) }) @@ -81,18 +83,29 @@ describe('getDisplayPrice', function () { const data = { name: 'product name', price: 25.6, - priceRanges: [ + tieredPrices: [ { - maxPrice: 25.6, - minPrice: 25.6, - pricebook: 'gbp-m-list-prices' + price: 30.6, + pricebook: 'gbp-m-list-prices', + quantity: 1 }, { - maxPrice: 25.6, - minPrice: 25.6, - pricebook: 'gbp-m-sale-prices' + price: 25.6, + pricebook: 'gbp-m-sale-prices', + quantity: 1 } - ], + ] + } + const {listPrice, currentPrice} = getDisplayPrice(data) + + expect(listPrice).toBe(30.6) + expect(currentPrice).toBe(25.6) + }) + + test('returns pick the closest tieredPrices for product currentPrice if quantity is more than 1', () => { + const data = { + name: 'product name', + price: 25.6, tieredPrices: [ { price: 30.6, @@ -103,12 +116,17 @@ describe('getDisplayPrice', function () { price: 25.6, pricebook: 'gbp-m-sale-prices', quantity: 1 + }, + { + price: 15, + pricebook: 'gbp-m-sale-prices', + quantity: 10 } ] } - const {listPrice, currentPrice} = getDisplayPrice(data) + const {listPrice, currentPrice} = getDisplayPrice(data, {quantity: 11}) expect(listPrice).toBe(30.6) - expect(currentPrice).toBe(25.6) + expect(currentPrice).toBe(15) }) }) From 87399dc2a8d2b872e87245bfe6ff55408d909c6a Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 26 Apr 2024 16:13:06 -0700 Subject: [PATCH 09/45] put back recommendation section --- .../app/pages/product-detail/index.jsx | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/template-retail-react-app/app/pages/product-detail/index.jsx b/packages/template-retail-react-app/app/pages/product-detail/index.jsx index d7e5bf5923..164582f6f0 100644 --- a/packages/template-retail-react-app/app/pages/product-detail/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-detail/index.jsx @@ -416,48 +416,48 @@ const ProductDetail = () => { )} {/* Product Recommendations */} - {/**/} - {/* {!isProductASet && (*/} - {/* */} - {/* }*/} - {/* recommender={EINSTEIN_RECOMMENDERS.PDP_COMPLETE_SET}*/} - {/* products={[product]}*/} - {/* mx={{base: -4, md: -8, lg: 0}}*/} - {/* shouldFetch={() => product?.id}*/} - {/* />*/} - {/* )}*/} - {/* */} - {/* }*/} - {/* recommender={EINSTEIN_RECOMMENDERS.PDP_MIGHT_ALSO_LIKE}*/} - {/* products={[product]}*/} - {/* mx={{base: -4, md: -8, lg: 0}}*/} - {/* shouldFetch={() => product?.id}*/} - {/* />*/} - - {/* */} - {/* }*/} - {/* recommender={EINSTEIN_RECOMMENDERS.PDP_RECENTLY_VIEWED}*/} - {/* mx={{base: -4, md: -8, lg: 0}}*/} - {/* />*/} - {/**/} + + {!isProductASet && ( + + } + recommender={EINSTEIN_RECOMMENDERS.PDP_COMPLETE_SET} + products={[product]} + mx={{base: -4, md: -8, lg: 0}} + shouldFetch={() => product?.id} + /> + )} + + } + recommender={EINSTEIN_RECOMMENDERS.PDP_MIGHT_ALSO_LIKE} + products={[product]} + mx={{base: -4, md: -8, lg: 0}} + shouldFetch={() => product?.id} + /> + + + } + recommender={EINSTEIN_RECOMMENDERS.PDP_RECENTLY_VIEWED} + mx={{base: -4, md: -8, lg: 0}} + /> + ) From 467240792a4d9de1011fccdf2e19c637ee154706 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 26 Apr 2024 16:16:34 -0700 Subject: [PATCH 10/45] changing label --- .../components/item-variant/item-price.jsx | 4 ++-- .../app/components/product-tile/index.jsx | 4 ++-- .../app/components/product-tile/index.test.js | 4 ++-- .../app/components/product-view/index.jsx | 4 ++-- .../app/components/product-view/index.test.js | 8 ++++---- .../static/translations/compiled/en-GB.json | 16 +++++++-------- .../static/translations/compiled/en-US.json | 16 +++++++-------- .../static/translations/compiled/en-XA.json | 20 +++++++++---------- .../translations/en-GB.json | 10 +++++----- .../translations/en-US.json | 10 +++++----- 10 files changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/template-retail-react-app/app/components/item-variant/item-price.jsx b/packages/template-retail-react-app/app/components/item-variant/item-price.jsx index cc3c422705..7cbc621f75 100644 --- a/packages/template-retail-react-app/app/components/item-variant/item-price.jsx +++ b/packages/template-retail-react-app/app/components/item-variant/item-price.jsx @@ -73,8 +73,8 @@ const ItemPrice = ({currency, align = 'right', baseDirection = 'column', ...prop {isProductASet && `${intl.formatMessage({ - defaultMessage: 'Starting at', - id: 'item_price.label.starting_at' + defaultMessage: 'From', + id: 'item_price.label.from' })} `} { prefixLabel={ isASet ? intl.formatMessage({ - id: 'product_view.label.starting_at_price', - defaultMessage: 'Starting at' + id: 'product_view.label.from', + defaultMessage: 'From' }) : null } diff --git a/packages/template-retail-react-app/app/components/product-tile/index.test.js b/packages/template-retail-react-app/app/components/product-tile/index.test.js index bd4d3724b6..38caa663ad 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.test.js +++ b/packages/template-retail-react-app/app/components/product-tile/index.test.js @@ -30,7 +30,7 @@ test('Renders Skeleton', () => { test('Product set - renders the appropriate price label', async () => { const {getByText} = renderWithProviders() - expect(getByText(/starting at/i)).toBeInTheDocument() + expect(getByText(/from/i)).toBeInTheDocument() }) test('Remove from wishlist cannot be muti-clicked', () => { @@ -71,7 +71,7 @@ test('renders strike through price with set product', () => { ) expect(getByText(/Winter Look/i)).toBeInTheDocument() - expect(getByText(/Starting at/i)).toBeInTheDocument() + expect(getByText(/from/i)).toBeInTheDocument() expect(getByText(/£44\.16/i)).toBeInTheDocument() expect(getByText(/£101\.76/i)).toBeInTheDocument() diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index c843b12fea..186c3bc01c 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -64,8 +64,8 @@ const ProductViewHeader = ({name, currentPrice, listPrice, currency, category, p prefixLabel={ isProductASet ? intl.formatMessage({ - id: 'product_view.label.starting_at_price', - defaultMessage: 'Starting at' + id: 'product_view.label.from', + defaultMessage: 'From' }) : null } diff --git a/packages/template-retail-react-app/app/components/product-view/index.test.js b/packages/template-retail-react-app/app/components/product-view/index.test.js index 532f46dac7..f9ab13682f 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.test.js +++ b/packages/template-retail-react-app/app/components/product-view/index.test.js @@ -137,14 +137,14 @@ test('renders a product set properly - parent item', () => { // NOTE: there can be duplicates of the same element, due to mobile and desktop views // (they're hidden with display:none style) - const startingAtLabel = screen.getAllByText(/starting at/i)[0] + const fromAtLabel = screen.getAllByText(/from/i)[0] const addSetToCartButton = screen.getAllByRole('button', {name: /add set to cart/i})[0] const addSetToWishlistButton = screen.getAllByRole('button', {name: /add set to wishlist/i})[0] const variationAttributes = screen.queryAllByRole('radiogroup') // e.g. sizes, colors const quantityPicker = screen.queryByRole('spinbutton', {name: /quantity/i}) // What should exist: - expect(startingAtLabel).toBeInTheDocument() + expect(fromAtLabel).toBeInTheDocument() expect(addSetToCartButton).toBeInTheDocument() expect(addSetToWishlistButton).toBeInTheDocument() @@ -166,7 +166,7 @@ test('renders a product set properly - child item', () => { const addToWishlistButton = screen.getAllByRole('button', {name: /add to wishlist/i})[0] const variationAttributes = screen.getAllByRole('radiogroup') // e.g. sizes, colors const quantityPicker = screen.getByRole('spinbutton', {name: /quantity/i}) - const startingAtLabels = screen.queryAllByText(/starting at/i) + const fromLabels = screen.queryAllByText(/from/i) // What should exist: expect(addToCartButton).toBeInTheDocument() @@ -175,7 +175,7 @@ test('renders a product set properly - child item', () => { expect(quantityPicker).toBeInTheDocument() // What should _not_ exist: - expect(startingAtLabels).toHaveLength(0) + expect(fromLabels).toHaveLength(0) }) test('validateOrderability callback is called when adding a set to cart', async () => { diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index f0c724d02b..51cda7e623 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -1667,10 +1667,10 @@ "value": "Unavailable" } ], - "item_price.label.starting_at": [ + "item_price.label.from": [ { "type": 0, - "value": "Starting at" + "value": "From" } ], "lCPCxk": [ @@ -2557,6 +2557,12 @@ "value": "Increment Quantity" } ], + "product_view.label.from": [ + { + "type": 0, + "value": "From" + } + ], "product_view.label.quantity": [ { "type": 0, @@ -2575,12 +2581,6 @@ "value": "+" } ], - "product_view.label.starting_at_price": [ - { - "type": 0, - "value": "Starting at" - } - ], "product_view.label.variant_type": [ { "type": 1, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index f0c724d02b..51cda7e623 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -1667,10 +1667,10 @@ "value": "Unavailable" } ], - "item_price.label.starting_at": [ + "item_price.label.from": [ { "type": 0, - "value": "Starting at" + "value": "From" } ], "lCPCxk": [ @@ -2557,6 +2557,12 @@ "value": "Increment Quantity" } ], + "product_view.label.from": [ + { + "type": 0, + "value": "From" + } + ], "product_view.label.quantity": [ { "type": 0, @@ -2575,12 +2581,6 @@ "value": "+" } ], - "product_view.label.starting_at_price": [ - { - "type": 0, - "value": "Starting at" - } - ], "product_view.label.variant_type": [ { "type": 1, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index 1ee7d2f973..27804c22af 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -3523,14 +3523,14 @@ "value": "]" } ], - "item_price.label.starting_at": [ + "item_price.label.from": [ { "type": 0, "value": "[" }, { "type": 0, - "value": "Şŧȧȧřŧīƞɠ ȧȧŧ" + "value": "Ƒřǿǿḿ" }, { "type": 0, @@ -5437,56 +5437,56 @@ "value": "]" } ], - "product_view.label.quantity": [ + "product_view.label.from": [ { "type": 0, "value": "[" }, { "type": 0, - "value": "Ɋŭŭȧȧƞŧīŧẏ" + "value": "Ƒřǿǿḿ" }, { "type": 0, "value": "]" } ], - "product_view.label.quantity_decrement": [ + "product_view.label.quantity": [ { "type": 0, "value": "[" }, { "type": 0, - "value": "−" + "value": "Ɋŭŭȧȧƞŧīŧẏ" }, { "type": 0, "value": "]" } ], - "product_view.label.quantity_increment": [ + "product_view.label.quantity_decrement": [ { "type": 0, "value": "[" }, { "type": 0, - "value": "+" + "value": "−" }, { "type": 0, "value": "]" } ], - "product_view.label.starting_at_price": [ + "product_view.label.quantity_increment": [ { "type": 0, "value": "[" }, { "type": 0, - "value": "Şŧȧȧřŧīƞɠ ȧȧŧ" + "value": "+" }, { "type": 0, diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 8b80bb74d8..060bc37098 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -702,8 +702,8 @@ "defaultMessage": "Unavailable", "description": "A unavailable badge placed on top of a product image" }, - "item_price.label.starting_at": { - "defaultMessage": "Starting at" + "item_price.label.from": { + "defaultMessage": "From" }, "lCPCxk": { "defaultMessage": "Please select all your options above" @@ -1093,6 +1093,9 @@ "product_view.label.assistive_msg.quantity_increment": { "defaultMessage": "Increment Quantity" }, + "product_view.label.from": { + "defaultMessage": "From" + }, "product_view.label.quantity": { "defaultMessage": "Quantity" }, @@ -1102,9 +1105,6 @@ "product_view.label.quantity_increment": { "defaultMessage": "+" }, - "product_view.label.starting_at_price": { - "defaultMessage": "Starting at" - }, "product_view.label.variant_type": { "defaultMessage": "{variantType}" }, diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 8b80bb74d8..060bc37098 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -702,8 +702,8 @@ "defaultMessage": "Unavailable", "description": "A unavailable badge placed on top of a product image" }, - "item_price.label.starting_at": { - "defaultMessage": "Starting at" + "item_price.label.from": { + "defaultMessage": "From" }, "lCPCxk": { "defaultMessage": "Please select all your options above" @@ -1093,6 +1093,9 @@ "product_view.label.assistive_msg.quantity_increment": { "defaultMessage": "Increment Quantity" }, + "product_view.label.from": { + "defaultMessage": "From" + }, "product_view.label.quantity": { "defaultMessage": "Quantity" }, @@ -1102,9 +1105,6 @@ "product_view.label.quantity_increment": { "defaultMessage": "+" }, - "product_view.label.starting_at_price": { - "defaultMessage": "Starting at" - }, "product_view.label.variant_type": { "defaultMessage": "{variantType}" }, From 2ae213cfca2f4e1abecd6838c5de1e26da1b4a9c Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 09:40:22 -0700 Subject: [PATCH 11/45] fix bundle size --- packages/template-retail-react-app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template-retail-react-app/package.json b/packages/template-retail-react-app/package.json index 27b24c3f38..ab9ed15ad3 100644 --- a/packages/template-retail-react-app/package.json +++ b/packages/template-retail-react-app/package.json @@ -93,7 +93,7 @@ "bundlesize": [ { "path": "build/main.js", - "maxSize": "43 kB" + "maxSize": "44 kB" }, { "path": "build/vendor.js", From 67892d77822487092229b765303b9136cc109c96 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 11:08:58 -0700 Subject: [PATCH 12/45] fix tierPrices filter logic --- .../app/components/product-tile/index.jsx | 16 ++++++++-------- .../app/components/product-view/index.jsx | 1 - .../app/utils/product-utils.js | 15 +++++++++++---- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.jsx b/packages/template-retail-react-app/app/components/product-tile/index.jsx index 7064985b0c..1d31dad343 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.jsx +++ b/packages/template-retail-react-app/app/components/product-tile/index.jsx @@ -26,7 +26,7 @@ import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-im import {useIntl} from 'react-intl' // Other -import {productUrlBuilder} from '@salesforce/retail-react-app/app/utils/url' +import {productUrlBuilder, rebuildPathWithParams} from '@salesforce/retail-react-app/app/utils/url' import Link from '@salesforce/retail-react-app/app/components/link' import withRegistration from '@salesforce/retail-react-app/app/components/with-registration' import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' @@ -91,15 +91,15 @@ const ProductTile = (props) => { const {listPrice, currentPrice} = getDisplayPrice({...product, ...variant}) const isASet = product?.hitType === 'set' || !!product?.type?.set - + let productUrl = variant + ? rebuildPathWithParams(productUrlBuilder({id: productId}), { + ...variant.variationValues, + pid: variant.productId + }) + : productUrlBuilder({id: productId}) return ( - + {image && ( diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index 186c3bc01c..ad3c926ac3 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -42,7 +42,6 @@ import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-ut const ProductViewHeader = ({name, currentPrice, listPrice, currency, category, productType}) => { const isProductASet = productType?.set const intl = useIntl() - console.log('currentPrice cccc', currentPrice) return ( {category && ( diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 698871e294..2e0311ef2a 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -5,6 +5,8 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import {isEmpty} from 'lodash' + /** * Get the human-friendly version of the variation values that users have selected. * Useful for displaying these values in the UI. @@ -49,10 +51,15 @@ export const getDisplayPrice = (product, opts = {}) => { .filter((i) => i !== null && i !== undefined) const promotionalPrice = promotionalPriceList?.length ? Math.min(...promotionalPriceList) : null - // if a product has tieredPrices, get the tiered that has the closest quantity to current quantity - const closestTieredPrice = product?.tieredPrices?.reduce((prev, curr) => { - return Math.abs(curr.quantity - quantity) < Math.abs(prev.quantity - quantity) ? curr : prev - }) + // if a product has tieredPrices, get the tiered that has the higher closest quantity to current quantity + const filteredTiered = product?.tieredPrices?.filter((tiered) => tiered.quantity <= quantity) + const closestTieredPrice = + !isEmpty(filteredTiered) && + filteredTiered.reduce((prev, curr) => { + return Math.abs(curr.quantity - quantity) < Math.abs(prev.quantity - quantity) + ? curr + : prev + }) const salePrices = [closestTieredPrice?.price, promotionalPrice, product?.price].filter(Boolean) // pick the smallest price among these price for the "current" price let currentPrice = salePrices?.length ? Math.min(...salePrices) : 0 From 74df5ab59224dcb199aa89e349332a54deb35fdd Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 11:23:42 -0700 Subject: [PATCH 13/45] fix tierPrices filter logic --- packages/template-retail-react-app/app/utils/product-utils.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 2e0311ef2a..572a8e6670 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -5,8 +5,6 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import {isEmpty} from 'lodash' - /** * Get the human-friendly version of the variation values that users have selected. * Useful for displaying these values in the UI. @@ -54,7 +52,7 @@ export const getDisplayPrice = (product, opts = {}) => { // if a product has tieredPrices, get the tiered that has the higher closest quantity to current quantity const filteredTiered = product?.tieredPrices?.filter((tiered) => tiered.quantity <= quantity) const closestTieredPrice = - !isEmpty(filteredTiered) && + !filteredTiered.length && filteredTiered.reduce((prev, curr) => { return Math.abs(curr.quantity - quantity) < Math.abs(prev.quantity - quantity) ? curr From 3c62c54c73068b3ec9d5576a26e5ba771f46ba78 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 11:26:07 -0700 Subject: [PATCH 14/45] fix tierPrices filter logic --- .../template-retail-react-app/app/utils/product-utils.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 572a8e6670..226330d696 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -50,9 +50,10 @@ export const getDisplayPrice = (product, opts = {}) => { const promotionalPrice = promotionalPriceList?.length ? Math.min(...promotionalPriceList) : null // if a product has tieredPrices, get the tiered that has the higher closest quantity to current quantity - const filteredTiered = product?.tieredPrices?.filter((tiered) => tiered.quantity <= quantity) + const filteredTiered = + product?.tieredPrices?.filter((tiered) => tiered.quantity <= quantity) || [] const closestTieredPrice = - !filteredTiered.length && + filteredTiered.length && filteredTiered.reduce((prev, curr) => { return Math.abs(curr.quantity - quantity) < Math.abs(prev.quantity - quantity) ? curr From 5025ed27d9b0a17fb5bef6b019486a73d5e3e8aa Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 11:36:53 -0700 Subject: [PATCH 15/45] fix tierPrices filter logic --- .../app/utils/product-utils.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 226330d696..38d17f122e 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -63,17 +63,20 @@ export const getDisplayPrice = (product, opts = {}) => { // pick the smallest price among these price for the "current" price let currentPrice = salePrices?.length ? Math.min(...salePrices) : 0 - let tieredPrices = product?.priceRanges || product?.tieredPrices || [] - const maxPriceTier = tieredPrices - ? Math.max(...tieredPrices.map((item) => item.price || item.maxPrice)) + // Master product and product set have priceRanges object, the others (variant/standard/bundle) have tieredPrices + const maxPriceRange = product?.priceRanges + ? Math.max(...product?.priceRanges?.map((item) => item.price || item.maxPrice)) : 0 - - // find the tieredPrice with has the highest value price - const highestTieredPrice = tieredPrices?.find( - (tier) => tier.price === maxPriceTier || tier.maxPrice === maxPriceTier + const highestPriceRange = product?.priceRanges?.find( + (range) => range.maxPrice === maxPriceRange ) + const maxTieredPrice = product?.tieredPrices + ? Math.max(...product?.tieredPrices?.map((item) => item.price || item.maxPrice)) + : 0 + // find the tieredPrice with has the highest value price + const highestTieredPrice = product?.tieredPrices?.find((tier) => tier.price === maxTieredPrice) return { - listPrice: highestTieredPrice?.price || highestTieredPrice?.maxPrice, + listPrice: highestPriceRange?.maxPrice || highestTieredPrice?.price, currentPrice } } From 1753373e32cc049782e3c70f436b5e76bdb8b5f5 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 11:45:50 -0700 Subject: [PATCH 16/45] fix lint --- packages/template-retail-react-app/app/utils/product-utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 38d17f122e..ad73e7d49c 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -65,13 +65,13 @@ export const getDisplayPrice = (product, opts = {}) => { // Master product and product set have priceRanges object, the others (variant/standard/bundle) have tieredPrices const maxPriceRange = product?.priceRanges - ? Math.max(...product?.priceRanges?.map((item) => item.price || item.maxPrice)) + ? Math.max(...(product?.priceRanges || []).map((item) => item.price || item.maxPrice)) : 0 const highestPriceRange = product?.priceRanges?.find( (range) => range.maxPrice === maxPriceRange ) const maxTieredPrice = product?.tieredPrices - ? Math.max(...product?.tieredPrices?.map((item) => item.price || item.maxPrice)) + ? Math.max(...(product?.tieredPrices || []).map((item) => item.price || item.maxPrice)) : 0 // find the tieredPrice with has the highest value price const highestTieredPrice = product?.tieredPrices?.find((tier) => tier.price === maxTieredPrice) From 913110c9bcfd6c4800f49c2f14fe1a0df3ba0c37 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 12:01:49 -0700 Subject: [PATCH 17/45] add more cases for display price util --- .../app/utils/product-utils.test.js | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index a49a759d9f..de9dae236c 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -102,7 +102,7 @@ describe('getDisplayPrice', function () { expect(currentPrice).toBe(25.6) }) - test('returns pick the closest tieredPrices for product currentPrice if quantity is more than 1', () => { + test('returns pick the closest tieredPrices for product currentPrice quantity', () => { const data = { name: 'product name', price: 25.6, @@ -129,4 +129,66 @@ describe('getDisplayPrice', function () { expect(listPrice).toBe(30.6) expect(currentPrice).toBe(15) }) + + test('should not pick the discounted when it does not reach the tierd quantity', () => { + const data = { + name: 'product name', + price: 25.6, + tieredPrices: [ + { + price: 30.6, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 25.6, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + }, + { + price: 15, + pricebook: 'gbp-m-sale-prices', + quantity: 10 + } + ] + } + const {listPrice, currentPrice} = getDisplayPrice(data, {quantity: 9}) + + expect(listPrice).toBe(30.6) + expect(currentPrice).toBe(25.6) + }) + + test('returns pick the promotional Price for product currentPrice when it is the lowest price among other sale prices', () => { + const data = { + name: 'product name', + price: 25.6, + productPromotions: [ + { + promotionalPrice: 10.99, + id: 'promotional-price' + } + ], + tieredPrices: [ + { + price: 30.6, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 25.6, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + }, + { + price: 15, + pricebook: 'gbp-m-sale-prices', + quantity: 10 + } + ] + } + const {listPrice, currentPrice} = getDisplayPrice(data, {quantity: 11}) + + expect(listPrice).toBe(30.6) + expect(currentPrice).toBe(10.99) + }) }) From 5392f7ef87beeb69e7e7ccbed57d6eb8db0f9cb4 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 12:04:41 -0700 Subject: [PATCH 18/45] add more cases for display price util --- .../template-retail-react-app/app/utils/product-utils.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index de9dae236c..2a5aa3c3fa 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -130,7 +130,7 @@ describe('getDisplayPrice', function () { expect(currentPrice).toBe(15) }) - test('should not pick the discounted when it does not reach the tierd quantity', () => { + test('should not pick the discounted tiered when it does not reach the tierd quantity', () => { const data = { name: 'product name', price: 25.6, From 809ca51b0331f693ca7c9a575400f7f910403f24 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 14:08:25 -0700 Subject: [PATCH 19/45] fix logic in getDisplayPrice --- packages/template-retail-react-app/app/utils/product-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index ad73e7d49c..e7a29bc676 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -76,7 +76,7 @@ export const getDisplayPrice = (product, opts = {}) => { // find the tieredPrice with has the highest value price const highestTieredPrice = product?.tieredPrices?.find((tier) => tier.price === maxTieredPrice) return { - listPrice: highestPriceRange?.maxPrice || highestTieredPrice?.price, + listPrice: highestTieredPrice?.price || highestPriceRange?.maxPrice, currentPrice } } From 90bd3d0481589d349bf25ce685d50ad5b1c793c4 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 14:15:09 -0700 Subject: [PATCH 20/45] PR feedback --- .../app/components/display-price/index.jsx | 8 +++----- .../app/components/product-tile/index.jsx | 3 +++ .../app/components/product-view/index.jsx | 7 ++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index e74193c3ac..0973b2ef56 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -9,7 +9,6 @@ import React from 'react' import PropTypes from 'prop-types' import {Box, Text} from '@salesforce/retail-react-app/app/components/shared/ui' import {useIntl} from 'react-intl' -import {useCurrency} from '@salesforce/retail-react-app/app/hooks' const DisplayPrice = ({ strikethroughPrice, @@ -20,16 +19,15 @@ const DisplayPrice = ({ strikethroughPriceProps }) => { const intl = useIntl() - const {currency: activeCurrency} = useCurrency() const currentPriceText = intl.formatNumber(currentPrice, { style: 'currency', - currency: currency || activeCurrency + currency: currency }) const strikethroughPriceText = strikethroughPrice && intl.formatNumber(strikethroughPrice, { style: 'currency', - currency: currency || activeCurrency + currency: currency }) return ( @@ -81,7 +79,7 @@ const DisplayPrice = ({ DisplayPrice.propTypes = { strikethroughPrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), currentPrice: PropTypes.number.isRequired, - currency: PropTypes.string, + currency: PropTypes.string.isRequired, prefixLabel: PropTypes.string, strikethroughPriceProps: PropTypes.object, currentPriceProps: PropTypes.object diff --git a/packages/template-retail-react-app/app/components/product-tile/index.jsx b/packages/template-retail-react-app/app/components/product-tile/index.jsx index 1d31dad343..f3ecf20377 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.jsx +++ b/packages/template-retail-react-app/app/components/product-tile/index.jsx @@ -30,6 +30,7 @@ import {productUrlBuilder, rebuildPathWithParams} from '@salesforce/retail-react import Link from '@salesforce/retail-react-app/app/components/link' import withRegistration from '@salesforce/retail-react-app/app/components/with-registration' import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' +import {useCurrency} from '@salesforce/retail-react-app/app/hooks' const IconButtonWithRegistration = withRegistration(IconButton) @@ -66,6 +67,7 @@ const ProductTile = (props) => { dynamicImageProps, ...rest } = props + const {currency} = useCurrency() const {image, productId} = product // ProductTile is used by two components, RecommendedProducts and ProductList. @@ -129,6 +131,7 @@ const ProductTile = (props) => { }) : null } + currency={currency} currentPriceProps={listPrice > currentPrice ? {as: 'b'} : {as: 'span'}} currentPrice={currentPrice} /> diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index ad3c926ac3..74543b4846 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -21,7 +21,7 @@ import { Fade, useTheme } from '@salesforce/retail-react-app/app/components/shared/ui' -import {useDerivedProduct} from '@salesforce/retail-react-app/app/hooks' +import {useCurrency, useDerivedProduct} from '@salesforce/retail-react-app/app/hooks' import {useAddToCartModalContext} from '@salesforce/retail-react-app/app/hooks/use-add-to-cart-modal' // project components @@ -111,6 +111,7 @@ const ProductView = forwardRef( }, ref ) => { + const {currency: activeCurrency} = useCurrency() const showToast = useToast() const intl = useIntl() const location = useLocation() @@ -305,7 +306,7 @@ const ProductView = forwardRef( listPrice={listPrice} currentPrice={currentPrice} productType={product?.type} - currency={product?.currency} + currency={product?.currency || activeCurrency} category={category} /> @@ -346,7 +347,7 @@ const ProductView = forwardRef( listPrice={listPrice} currentPrice={currentPrice} productType={product?.type} - currency={product?.currency} + currency={product?.currency || activeCurrency} category={category} /> From 906b52162412fc198de5e5203953286a159ee937 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 29 Apr 2024 17:21:00 -0700 Subject: [PATCH 21/45] PR feedback --- .../app/utils/product-utils.js | 16 ++++++++++------ .../app/utils/product-utils.test.js | 10 +++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index e7a29bc676..1c0b0ce036 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -63,20 +63,24 @@ export const getDisplayPrice = (product, opts = {}) => { // pick the smallest price among these price for the "current" price let currentPrice = salePrices?.length ? Math.min(...salePrices) : 0 - // Master product and product set have priceRanges object, the others (variant/standard/bundle) have tieredPrices + // Master product and product set have priceRanges object + // the price returned from API is the smallest price among price book + // to figure out what is the original price, we find the lastest minPrice among the books const maxPriceRange = product?.priceRanges - ? Math.max(...(product?.priceRanges || []).map((item) => item.price || item.maxPrice)) + ? Math.max(...(product?.priceRanges || []).map((item) => item.minPrice)) : 0 const highestPriceRange = product?.priceRanges?.find( - (range) => range.maxPrice === maxPriceRange + (range) => range.minPrice === maxPriceRange ) + // for standard/variant/bundle product, they dont have priceRanges, only tieredPrices + // since the price is the lowest value returned from the API, each product will have at lease a single item tiered price + // the highest value of tieredPrices is presumptively the list price const maxTieredPrice = product?.tieredPrices - ? Math.max(...(product?.tieredPrices || []).map((item) => item.price || item.maxPrice)) + ? Math.max(...(product?.tieredPrices || []).map((item) => item.price)) : 0 - // find the tieredPrice with has the highest value price const highestTieredPrice = product?.tieredPrices?.find((tier) => tier.price === maxTieredPrice) return { - listPrice: highestTieredPrice?.price || highestPriceRange?.maxPrice, + listPrice: highestTieredPrice?.price || highestPriceRange?.minPrice, currentPrice } } diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index 2a5aa3c3fa..8419941e70 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -59,24 +59,24 @@ describe('getDisplayPrice', function () { test('returns listPrice and currentPrice for product that has only priceRanges', () => { const data = { name: 'product name', - price: 37.76, + price: 30.76, priceRanges: [ { maxPrice: 40.76, - minPrice: 30.76, + minPrice: 35.76, pricebook: 'gbp-m-list-prices' }, { maxPrice: 37.76, - minPrice: 37.76, + minPrice: 30.76, pricebook: 'gbp-m-sale-prices' } ] } const {listPrice, currentPrice} = getDisplayPrice(data) - expect(listPrice).toBe(40.76) - expect(currentPrice).toBe(37.76) + expect(listPrice).toBe(35.76) + expect(currentPrice).toBe(30.76) }) test('returns listPrice and currentPrice for product that has only tieredPrices', () => { From 7a36794e04c2122e219df4303a81ea1541f6b9ee Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 30 Apr 2024 11:33:13 -0700 Subject: [PATCH 22/45] fix tests --- .../app/components/product-tile/index.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.test.js b/packages/template-retail-react-app/app/components/product-tile/index.test.js index 38caa663ad..9d9192a047 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.test.js +++ b/packages/template-retail-react-app/app/components/product-tile/index.test.js @@ -72,13 +72,13 @@ test('renders strike through price with set product', () => { ) expect(getByText(/Winter Look/i)).toBeInTheDocument() expect(getByText(/from/i)).toBeInTheDocument() + expect(getByText(/£40\.16/i)).toBeInTheDocument() expect(getByText(/£44\.16/i)).toBeInTheDocument() - expect(getByText(/£101\.76/i)).toBeInTheDocument() const currentPriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(currentPriceTag[0]).getByText(/£44\.16/i)).toBeDefined() - expect(within(strikethroughPriceTag[0]).getByText(/£101\.76/i)).toBeDefined() + expect(within(currentPriceTag[0]).getByText(/£40\.16/i)).toBeDefined() + expect(within(strikethroughPriceTag[0]).getByText(/£44\.16/i)).toBeDefined() expect(currentPriceTag).toHaveLength(1) expect(strikethroughPriceTag).toHaveLength(1) }) @@ -118,7 +118,7 @@ const mockProductSearchItemSet = { title: 'Winter Look, ' }, orderable: true, - price: 44.16, + price: 40.16, priceMax: 71.03, pricePerUnit: 44.16, pricePerUnitMax: 71.03, @@ -130,7 +130,7 @@ const mockProductSearchItemSet = { }, { maxPrice: 71.03, - minPrice: 44.16, + minPrice: 40.16, pricebook: 'gbp-m-sale-prices' } ], From 87fbc8b3a178f0fbd0907ec13ce815d35224b838 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 9 May 2024 13:30:08 -0700 Subject: [PATCH 23/45] Apply formattedmessage to display price, refactor price info logic in getPriceData --- .../app/components/_app/index.jsx | 12 +- .../app/components/display-price/index.jsx | 149 ++++---- .../components/display-price/index.test.js | 24 +- .../app/components/product-tile/index.jsx | 50 +-- .../app/components/product-tile/index.test.js | 333 ++---------------- .../app/components/product-view/index.jsx | 39 +- .../app/hooks/use-add-to-cart-modal.js | 15 +- .../app/hooks/use-derived-product.js | 7 +- .../app/mocks/product-search-hit-data.js | 217 ++++++++++++ .../app/pages/product-list/index.jsx | 2 +- .../static/translations/compiled/en-GB.json | 246 +++++++++++-- .../static/translations/compiled/en-US.json | 246 +++++++++++-- .../static/translations/compiled/en-XA.json | 262 ++++++++++++-- .../app/utils/product-utils.js | 105 ++++-- .../app/utils/product-utils.test.js | 18 +- .../app/utils/test-utils.js | 26 +- .../app/utils/utils.js | 17 + .../app/utils/utils.test.js | 43 ++- .../translations/en-GB.json | 10 +- .../translations/en-US.json | 10 +- 20 files changed, 1221 insertions(+), 610 deletions(-) create mode 100644 packages/template-retail-react-app/app/mocks/product-search-hit-data.js diff --git a/packages/template-retail-react-app/app/components/_app/index.jsx b/packages/template-retail-react-app/app/components/_app/index.jsx index 708e707057..429dc7c24c 100644 --- a/packages/template-retail-react-app/app/components/_app/index.jsx +++ b/packages/template-retail-react-app/app/components/_app/index.jsx @@ -57,7 +57,7 @@ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-curre import {withCommerceSdkReact} from '@salesforce/retail-react-app/app/components/with-commerce-sdk-react/with-commerce-sdk-react' // Localization -import {IntlProvider} from 'react-intl' +import {FormattedNumber, IntlProvider} from 'react-intl' // Others import {watchOnlineStatus, flatten, isServer} from '@salesforce/retail-react-app/app/utils/utils' @@ -74,6 +74,7 @@ import { import Seo from '@salesforce/retail-react-app/app/components/seo' import {Helmet} from 'react-helmet' +import {Text} from '@chakra-ui/react' const PlaceholderComponent = () => (
@@ -279,6 +280,15 @@ const App = (props) => { )} {chunks}, + br: () =>
, + p: (chunks) => {chunks}, + s: (chunks) => {chunks}, + span: (chunks) => {chunks} + }} onError={(err) => { if (!messages) { // During the ssr prepass phase the messages object has not loaded, so we can suppress diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 0973b2ef56..203a250e50 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -8,81 +8,94 @@ import React from 'react' import PropTypes from 'prop-types' import {Box, Text} from '@salesforce/retail-react-app/app/components/shared/ui' -import {useIntl} from 'react-intl' +import {FormattedMessage, FormattedNumber} from 'react-intl' -const DisplayPrice = ({ - strikethroughPrice, - currentPrice, - prefixLabel, - currency, - currentPriceProps, - strikethroughPriceProps -}) => { - const intl = useIntl() - const currentPriceText = intl.formatNumber(currentPrice, { - style: 'currency', - currency: currency - }) - const strikethroughPriceText = - strikethroughPrice && - intl.formatNumber(strikethroughPrice, { - style: 'currency', - currency: currency - }) - return ( - - {prefixLabel && ( - - {prefixLabel} - - )} - - {currentPriceText} +/** + * @param priceData - price info extracted from a product + * // If a product is a set, + * on PLP, don't show price at all + * on PDP, the set children will have it own price as From X (cross) Y + * // if a product is a master + * on PLP and PDP, show From X (cross) Y , the X and Y are + * sale and list price of variant that has the lowest price (including promotionalPrice) + * // if a standard/bundle + * show exact price on PLP and PDP as X (cross) Y + * @param currency - currency + */ +const DisplayPrice = ({priceData, currency}) => { + const PRICE_DATA_CUSTOM_TAGS = { + // we re-defined here since they are specific for this component + s: (chunks) => ( + + {chunks} - {/*Allowing display price of 0*/} - {typeof strikethroughPrice === 'number' && ( - - {strikethroughPriceText} - - )} + ), + price: (chunks) => + } + return ( + + ) } DisplayPrice.propTypes = { - strikethroughPrice: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - currentPrice: PropTypes.number.isRequired, - currency: PropTypes.string.isRequired, - prefixLabel: PropTypes.string, - strikethroughPriceProps: PropTypes.object, - currentPriceProps: PropTypes.object + priceData: PropTypes.shape({ + salePrice: PropTypes.number.isRequired, + isOnSale: PropTypes.bool.isRequired, + listPrice: PropTypes.number, + isASet: PropTypes.bool, + isMaster: PropTypes.bool, + isRange: PropTypes.bool, + hasRepresentedProduct: PropTypes.bool, + maxPrice: PropTypes.number, + tieredPrice: PropTypes.number + }), + currency: PropTypes.string } export default DisplayPrice diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index ba213e73d1..5625304d45 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -10,18 +10,23 @@ import DisplayPrice from '@salesforce/retail-react-app/app/components/display-pr import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' describe('DisplayPrice', function () { + const data = { + salePrice: 90, + listPrice: 100, + isASet: false, + isOnSale: true, + isMaster: true, + isRange: true, + hasRepresentedProduct: false + } test('should render without error', () => { - renderWithProviders( - - ) + renderWithProviders() expect(screen.getByText(/£90\.00/i)).toBeInTheDocument() expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() }) test('should render according html tag for prices', () => { - const {container} = renderWithProviders( - - ) + const {container} = renderWithProviders() const currentPriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') expect(within(currentPriceTag[0]).getByText(/£90\.00/i)).toBeDefined() @@ -30,8 +35,11 @@ describe('DisplayPrice', function () { expect(strikethroughPriceTag).toHaveLength(1) }) - test('should not render discount price if not available', () => { - renderWithProviders() + test('should not render list price when price is not on sale', () => { + renderWithProviders( + + ) + screen.logTestingPlaygroundURL() expect(screen.queryByText(/£90\.00/i)).not.toBeInTheDocument() expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() }) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.jsx b/packages/template-retail-react-app/app/components/product-tile/index.jsx index f3ecf20377..64c168c4b4 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.jsx +++ b/packages/template-retail-react-app/app/components/product-tile/index.jsx @@ -29,8 +29,9 @@ import {useIntl} from 'react-intl' import {productUrlBuilder, rebuildPathWithParams} from '@salesforce/retail-react-app/app/utils/url' import Link from '@salesforce/retail-react-app/app/components/link' import withRegistration from '@salesforce/retail-react-app/app/components/with-registration' -import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' +import {getPriceData} from '@salesforce/retail-react-app/app/utils/product-utils' import {useCurrency} from '@salesforce/retail-react-app/app/hooks' +import {PRICE_DISPLAY_FORMAT} from '@salesforce/retail-react-app/app/constants' const IconButtonWithRegistration = withRegistration(IconButton) @@ -78,30 +79,20 @@ const ProductTile = (props) => { const isFavouriteLoading = useRef(false) const styles = useMultiStyleConfig('ProductTile') - // NOTE: swatches will implement later to set variant accordingly, - // On first load, for variant product, get the variant that is the represent product - // Also, product tile can be used in RecommendedProducts where it calls getProducts which does not have representedProduct - // in that case we use the first variant that has price book to set up discount price - // Not all variants has set in a priceBook, meaning not having tieredPrices. - const variant = useMemo( - () => - product?.variants?.find( - (i) => i?.productId === product?.representedProduct?.id || !!i?.tieredPrices - ), - [product] - ) - const {listPrice, currentPrice} = getDisplayPrice({...product, ...variant}) - const isASet = product?.hitType === 'set' || !!product?.type?.set - let productUrl = variant - ? rebuildPathWithParams(productUrlBuilder({id: productId}), { - ...variant.variationValues, - pid: variant.productId - }) - : productUrlBuilder({id: productId}) + //TODO variants needs to be filter according to selectedAttribute value + const variants = product?.variants + + const priceData = getPriceData({...product, variants}) + return ( - + {image && ( @@ -121,20 +112,7 @@ const ProductTile = (props) => { {localizedProductName} {/* Price */} - currentPrice ? listPrice : null} - prefixLabel={ - isASet - ? intl.formatMessage({ - id: 'product_view.label.from', - defaultMessage: 'From' - }) - : null - } - currency={currency} - currentPriceProps={listPrice > currentPrice ? {as: 'b'} : {as: 'span'}} - currentPrice={currentPrice} - /> + {enableFavourite && ( { const {getAllByRole} = renderWithProviders() @@ -27,18 +33,12 @@ test('Renders Skeleton', () => { expect(skeleton).toBeDefined() }) -test('Product set - renders the appropriate price label', async () => { - const {getByText} = renderWithProviders() - - expect(getByText(/from/i)).toBeInTheDocument() -}) - test('Remove from wishlist cannot be muti-clicked', () => { const onClick = jest.fn() const {getByTestId} = renderWithProviders( @@ -52,329 +52,40 @@ test('Remove from wishlist cannot be muti-clicked', () => { test('renders strike through price with variants product', () => { const {getByText, container} = renderWithProviders( - + ) expect(getByText(/black flat front wool suit/i)).toBeInTheDocument() expect(getByText(/£191\.99/i)).toBeInTheDocument() expect(getByText(/£320\.00/i)).toBeInTheDocument() - const currentPriceTag = container.querySelectorAll('b') + const salePriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(currentPriceTag[0]).getByText(/£191\.99/i)).toBeDefined() + expect(within(salePriceTag[0]).getByText(/£191\.99/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£320\.00/i)).toBeDefined() - expect(currentPriceTag).toHaveLength(1) + expect(salePriceTag).toHaveLength(1) expect(strikethroughPriceTag).toHaveLength(1) }) -test('renders strike through price with set product', () => { - const {getByText, container} = renderWithProviders( - +test('Product set - does not render strike through price', () => { + const {getByText, queryByText, container} = renderWithProviders( + ) expect(getByText(/Winter Look/i)).toBeInTheDocument() - expect(getByText(/from/i)).toBeInTheDocument() - expect(getByText(/£40\.16/i)).toBeInTheDocument() - expect(getByText(/£44\.16/i)).toBeInTheDocument() - - const currentPriceTag = container.querySelectorAll('b') - const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(currentPriceTag[0]).getByText(/£40\.16/i)).toBeDefined() - expect(within(strikethroughPriceTag[0]).getByText(/£44\.16/i)).toBeDefined() - expect(currentPriceTag).toHaveLength(1) - expect(strikethroughPriceTag).toHaveLength(1) + expect(queryByText(/from/i)).not.toBeInTheDocument() + expect(queryByText(/£40\.16/i)).not.toBeInTheDocument() + expect(queryByText(/£44\.16/i)).not.toBeInTheDocument() }) test('renders strike through price with standard product', () => { const {getByText, container} = renderWithProviders( - + ) expect(getByText(/Laptop Briefcase with wheels \(37L\)/i)).toBeInTheDocument() expect(getByText(/£63\.99/i)).toBeInTheDocument() - const currentPriceTag = container.querySelectorAll('b') + const salePriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(currentPriceTag[0]).getByText(/£63\.99/i)).toBeDefined() + expect(within(salePriceTag[0]).getByText(/£63\.99/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£67\.99/i)).toBeDefined() - expect(currentPriceTag).toHaveLength(1) + expect(salePriceTag).toHaveLength(1) expect(strikethroughPriceTag).toHaveLength(1) }) - -const mockProductSearchItem = { - currency: 'USD', - image: { - alt: 'Charcoal Single Pleat Wool Suit, , large', - disBaseLink: - 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4de8166b/images/large/PG.33698RUBN4Q.CHARCWL.PZ.jpg' - }, - price: 299.99, - productName: 'Charcoal Single Pleat Wool Suit' -} -const mockProductSearchItemSet = { - currency: 'GBP', - hitType: 'set', - image: { - alt: 'Winter Look, , large', - disBaseLink: - 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwe1c4cd52/images/large/PG.10205921.JJ5FUXX.PZ.jpg', - link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dwe1c4cd52/images/large/PG.10205921.JJ5FUXX.PZ.jpg', - title: 'Winter Look, ' - }, - orderable: true, - price: 40.16, - priceMax: 71.03, - pricePerUnit: 44.16, - pricePerUnitMax: 71.03, - priceRanges: [ - { - maxPrice: 101.76, - minPrice: 44.16, - pricebook: 'gbp-m-list-prices' - }, - { - maxPrice: 71.03, - minPrice: 40.16, - pricebook: 'gbp-m-sale-prices' - } - ], - productId: 'winter-lookM', - productName: 'Winter Look', - productType: { - set: true - }, - representedProduct: { - id: '740357357531M', - c_color: 'BLACKLE', - c_refinementColor: 'black', - c_size: '065', - c_width: 'M' - } -} -const mockStandardProduct = { - currency: 'GBP', - hitType: 'product', - image: { - alt: 'Laptop Briefcase with wheels (37L), , large', - disBaseLink: - 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw7cb2d401/images/large/P0048_001.jpg', - link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dw7cb2d401/images/large/P0048_001.jpg', - title: 'Laptop Briefcase with wheels (37L), ' - }, - orderable: true, - price: 63.99, - pricePerUnit: 63.99, - productId: 'P0048M', - productName: 'Laptop Briefcase with wheels (37L)', - productType: { - item: true - }, - representedProduct: { - id: 'P0048M', - c_styleNumber: 'P0048', - c_tabDescription: - 'Perfect for business travel, this briefcase is ultra practical with plenty of space for your laptop and all its extras, as well as storage for documents, paperwork and all your essential items. The wheeled system allows you to travel comfortably with your work and when you reach your destination, you can remove the laptop compartment and carry over your shoulder to meetings. It’s the business.', - c_tabDetails: - '1682 ballistic nylon and genuine leather inserts| Spacious main storage compartment for documents and binders|Removable, padded laptop sleeve with D-rings for carrying with shoulder strap|Change handle system and cantilever wheels|Zip pull in gunmetal with black rubber insert Leather “comfort” insert detailed handle|Internal storage pockets for CD-Rom and peripherals|Real leather inserts' - }, - representedProducts: [ - { - id: 'P0048M' - } - ], - tieredPrices: [ - { - price: 67.99, - pricebook: 'gbp-m-list-prices', - quantity: 1 - } - ] -} -const mockProductWithVariants = { - currency: 'GBP', - hitType: 'master', - image: { - alt: 'Black Flat Front Wool Suit, , large', - disBaseLink: - 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw3d8972fe/images/large/PG.52001DAN84Q.BLACKWL.PZ.jpg', - link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dw3d8972fe/images/large/PG.52001DAN84Q.BLACKWL.PZ.jpg', - title: 'Black Flat Front Wool Suit, ' - }, - orderable: true, - price: 191.99, - pricePerUnit: 191.99, - priceRanges: [ - { - maxPrice: 320, - minPrice: 320, - pricebook: 'gbp-m-list-prices' - }, - { - maxPrice: 191.99, - minPrice: 191.99, - pricebook: 'gbp-m-sale-prices' - } - ], - productId: '25686544M', - productName: 'Black Flat Front Wool Suit', - productType: { - master: true - }, - representedProduct: { - id: '750518703077M', - c_color: 'BLACKWL', - c_refinementColor: 'black', - c_size: '048', - c_width: 'V' - }, - representedProducts: [ - { - id: '750518703077M' - }, - { - id: '750518703060M' - }, - { - id: '750518703039M' - }, - { - id: '750518703046M' - } - ], - variants: [ - { - orderable: true, - price: 191.99, - productId: '750518703077M', - tieredPrices: [ - { - price: 320, - pricebook: 'gbp-m-list-prices', - quantity: 1 - }, - { - price: 191.99, - pricebook: 'gbp-m-sale-prices', - quantity: 1 - } - ], - variationValues: { - color: 'BLACKWL', - size: '048', - width: 'V' - } - }, - { - orderable: true, - price: 191.99, - productId: '750518703060M', - tieredPrices: [ - { - price: 320, - pricebook: 'gbp-m-list-prices', - quantity: 1 - }, - { - price: 191.99, - pricebook: 'gbp-m-sale-prices', - quantity: 1 - } - ], - variationValues: { - color: 'BLACKWL', - size: '046', - width: 'V' - } - }, - { - orderable: true, - price: 191.99, - productId: '750518703039M', - tieredPrices: [ - { - price: 320, - pricebook: 'gbp-m-list-prices', - quantity: 1 - }, - { - price: 191.99, - pricebook: 'gbp-m-sale-prices', - quantity: 1 - } - ], - variationValues: { - color: 'BLACKWL', - size: '042', - width: 'V' - } - }, - { - orderable: true, - price: 191.99, - productId: '750518703046M', - tieredPrices: [ - { - price: 320, - pricebook: 'gbp-m-list-prices', - quantity: 1 - }, - { - price: 191.99, - pricebook: 'gbp-m-sale-prices', - quantity: 1 - } - ], - variationValues: { - color: 'BLACKWL', - size: '043', - width: 'V' - } - } - ], - variationAttributes: [ - { - id: 'color', - name: 'Colour', - values: [ - { - name: 'Black', - orderable: true, - value: 'BLACKWL' - } - ] - }, - { - id: 'size', - name: 'Size', - values: [ - { - name: '42', - orderable: true, - value: '042' - }, - { - name: '43', - orderable: true, - value: '043' - }, - { - name: '46', - orderable: true, - value: '046' - }, - { - name: '48', - orderable: true, - value: '048' - } - ] - }, - { - id: 'width', - name: 'Width', - values: [ - { - name: 'Regular', - orderable: true, - value: 'V' - } - ] - } - ] -} diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index 74543b4846..bb27225950 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -37,11 +37,9 @@ import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants' import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price' import Swatch from '@salesforce/retail-react-app/app/components/swatch-group/swatch' import SwatchGroup from '@salesforce/retail-react-app/app/components/swatch-group' -import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' +import {getPriceData} from '@salesforce/retail-react-app/app/utils/product-utils' -const ProductViewHeader = ({name, currentPrice, listPrice, currency, category, productType}) => { - const isProductASet = productType?.set - const intl = useIntl() +const ProductViewHeader = ({name, currency, priceData, category}) => { return ( {category && ( @@ -55,20 +53,8 @@ const ProductViewHeader = ({name, currentPrice, listPrice, currency, category, p {`${name}`} - - currentPrice ? listPrice : null} - currentPrice={currentPrice} - currency={currency} - prefixLabel={ - isProductASet - ? intl.formatMessage({ - id: 'product_view.label.from', - defaultMessage: 'From' - }) - : null - } - /> + + ) @@ -76,11 +62,8 @@ const ProductViewHeader = ({name, currentPrice, listPrice, currency, category, p ProductViewHeader.propTypes = { name: PropTypes.string, - currentPrice: PropTypes.number, - listPrice: PropTypes.number, currency: PropTypes.string, - category: PropTypes.array, - productType: PropTypes.object + category: PropTypes.array } const ButtonWithRegistration = withRegistration(Button) @@ -135,8 +118,8 @@ const ProductView = forwardRef( stockLevel, stepQuantity } = useDerivedProduct(product, isProductPartOfSet) - const {listPrice, currentPrice} = useMemo(() => { - return getDisplayPrice(product, {quantity}) + const priceData = useMemo(() => { + return getPriceData(product, {quantity}) }, [product, quantity]) const canAddToWishlist = !isProductLoading const isProductASet = product?.type.set @@ -303,9 +286,7 @@ const ProductView = forwardRef( @@ -344,9 +325,7 @@ const ProductView = forwardRef( diff --git a/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js b/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js index ecbdf2618d..c89599be54 100644 --- a/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js +++ b/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js @@ -30,7 +30,7 @@ import RecommendedProducts from '@salesforce/retail-react-app/app/components/rec import {LockIcon} from '@salesforce/retail-react-app/app/components/icons' import {findImageGroupBy} from '@salesforce/retail-react-app/app/utils/image-groups-utils' import { - getDisplayPrice, + getPriceData, getDisplayVariationValues } from '@salesforce/retail-react-app/app/utils/product-utils' import {EINSTEIN_RECOMMENDERS} from '@salesforce/retail-react-app/app/constants' @@ -114,10 +114,7 @@ export const AddToCartModal = () => { viewType: 'small', selectedVariationAttributes: variant.variationValues })?.images?.[0] - const { - currentPrice: lineItemCurrentPrice, - listPrice: lineItemListPrice - } = getDisplayPrice(product) + const priceData = getPriceData(product) const variationAttributeValues = getDisplayVariationValues( product.variationAttributes, variant.variationValues @@ -170,13 +167,7 @@ export const AddToCartModal = () => { lineItemCurrentPrice - ? lineItemListPrice * quantity - : null - } - currentPrice={lineItemCurrentPrice * quantity} + priceData={priceData} currency={currency} /> diff --git a/packages/template-retail-react-app/app/hooks/use-derived-product.js b/packages/template-retail-react-app/app/hooks/use-derived-product.js index 647cd8e17d..205f824689 100644 --- a/packages/template-retail-react-app/app/hooks/use-derived-product.js +++ b/packages/template-retail-react-app/app/hooks/use-derived-product.js @@ -10,7 +10,6 @@ import {useVariant} from '@salesforce/retail-react-app/app/hooks/use-variant' import {useIntl} from 'react-intl' import {useVariationParams} from '@salesforce/retail-react-app/app/hooks/use-variation-params' import {useVariationAttributes} from '@salesforce/retail-react-app/app/hooks/use-variation-attributes' -import {getDisplayPrice} from '@salesforce/retail-react-app/app/utils/product-utils' const OUT_OF_STOCK = 'OUT_OF_STOCK' const UNFULFILLABLE = 'UNFULFILLABLE' @@ -55,8 +54,6 @@ export const useDerivedProduct = (product, isProductPartOfSet = false) => { (isOutOfStock && inventoryMessages[OUT_OF_STOCK]) || (unfulfillable && inventoryMessages[UNFULFILLABLE]) - const {basePrice, discountPrice} = getDisplayPrice(product) - // If the `initialQuantity` changes, update the state. This typically happens // when either the master product changes, or the inventory of the product changes // from out-of-stock to in-stock or vice versa. @@ -75,8 +72,6 @@ export const useDerivedProduct = (product, isProductPartOfSet = false) => { variationParams, setQuantity, variant, - stockLevel, - basePrice, - discountPrice + stockLevel } } diff --git a/packages/template-retail-react-app/app/mocks/product-search-hit-data.js b/packages/template-retail-react-app/app/mocks/product-search-hit-data.js new file mode 100644 index 0000000000..d31bb632d2 --- /dev/null +++ b/packages/template-retail-react-app/app/mocks/product-search-hit-data.js @@ -0,0 +1,217 @@ +const mockProductSearchItem = { + currency: 'USD', + image: { + alt: 'Charcoal Single Pleat Wool Suit, , large', + disBaseLink: + 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw4de8166b/images/large/PG.33698RUBN4Q.CHARCWL.PZ.jpg' + }, + price: 299.99, + productName: 'Charcoal Single Pleat Wool Suit' +} +const mockProductSetHit = { + currency: 'GBP', + hitType: 'set', + image: { + alt: 'Winter Look, , large', + disBaseLink: + 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dwe1c4cd52/images/large/PG.10205921.JJ5FUXX.PZ.jpg', + link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dwe1c4cd52/images/large/PG.10205921.JJ5FUXX.PZ.jpg', + title: 'Winter Look, ' + }, + orderable: true, + price: 40.16, + priceMax: 71.03, + pricePerUnit: 44.16, + pricePerUnitMax: 71.03, + priceRanges: [ + { + maxPrice: 101.76, + minPrice: 44.16, + pricebook: 'gbp-m-list-prices' + }, + { + maxPrice: 71.03, + minPrice: 40.16, + pricebook: 'gbp-m-sale-prices' + } + ], + productId: 'winter-lookM', + productName: 'Winter Look', + productType: { + set: true + }, + representedProduct: { + id: '740357357531M', + c_color: 'BLACKLE', + c_refinementColor: 'black', + c_size: '065', + c_width: 'M' + } +} +const mockStandardProductHit = { + currency: 'GBP', + hitType: 'product', + image: { + alt: 'Laptop Briefcase with wheels (37L), , large', + disBaseLink: + 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw7cb2d401/images/large/P0048_001.jpg', + link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dw7cb2d401/images/large/P0048_001.jpg', + title: 'Laptop Briefcase with wheels (37L), ' + }, + orderable: true, + price: 63.99, + pricePerUnit: 63.99, + productId: 'P0048M', + productName: 'Laptop Briefcase with wheels (37L)', + productType: { + item: true + }, + representedProduct: { + id: 'P0048M', + c_styleNumber: 'P0048', + c_tabDescription: + 'Perfect for business travel, this briefcase is ultra practical with plenty of space for your laptop and all its extras, as well as storage for documents, paperwork and all your essential items. The wheeled system allows you to travel comfortably with your work and when you reach your destination, you can remove the laptop compartment and carry over your shoulder to meetings. It’s the business.', + c_tabDetails: + '1682 ballistic nylon and genuine leather inserts| Spacious main storage compartment for documents and binders|Removable, padded laptop sleeve with D-rings for carrying with shoulder strap|Change handle system and cantilever wheels|Zip pull in gunmetal with black rubber insert Leather “comfort” insert detailed handle|Internal storage pockets for CD-Rom and peripherals|Real leather inserts' + }, + representedProducts: [ + { + id: 'P0048M' + } + ], + tieredPrices: [ + { + price: 67.99, + pricebook: 'gbp-m-list-prices', + quantity: 1 + } + ] +} +const mockVariantProductHit = { + currency: 'GBP', + hitType: 'master', + image: { + alt: 'Black Flat Front Wool Suit, , large', + disBaseLink: + 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw3d8972fe/images/large/PG.52001DAN84Q.BLACKWL.PZ.jpg', + link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dw3d8972fe/images/large/PG.52001DAN84Q.BLACKWL.PZ.jpg', + title: 'Black Flat Front Wool Suit, ' + }, + orderable: true, + price: 191.99, + pricePerUnit: 191.99, + priceRanges: [ + { + maxPrice: 320, + minPrice: 320, + pricebook: 'gbp-m-list-prices' + }, + { + maxPrice: 191.99, + minPrice: 191.99, + pricebook: 'gbp-m-sale-prices' + } + ], + productId: '25686544M', + productName: 'Black Flat Front Wool Suit', + productType: { + master: true + }, + representedProduct: { + id: '750518703077M', + c_color: 'BLACKWL', + c_refinementColor: 'black', + c_size: '048', + c_width: 'V' + }, + representedProducts: [ + { + id: '750518703077M' + }, + { + id: '750518703060M' + }, + { + id: '750518703039M' + }, + { + id: '750518703046M' + } + ], + variants: [ + { + orderable: true, + price: 191.99, + productId: '750518703077M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKWL', + size: '048', + width: 'V' + } + } + ], + variationAttributes: [ + { + id: 'color', + name: 'Colour', + values: [ + { + name: 'Black', + orderable: true, + value: 'BLACKWL' + } + ] + }, + { + id: 'size', + name: 'Size', + values: [ + { + name: '42', + orderable: true, + value: '042' + }, + { + name: '43', + orderable: true, + value: '043' + }, + { + name: '46', + orderable: true, + value: '046' + }, + { + name: '48', + orderable: true, + value: '048' + } + ] + }, + { + id: 'width', + name: 'Width', + values: [ + { + name: 'Regular', + orderable: true, + value: 'V' + } + ] + } + ] +} + +export {mockProductSetHit, mockStandardProductHit, mockVariantProductHit, mockProductSearchItem} diff --git a/packages/template-retail-react-app/app/pages/product-list/index.jsx b/packages/template-retail-react-app/app/pages/product-list/index.jsx index 5d0600fe56..a7ee0a2ac2 100644 --- a/packages/template-retail-react-app/app/pages/product-list/index.jsx +++ b/packages/template-retail-react-app/app/pages/product-list/index.jsx @@ -151,7 +151,7 @@ const ProductList = (props) => { ...restOfParams, perPricebook: true, allVariationProperties: true, - expand: ['promotions', 'variations', 'prices', 'images'], + expand: ['promotions', 'variations', 'prices', 'images', 'availability'], refine: _refine } }, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 51cda7e623..711703a911 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -2481,16 +2481,6 @@ "value": " to wishlist" } ], - "product_tile.assistive_msg.original_price": [ - { - "type": 0, - "value": "strikethrough price " - }, - { - "type": 1, - "value": "strikethroughPrice" - } - ], "product_tile.assistive_msg.remove_from_wishlist": [ { "type": 0, @@ -2505,14 +2495,236 @@ "value": " from wishlist" } ], - "product_tile.assistive_msg.sale_price": [ + "product_tile.price_display": [ + { + "options": { + "false": { + "value": [ + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": "From" + } + ] + } + }, + "type": 5, + "value": "isRange" + }, + { + "type": 0, + "value": " " + } + ] + } + }, + "type": 5, + "value": "isMaster" + }, { "type": 0, - "value": "current price " + "value": " " }, { - "type": 1, - "value": "currentPrice" + "options": { + "false": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "true": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "span" + } + ] + } + }, + "type": 5, + "value": "hasRepresentedProduct" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + }, + { + "type": 0, + "value": " " + }, + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "listPrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "s" + }, + { + "type": 0, + "value": " " + } + ] + } + }, + "type": 5, + "value": "isOnSale" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + ] + } + }, + "type": 5, + "value": "isASet" } ], "product_view.button.add_set_to_cart": [ @@ -2557,12 +2769,6 @@ "value": "Increment Quantity" } ], - "product_view.label.from": [ - { - "type": 0, - "value": "From" - } - ], "product_view.label.quantity": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 51cda7e623..711703a911 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -2481,16 +2481,6 @@ "value": " to wishlist" } ], - "product_tile.assistive_msg.original_price": [ - { - "type": 0, - "value": "strikethrough price " - }, - { - "type": 1, - "value": "strikethroughPrice" - } - ], "product_tile.assistive_msg.remove_from_wishlist": [ { "type": 0, @@ -2505,14 +2495,236 @@ "value": " from wishlist" } ], - "product_tile.assistive_msg.sale_price": [ + "product_tile.price_display": [ + { + "options": { + "false": { + "value": [ + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": "From" + } + ] + } + }, + "type": 5, + "value": "isRange" + }, + { + "type": 0, + "value": " " + } + ] + } + }, + "type": 5, + "value": "isMaster" + }, { "type": 0, - "value": "current price " + "value": " " }, { - "type": 1, - "value": "currentPrice" + "options": { + "false": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "true": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "span" + } + ] + } + }, + "type": 5, + "value": "hasRepresentedProduct" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + }, + { + "type": 0, + "value": " " + }, + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "listPrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "s" + }, + { + "type": 0, + "value": " " + } + ] + } + }, + "type": 5, + "value": "isOnSale" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + ] + } + }, + "type": 5, + "value": "isASet" } ], "product_view.button.add_set_to_cart": [ @@ -2557,12 +2769,6 @@ "value": "Increment Quantity" } ], - "product_view.label.from": [ - { - "type": 0, - "value": "From" - } - ], "product_view.label.quantity": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index 27804c22af..02bc968896 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -5281,24 +5281,6 @@ "value": "]" } ], - "product_tile.assistive_msg.original_price": [ - { - "type": 0, - "value": "[" - }, - { - "type": 0, - "value": "şŧřīķḗḗŧħřǿǿŭŭɠħ ƥřīƈḗḗ " - }, - { - "type": 1, - "value": "strikethroughPrice" - }, - { - "type": 0, - "value": "]" - } - ], "product_tile.assistive_msg.remove_from_wishlist": [ { "type": 0, @@ -5321,18 +5303,240 @@ "value": "]" } ], - "product_tile.assistive_msg.sale_price": [ + "product_tile.price_display": [ { "type": 0, "value": "[" }, + { + "options": { + "false": { + "value": [ + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": "Ƒřǿǿḿ" + } + ] + } + }, + "type": 5, + "value": "isRange" + }, + { + "type": 0, + "value": " " + } + ] + } + }, + "type": 5, + "value": "isMaster" + }, { "type": 0, - "value": "ƈŭŭřřḗḗƞŧ ƥřīƈḗḗ " + "value": " " }, { - "type": 1, - "value": "currentPrice" + "options": { + "false": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "true": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "span" + } + ] + } + }, + "type": 5, + "value": "hasRepresentedProduct" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + } + ] + }, + "true": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "b" + }, + { + "type": 0, + "value": " " + }, + { + "children": [ + { + "children": [ + { + "type": 1, + "value": "listPrice" + } + ], + "type": 8, + "value": "price" + } + ], + "type": 8, + "value": "s" + }, + { + "type": 0, + "value": " " + } + ] + } + }, + "type": 5, + "value": "isOnSale" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + ] + } + }, + "type": 5, + "value": "isASet" }, { "type": 0, @@ -5437,20 +5641,6 @@ "value": "]" } ], - "product_view.label.from": [ - { - "type": 0, - "value": "[" - }, - { - "type": 0, - "value": "Ƒřǿǿḿ" - }, - { - "type": 0, - "value": "]" - } - ], "product_view.label.quantity": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 1c0b0ce036..e6830d44b8 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -5,6 +5,8 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import {getSmallestValByProperty} from '@salesforce/retail-react-app/app/utils/utils' + /** * Get the human-friendly version of the variation values that users have selected. * Useful for displaying these values in the UI. @@ -35,23 +37,69 @@ export const getDisplayVariationValues = (variationAttributes, values = {}) => { } /** - * This function extract the list price and current price of a product - * If a product has promotional price, it will prioritize that value for current price - * List price will take the highest value among the price book prices + * This function extract the price information of a given product + * If a product is a master, + * salePrice: get the lowest price (including promotional prices) among variants + * listPrice: get the list price of the variant that has lowest price (including promotional price) + * maxPrice: the max price in tieredPrices of variant that has lowest price * @param {object} product - product detail object - * @param {object} opts - product detail object - * @returns {{listPrice: number, currentPrice: number}} + * @param {object} opts - options to pass into the function like intl, quantity, and currency */ -export const getDisplayPrice = (product, opts = {}) => { +export const getPriceData = (product, opts = {}) => { const {quantity = 1} = opts - const promotionalPriceList = product?.productPromotions - ?.map((promo) => promo.promotionalPrice) - .filter((i) => i !== null && i !== undefined) - const promotionalPrice = promotionalPriceList?.length ? Math.min(...promotionalPriceList) : null + const isASet = product?.hitType === 'set' || !!product?.type?.set + const isMaster = product?.hitType === 'master' || !!product?.type?.master + const hasRepresentedProduct = !!product?.representedProduct?.id + let salePrice + let variantWithLowestPrice + // grab the variant that has the lowest price (including promotional price) + if (isMaster) { + const variants = product?.variants || [] + variantWithLowestPrice = variants.reduce( + (minVariant, variant) => { + const promotions = variant.productPromotions || [] + const smallestPromotionalPrice = getSmallestValByProperty( + promotions, + 'promotionalPrice' + ) + const variantSalePrice = + smallestPromotionalPrice && smallestPromotionalPrice < variant.price + ? smallestPromotionalPrice + : variant.price + return variantSalePrice < minVariant.minPrice + ? {minPrice: variantSalePrice, variant} + : minVariant + }, + {minPrice: Infinity, variant: null} + ) + } + + if (isMaster) { + salePrice = variantWithLowestPrice?.minPrice + } else { + const promotionalPrice = getSmallestValByProperty( + product?.productPromotions, + 'promotionalPrice' + ) + salePrice = + promotionalPrice && promotionalPrice < product?.price + ? promotionalPrice + : product?.price + } + // since the price is the lowest value among price books, each product will have at lease a single item tiered price at quantity 1 + // the highest value of tieredPrices is presumptively the list price + const tieredPrices = variantWithLowestPrice?.variant.tieredPrices || product?.tieredPrices || [] + const maxTieredPrice = tieredPrices.length + ? Math.max(...tieredPrices.map((item) => item.price)) + : undefined + const highestTieredPrice = tieredPrices.find((tier) => tier.price === maxTieredPrice) + const listPrice = highestTieredPrice?.price // if a product has tieredPrices, get the tiered that has the higher closest quantity to current quantity const filteredTiered = - product?.tieredPrices?.filter((tiered) => tiered.quantity <= quantity) || [] + (product?.tieredPrices || variantWithLowestPrice?.variants?.tieredPrices)?.filter( + (tiered) => tiered.quantity <= quantity + ) || [] const closestTieredPrice = filteredTiered.length && filteredTiered.reduce((prev, curr) => { @@ -59,28 +107,19 @@ export const getDisplayPrice = (product, opts = {}) => { ? curr : prev }) - const salePrices = [closestTieredPrice?.price, promotionalPrice, product?.price].filter(Boolean) - // pick the smallest price among these price for the "current" price - let currentPrice = salePrices?.length ? Math.min(...salePrices) : 0 - - // Master product and product set have priceRanges object - // the price returned from API is the smallest price among price book - // to figure out what is the original price, we find the lastest minPrice among the books - const maxPriceRange = product?.priceRanges - ? Math.max(...(product?.priceRanges || []).map((item) => item.minPrice)) - : 0 - const highestPriceRange = product?.priceRanges?.find( - (range) => range.minPrice === maxPriceRange - ) - // for standard/variant/bundle product, they dont have priceRanges, only tieredPrices - // since the price is the lowest value returned from the API, each product will have at lease a single item tiered price - // the highest value of tieredPrices is presumptively the list price - const maxTieredPrice = product?.tieredPrices - ? Math.max(...(product?.tieredPrices || []).map((item) => item.price)) - : 0 - const highestTieredPrice = product?.tieredPrices?.find((tier) => tier.price === maxTieredPrice) return { - listPrice: highestTieredPrice?.price || highestPriceRange?.minPrice, - currentPrice + salePrice, + listPrice, + isOnSale: salePrice < listPrice, + isASet, + isMaster, + // For a product, set price is the lowest price of its children, so the price should be considered a range + // For a master product, when it has more than 2 variants, we use the lowest priced variant, so it is considered a range price + // but for master that has one variant, it is not considered range + isRange: (isMaster && product?.variants.length > 1) || isASet || false, + hasRepresentedProduct, + // priceMax is for product set + tieredPrice: closestTieredPrice?.price, + maxPrice: product?.priceMax || maxTieredPrice } } diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index 8419941e70..33c02c2371 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -6,7 +6,7 @@ */ import { - getDisplayPrice, + getPriceData, getDisplayVariationValues } from '@salesforce/retail-react-app/app/utils/product-utils' @@ -55,8 +55,8 @@ describe('getDisplayVariationValues', function () { }) }) -describe('getDisplayPrice', function () { - test('returns listPrice and currentPrice for product that has only priceRanges', () => { +describe('getPriceData', function () { + test('returns price data for master product that has more than one variant', () => { const data = { name: 'product name', price: 30.76, @@ -73,13 +73,13 @@ describe('getDisplayPrice', function () { } ] } - const {listPrice, currentPrice} = getDisplayPrice(data) + const {listPrice, currentPrice} = getPriceData(data) expect(listPrice).toBe(35.76) expect(currentPrice).toBe(30.76) }) - test('returns listPrice and currentPrice for product that has only tieredPrices', () => { + test('returns price data for master product that has ONLY one variant', () => { const data = { name: 'product name', price: 25.6, @@ -96,7 +96,7 @@ describe('getDisplayPrice', function () { } ] } - const {listPrice, currentPrice} = getDisplayPrice(data) + const {listPrice, currentPrice} = getPriceData(data) expect(listPrice).toBe(30.6) expect(currentPrice).toBe(25.6) @@ -124,7 +124,7 @@ describe('getDisplayPrice', function () { } ] } - const {listPrice, currentPrice} = getDisplayPrice(data, {quantity: 11}) + const {listPrice, currentPrice} = getPriceData(data, {quantity: 11}) expect(listPrice).toBe(30.6) expect(currentPrice).toBe(15) @@ -152,7 +152,7 @@ describe('getDisplayPrice', function () { } ] } - const {listPrice, currentPrice} = getDisplayPrice(data, {quantity: 9}) + const {listPrice, currentPrice} = getPriceData(data, {quantity: 9}) expect(listPrice).toBe(30.6) expect(currentPrice).toBe(25.6) @@ -186,7 +186,7 @@ describe('getDisplayPrice', function () { } ] } - const {listPrice, currentPrice} = getDisplayPrice(data, {quantity: 11}) + const {listPrice, currentPrice} = getPriceData(data, {quantity: 11}) expect(listPrice).toBe(30.6) expect(currentPrice).toBe(10.99) diff --git a/packages/template-retail-react-app/app/utils/test-utils.js b/packages/template-retail-react-app/app/utils/test-utils.js index c6105b6f37..d9e9cabfd6 100644 --- a/packages/template-retail-react-app/app/utils/test-utils.js +++ b/packages/template-retail-react-app/app/utils/test-utils.js @@ -26,6 +26,7 @@ import {createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url' import {getSiteByReference} from '@salesforce/retail-react-app/app/utils/site-utils' import jwt from 'jsonwebtoken' import userEvent from '@testing-library/user-event' +import {Text} from '@chakra-ui/react' // This JWT's payload is special // it includes 3 fields that commerce-sdk-react cares: // exp, isb and sub @@ -79,7 +80,17 @@ export const DEFAULT_SITE = 'global' export const renderWithReactIntl = (node, locale = DEFAULT_LOCALE) => { return render( - + {chunks}, + br: () =>
, + p: (chunks) =>

{chunks}

, + s: (chunks) => {chunks}, + span: (chunks) => {chunks} + }} + locale={locale} + defaultLocale={locale} + > {node}
) @@ -122,7 +133,18 @@ export const TestProviders = ({ return ( - + {chunks}, + br: () =>
, + p: (chunks) =>

{chunks}

, + s: (chunks) => {chunks}, + span: (chunks) => {chunks} + }} + locale={locale.id} + defaultLocale={DEFAULT_LOCALE} + messages={messages} + > { * @return {boolean} */ export const isHydrated = () => typeof window !== 'undefined' && !window.__HYDRATING__ + +/** + * Find the smallest value by key from a given array + * @param arr + * @param key + */ +export const getSmallestValByProperty = (arr, key) => { + if (!arr || !arr.length) return undefined + if (!key) { + throw new Error('Please specify a key.') + } + const vals = arr + .map((item) => item[key]) + .filter(Boolean) + .filter(Number) + return vals.length ? Math.min(...vals) : undefined +} diff --git a/packages/template-retail-react-app/app/utils/utils.test.js b/packages/template-retail-react-app/app/utils/utils.test.js index c6729b1e72..76840ff6d0 100644 --- a/packages/template-retail-react-app/app/utils/utils.test.js +++ b/packages/template-retail-react-app/app/utils/utils.test.js @@ -6,7 +6,11 @@ */ import * as utils from '@salesforce/retail-react-app/app/utils/utils' import EventEmitter from 'events' -import {flatten, shallowEquals} from '@salesforce/retail-react-app/app/utils/utils' +import { + flatten, + getSmallestValByProperty, + shallowEquals +} from '@salesforce/retail-react-app/app/utils/utils' afterEach(() => { jest.clearAllMocks() @@ -180,3 +184,40 @@ describe('keysToCamel', () => { }) }) }) + +describe('getSmallestValByKey', function () { + test('should return the smallest value by key', () => { + const data = [ + { + name: 'Product 1', + price: 10 + }, + { + name: 'Product 2', + price: 9 + } + ] + const val = getSmallestValByProperty(data, 'price') + expect(val).toEqual(9) + }) + test('should undefined if array is not passed in', () => { + const data = { + name: 'Cowl neck top' + } + const val = getSmallestValByProperty(data, 'price') + expect(val).toBeUndefined() + }) + test('should throw an error if key name is undefined', () => { + const data = [ + { + name: 'Product 1', + price: 10 + }, + { + name: 'Product 2', + price: 9 + } + ] + expect(() => getSmallestValByProperty(data)).toThrow() + }) +}) diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 060bc37098..953c09fdad 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -1063,14 +1063,11 @@ "product_tile.assistive_msg.add_to_wishlist": { "defaultMessage": "Add {product} to wishlist" }, - "product_tile.assistive_msg.original_price": { - "defaultMessage": "strikethrough price {strikethroughPrice}" - }, "product_tile.assistive_msg.remove_from_wishlist": { "defaultMessage": "Remove {product} from wishlist" }, - "product_tile.assistive_msg.sale_price": { - "defaultMessage": "current price {currentPrice}" + "product_tile.price_display": { + "defaultMessage": "{isMaster, select, true { { isRange, select, true {From} false {} other {} } } false {} other {} } {isASet, select, true {} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" @@ -1093,9 +1090,6 @@ "product_view.label.assistive_msg.quantity_increment": { "defaultMessage": "Increment Quantity" }, - "product_view.label.from": { - "defaultMessage": "From" - }, "product_view.label.quantity": { "defaultMessage": "Quantity" }, diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 060bc37098..953c09fdad 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -1063,14 +1063,11 @@ "product_tile.assistive_msg.add_to_wishlist": { "defaultMessage": "Add {product} to wishlist" }, - "product_tile.assistive_msg.original_price": { - "defaultMessage": "strikethrough price {strikethroughPrice}" - }, "product_tile.assistive_msg.remove_from_wishlist": { "defaultMessage": "Remove {product} from wishlist" }, - "product_tile.assistive_msg.sale_price": { - "defaultMessage": "current price {currentPrice}" + "product_tile.price_display": { + "defaultMessage": "{isMaster, select, true { { isRange, select, true {From} false {} other {} } } false {} other {} } {isASet, select, true {} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" @@ -1093,9 +1090,6 @@ "product_view.label.assistive_msg.quantity_increment": { "defaultMessage": "Increment Quantity" }, - "product_view.label.from": { - "defaultMessage": "From" - }, "product_view.label.quantity": { "defaultMessage": "Quantity" }, From cf3197691c14d230754e6b0d192f4a77a228b17c Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 9 May 2024 13:33:35 -0700 Subject: [PATCH 24/45] remove console --- .../app/components/display-price/index.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index 5625304d45..336591a695 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -39,8 +39,7 @@ describe('DisplayPrice', function () { renderWithProviders( ) - screen.logTestingPlaygroundURL() - expect(screen.queryByText(/£90\.00/i)).not.toBeInTheDocument() + expect(screen.queryByText(/£90\.`00/i)).not.toBeInTheDocument() expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() }) }) From 2afea83b4c0bd96e69b82dd9be8955f5968d8f12 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 9 May 2024 13:57:46 -0700 Subject: [PATCH 25/45] guard the values --- .../app/utils/product-utils.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index e6830d44b8..11301b3b78 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -88,18 +88,16 @@ export const getPriceData = (product, opts = {}) => { } // since the price is the lowest value among price books, each product will have at lease a single item tiered price at quantity 1 // the highest value of tieredPrices is presumptively the list price - const tieredPrices = variantWithLowestPrice?.variant.tieredPrices || product?.tieredPrices || [] - const maxTieredPrice = tieredPrices.length + const tieredPrices = + variantWithLowestPrice?.variant?.tieredPrices || product?.tieredPrices || [] + const maxTieredPrice = tieredPrices?.length ? Math.max(...tieredPrices.map((item) => item.price)) : undefined const highestTieredPrice = tieredPrices.find((tier) => tier.price === maxTieredPrice) const listPrice = highestTieredPrice?.price // if a product has tieredPrices, get the tiered that has the higher closest quantity to current quantity - const filteredTiered = - (product?.tieredPrices || variantWithLowestPrice?.variants?.tieredPrices)?.filter( - (tiered) => tiered.quantity <= quantity - ) || [] + const filteredTiered = tieredPrices.filter((tiered) => tiered.quantity <= quantity) const closestTieredPrice = filteredTiered.length && filteredTiered.reduce((prev, curr) => { @@ -116,7 +114,7 @@ export const getPriceData = (product, opts = {}) => { // For a product, set price is the lowest price of its children, so the price should be considered a range // For a master product, when it has more than 2 variants, we use the lowest priced variant, so it is considered a range price // but for master that has one variant, it is not considered range - isRange: (isMaster && product?.variants.length > 1) || isASet || false, + isRange: (isMaster && product?.variants?.length > 1) || isASet || false, hasRepresentedProduct, // priceMax is for product set tieredPrice: closestTieredPrice?.price, From 165c2dbf122b26bc5000d9087739bdbbe9ac0591 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Thu, 9 May 2024 16:42:50 -0700 Subject: [PATCH 26/45] fix tests --- .../app/components/product-tile/index.test.js | 24 +- .../app/mocks/product-search-hit-data.js | 533 +++++++++++++++++- .../app/utils/product-utils.test.js | 182 ++---- 3 files changed, 606 insertions(+), 133 deletions(-) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.test.js b/packages/template-retail-react-app/app/components/product-tile/index.test.js index ba6f3bdbb3..0b4a1df8b4 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.test.js +++ b/packages/template-retail-react-app/app/components/product-tile/index.test.js @@ -7,12 +7,12 @@ import React from 'react' import ProductTile, {Skeleton} from '@salesforce/retail-react-app/app/components/product-tile/index' import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' -import {fireEvent, screen, within} from '@testing-library/react' +import {fireEvent, within} from '@testing-library/react' import { + mockMasterProductHitWithOneVariant, mockProductSearchItem, mockProductSetHit, - mockStandardProductHit, - mockVariantProductHit + mockStandardProductHit } from '@salesforce/retail-react-app/app/mocks/product-search-hit-data' test('Renders links and images', () => { @@ -50,7 +50,7 @@ test('Remove from wishlist cannot be muti-clicked', () => { expect(onClick).toHaveBeenCalledTimes(1) }) -test('renders strike through price with variants product', () => { +test('renders exact price with strikethrough price for master product can be filtered down to one variant ', () => { const {getByText, container} = renderWithProviders( ) @@ -66,6 +66,22 @@ test('renders strike through price with variants product', () => { expect(strikethroughPriceTag).toHaveLength(1) }) +test('renders exact price with strikethrough price for master product can be filtered down to one variant ', () => { + const {getByText, container} = renderWithProviders( + + ) + expect(getByText(/black flat front wool suit/i)).toBeInTheDocument() + expect(getByText(/£191\.99/i)).toBeInTheDocument() + expect(getByText(/£320\.00/i)).toBeInTheDocument() + + const salePriceTag = container.querySelectorAll('b') + const strikethroughPriceTag = container.querySelectorAll('s') + expect(within(salePriceTag[0]).getByText(/£191\.99/i)).toBeDefined() + expect(within(strikethroughPriceTag[0]).getByText(/£320\.00/i)).toBeDefined() + expect(salePriceTag).toHaveLength(1) + expect(strikethroughPriceTag).toHaveLength(1) +}) + test('Product set - does not render strike through price', () => { const {getByText, queryByText, container} = renderWithProviders( diff --git a/packages/template-retail-react-app/app/mocks/product-search-hit-data.js b/packages/template-retail-react-app/app/mocks/product-search-hit-data.js index d31bb632d2..6984cb7437 100644 --- a/packages/template-retail-react-app/app/mocks/product-search-hit-data.js +++ b/packages/template-retail-react-app/app/mocks/product-search-hit-data.js @@ -87,7 +87,7 @@ const mockStandardProductHit = { } ] } -const mockVariantProductHit = { +const mockMasterProductHitWithOneVariant = { currency: 'GBP', hitType: 'master', image: { @@ -213,5 +213,532 @@ const mockVariantProductHit = { } ] } - -export {mockProductSetHit, mockStandardProductHit, mockVariantProductHit, mockProductSearchItem} +const mockMasterProductHitWithMultipleVariants = { + currency: 'GBP', + hitType: 'master', + image: { + alt: 'Black Single Pleat Athletic Fit Wool Suit - Edit, , large', + disBaseLink: + 'https://edge.disstg.commercecloud.salesforce.com/dw/image/v2/ZZRF_001/on/demandware.static/-/Sites-apparel-m-catalog/default/dw5d64302b/images/large/PG.52001RUBN4Q.BLACKFB.PZ.jpg', + link: 'https://zzrf-001.dx.commercecloud.salesforce.com/on/demandware.static/-/Sites-apparel-m-catalog/default/dw5d64302b/images/large/PG.52001RUBN4Q.BLACKFB.PZ.jpg', + title: 'Black Single Pleat Athletic Fit Wool Suit - Edit, ' + }, + orderable: true, + price: 191.99, + pricePerUnit: 191.99, + priceRanges: [ + { + maxPrice: 320, + minPrice: 191.99, + pricebook: 'gbp-m-list-prices' + }, + { + maxPrice: 191.99, + minPrice: 191.99, + pricebook: 'gbp-m-sale-prices' + } + ], + productId: '25604524M', + productName: 'Black Single Pleat Athletic Fit Wool Suit - Edit', + productType: { + master: true + }, + representedProduct: { + id: '750518699660M' + }, + variants: [ + { + orderable: true, + price: 191.99, + productId: '750518699660M', + tieredPrices: [ + { + price: 223.99, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '050', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699585M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '039', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699653M', + tieredPrices: [ + { + price: 191.99, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '048', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699615M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '042', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699608M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '041', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699646M', + tieredPrices: [ + { + price: 191.99, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '046', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699592M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '040', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699622M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '043', + width: 'V' + } + }, + { + orderable: false, + price: 191.99, + productId: '750518699578M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '038', + width: 'V' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699875M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '046', + width: 'L' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699868M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '044', + width: 'L' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699820M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '040', + width: 'L' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699882M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '048', + width: 'L' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699851M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '043', + width: 'L' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699844M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '042', + width: 'L' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699769M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '044', + width: 'S' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699721M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '040', + width: 'S' + } + }, + { + orderable: true, + price: 191.99, + productId: '750518699745M', + tieredPrices: [ + { + price: 320, + pricebook: 'gbp-m-list-prices', + quantity: 1 + }, + { + price: 191.99, + pricebook: 'gbp-m-sale-prices', + quantity: 1 + } + ], + variationValues: { + color: 'BLACKFB', + size: '042', + width: 'S' + } + } + ], + variationAttributes: [ + { + id: 'color', + name: 'Colour', + values: [ + { + name: 'Black', + orderable: true, + value: 'BLACKFB' + } + ] + }, + { + id: 'size', + name: 'Size', + values: [ + { + name: '38', + orderable: false, + value: '038' + }, + { + name: '39', + orderable: true, + value: '039' + }, + { + name: '40', + orderable: true, + value: '040' + }, + { + name: '41', + orderable: true, + value: '041' + }, + { + name: '42', + orderable: true, + value: '042' + }, + { + name: '43', + orderable: true, + value: '043' + }, + { + name: '44', + orderable: true, + value: '044' + }, + { + name: '46', + orderable: true, + value: '046' + }, + { + name: '48', + orderable: true, + value: '048' + }, + { + name: '50', + orderable: true, + value: '050' + } + ] + }, + { + id: 'width', + name: 'Width', + values: [ + { + name: 'Short', + orderable: true, + value: 'S' + }, + { + name: 'Regular', + orderable: true, + value: 'V' + }, + { + name: 'Long', + orderable: true, + value: 'L' + } + ] + } + ] +} +export { + mockProductSetHit, + mockStandardProductHit, + mockMasterProductHitWithOneVariant, + mockProductSearchItem, + mockMasterProductHitWithMultipleVariants +} diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index 33c02c2371..c9c515a213 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -9,6 +9,12 @@ import { getPriceData, getDisplayVariationValues } from '@salesforce/retail-react-app/app/utils/product-utils' +import { + mockMasterProductHitWithMultipleVariants, + mockMasterProductHitWithOneVariant, + mockProductSetHit, + mockStandardProductHit +} from '@salesforce/retail-react-app/app/mocks/product-search-hit-data' describe('getDisplayVariationValues', function () { const variationAttributes = [ @@ -57,138 +63,62 @@ describe('getDisplayVariationValues', function () { describe('getPriceData', function () { test('returns price data for master product that has more than one variant', () => { - const data = { - name: 'product name', - price: 30.76, - priceRanges: [ - { - maxPrice: 40.76, - minPrice: 35.76, - pricebook: 'gbp-m-list-prices' - }, - { - maxPrice: 37.76, - minPrice: 30.76, - pricebook: 'gbp-m-sale-prices' - } - ] - } - const {listPrice, currentPrice} = getPriceData(data) - - expect(listPrice).toBe(35.76) - expect(currentPrice).toBe(30.76) + const priceData = getPriceData(mockMasterProductHitWithMultipleVariants) + expect(priceData).toEqual({ + salePrice: 191.99, + listPrice: 223.99, + isOnSale: true, + isASet: false, + isMaster: true, + isRange: true, + hasRepresentedProduct: true, + tieredPrice: 223.99, + maxPrice: 223.99 + }) }) test('returns price data for master product that has ONLY one variant', () => { - const data = { - name: 'product name', - price: 25.6, - tieredPrices: [ - { - price: 30.6, - pricebook: 'gbp-m-list-prices', - quantity: 1 - }, - { - price: 25.6, - pricebook: 'gbp-m-sale-prices', - quantity: 1 - } - ] - } - const {listPrice, currentPrice} = getPriceData(data) - - expect(listPrice).toBe(30.6) - expect(currentPrice).toBe(25.6) - }) - - test('returns pick the closest tieredPrices for product currentPrice quantity', () => { - const data = { - name: 'product name', - price: 25.6, - tieredPrices: [ - { - price: 30.6, - pricebook: 'gbp-m-list-prices', - quantity: 1 - }, - { - price: 25.6, - pricebook: 'gbp-m-sale-prices', - quantity: 1 - }, - { - price: 15, - pricebook: 'gbp-m-sale-prices', - quantity: 10 - } - ] - } - const {listPrice, currentPrice} = getPriceData(data, {quantity: 11}) - - expect(listPrice).toBe(30.6) - expect(currentPrice).toBe(15) + const priceData = getPriceData(mockMasterProductHitWithOneVariant) + expect(priceData).toEqual({ + salePrice: 191.99, + listPrice: 320, + isOnSale: true, + isASet: false, + isMaster: true, + isRange: false, + hasRepresentedProduct: true, + tieredPrice: 320, + maxPrice: 320 + }) }) - test('should not pick the discounted tiered when it does not reach the tierd quantity', () => { - const data = { - name: 'product name', - price: 25.6, - tieredPrices: [ - { - price: 30.6, - pricebook: 'gbp-m-list-prices', - quantity: 1 - }, - { - price: 25.6, - pricebook: 'gbp-m-sale-prices', - quantity: 1 - }, - { - price: 15, - pricebook: 'gbp-m-sale-prices', - quantity: 10 - } - ] - } - const {listPrice, currentPrice} = getPriceData(data, {quantity: 9}) - - expect(listPrice).toBe(30.6) - expect(currentPrice).toBe(25.6) + test('returns correct priceData for product set', () => { + const priceData = getPriceData(mockProductSetHit) + expect(priceData).toEqual({ + salePrice: 40.16, + listPrice: undefined, + isOnSale: false, + isASet: true, + isMaster: false, + isRange: true, + hasRepresentedProduct: true, + tieredPrice: undefined, + maxPrice: 71.03 + }) }) - test('returns pick the promotional Price for product currentPrice when it is the lowest price among other sale prices', () => { - const data = { - name: 'product name', - price: 25.6, - productPromotions: [ - { - promotionalPrice: 10.99, - id: 'promotional-price' - } - ], - tieredPrices: [ - { - price: 30.6, - pricebook: 'gbp-m-list-prices', - quantity: 1 - }, - { - price: 25.6, - pricebook: 'gbp-m-sale-prices', - quantity: 1 - }, - { - price: 15, - pricebook: 'gbp-m-sale-prices', - quantity: 10 - } - ] - } - const {listPrice, currentPrice} = getPriceData(data, {quantity: 11}) - - expect(listPrice).toBe(30.6) - expect(currentPrice).toBe(10.99) + test('returns correct priceData for standard product', () => { + const priceData = getPriceData(mockStandardProductHit) + expect(priceData).toEqual({ + salePrice: 63.99, + listPrice: 67.99, + isOnSale: true, + isASet: false, + isMaster: false, + isRange: false, + hasRepresentedProduct: true, + tieredPrice: 67.99, + maxPrice: 67.99 + }) }) }) From cf518877d3c9647a222b0a118ad3ecc2cf95a255 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 10 May 2024 11:43:01 -0700 Subject: [PATCH 27/45] adjust price display --- .../app/components/display-price/index.jsx | 23 +++- .../static/translations/compiled/en-GB.json | 104 +++++++++++++++++- .../static/translations/compiled/en-US.json | 104 +++++++++++++++++- .../static/translations/compiled/en-XA.json | 104 +++++++++++++++++- .../translations/en-GB.json | 2 +- .../translations/en-US.json | 2 +- 6 files changed, 330 insertions(+), 9 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 203a250e50..f94b3c856c 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -13,8 +13,9 @@ import {FormattedMessage, FormattedNumber} from 'react-intl' /** * @param priceData - price info extracted from a product * // If a product is a set, - * on PLP, don't show price at all - * on PDP, the set children will have it own price as From X (cross) Y + * on PLP, Show From X where X is the lowest of its children + * on PDP, Show From X where X is the lowest of its children and + * the set children will have it own price as From X (cross) Y * // if a product is a master * on PLP and PDP, show From X (cross) Y , the X and Y are * sale and list price of variant that has the lowest price (including promotionalPrice) @@ -42,7 +43,21 @@ const DisplayPrice = ({priceData, currency}) => { { { isRange, select, - true {From} + true { + { + isOnSale, select, + true {From} + fales { + { + hasRepresentedProduct, select, + true {From} + false {From} + other {} + } + } + other {From} + } + } false {} other {} } @@ -51,7 +66,7 @@ const DisplayPrice = ({priceData, currency}) => { other {} } {isASet, select, - true {} + true {From {salePrice}} false { { diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 711703a911..6b9d746054 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -2526,7 +2526,95 @@ "value": [ { "type": 0, - "value": "From" + "value": " " + }, + { + "options": { + "fales": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "From" + } + ], + "type": 8, + "value": "span" + } + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "From" + } + ], + "type": 8, + "value": "b" + } + ] + } + }, + "type": 5, + "value": "hasRepresentedProduct" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "From" + } + ], + "type": 8, + "value": "span" + } + ] + }, + "true": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "From" + } + ], + "type": 8, + "value": "b" + } + ] + } + }, + "type": 5, + "value": "isOnSale" + }, + { + "type": 0, + "value": " " } ] } @@ -2720,6 +2808,20 @@ }, "true": { "value": [ + { + "type": 0, + "value": "From " + }, + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "span" + } ] } }, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 711703a911..6b9d746054 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -2526,7 +2526,95 @@ "value": [ { "type": 0, - "value": "From" + "value": " " + }, + { + "options": { + "fales": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "From" + } + ], + "type": 8, + "value": "span" + } + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "From" + } + ], + "type": 8, + "value": "b" + } + ] + } + }, + "type": 5, + "value": "hasRepresentedProduct" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "From" + } + ], + "type": 8, + "value": "span" + } + ] + }, + "true": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "From" + } + ], + "type": 8, + "value": "b" + } + ] + } + }, + "type": 5, + "value": "isOnSale" + }, + { + "type": 0, + "value": " " } ] } @@ -2720,6 +2808,20 @@ }, "true": { "value": [ + { + "type": 0, + "value": "From " + }, + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "span" + } ] } }, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index 02bc968896..bd99089899 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -5338,7 +5338,95 @@ "value": [ { "type": 0, - "value": "Ƒřǿǿḿ" + "value": " " + }, + { + "options": { + "fales": { + "value": [ + { + "type": 0, + "value": " " + }, + { + "options": { + "false": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "Ƒřǿǿḿ" + } + ], + "type": 8, + "value": "span" + } + ] + }, + "other": { + "value": [ + ] + }, + "true": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "Ƒřǿǿḿ" + } + ], + "type": 8, + "value": "b" + } + ] + } + }, + "type": 5, + "value": "hasRepresentedProduct" + }, + { + "type": 0, + "value": " " + } + ] + }, + "other": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "Ƒřǿǿḿ" + } + ], + "type": 8, + "value": "span" + } + ] + }, + "true": { + "value": [ + { + "children": [ + { + "type": 0, + "value": "Ƒřǿǿḿ" + } + ], + "type": 8, + "value": "b" + } + ] + } + }, + "type": 5, + "value": "isOnSale" + }, + { + "type": 0, + "value": " " } ] } @@ -5532,6 +5620,20 @@ }, "true": { "value": [ + { + "type": 0, + "value": "Ƒřǿǿḿ " + }, + { + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "span" + } ] } }, diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 953c09fdad..74ed825ced 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -1067,7 +1067,7 @@ "defaultMessage": "Remove {product} from wishlist" }, "product_tile.price_display": { - "defaultMessage": "{isMaster, select, true { { isRange, select, true {From} false {} other {} } } false {} other {} } {isASet, select, true {} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" + "defaultMessage": "{isMaster, select, true { { isRange, select, true { { isOnSale, select, true {From} fales { { hasRepresentedProduct, select, true {From} false {From} other {} } } other {From} } } false {} other {} } } false {} other {} } {isASet, select, true {From {salePrice}} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 953c09fdad..74ed825ced 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -1067,7 +1067,7 @@ "defaultMessage": "Remove {product} from wishlist" }, "product_tile.price_display": { - "defaultMessage": "{isMaster, select, true { { isRange, select, true {From} false {} other {} } } false {} other {} } {isASet, select, true {} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" + "defaultMessage": "{isMaster, select, true { { isRange, select, true { { isOnSale, select, true {From} fales { { hasRepresentedProduct, select, true {From} false {From} other {} } } other {From} } } false {} other {} } } false {} other {} } {isASet, select, true {From {salePrice}} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" From 7600f9582c3c7f3ac7feaf931ce5cf90a357d7ba Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Fri, 10 May 2024 12:13:14 -0700 Subject: [PATCH 28/45] fix tests --- .../app/components/_app/index.jsx | 2 +- .../app/components/display-price/index.jsx | 2 +- .../components/display-price/index.test.js | 5 +-- .../app/components/product-tile/index.jsx | 5 ++- .../app/components/product-tile/index.test.js | 32 ++++++++++--------- .../app/components/product-view/index.jsx | 3 +- .../app/components/product-view/index.test.js | 5 +-- .../app/mocks/product-search-hit-data.js | 7 ++++ .../static/translations/compiled/en-GB.json | 10 ++++-- .../static/translations/compiled/en-US.json | 10 ++++-- .../static/translations/compiled/en-XA.json | 10 ++++-- .../app/utils/test-utils.js | 1 - .../app/utils/utils.test.js | 2 +- .../translations/en-GB.json | 2 +- .../translations/en-US.json | 2 +- 15 files changed, 63 insertions(+), 35 deletions(-) diff --git a/packages/template-retail-react-app/app/components/_app/index.jsx b/packages/template-retail-react-app/app/components/_app/index.jsx index 429dc7c24c..4046ede86f 100644 --- a/packages/template-retail-react-app/app/components/_app/index.jsx +++ b/packages/template-retail-react-app/app/components/_app/index.jsx @@ -57,7 +57,7 @@ import {useCurrentBasket} from '@salesforce/retail-react-app/app/hooks/use-curre import {withCommerceSdkReact} from '@salesforce/retail-react-app/app/components/with-commerce-sdk-react/with-commerce-sdk-react' // Localization -import {FormattedNumber, IntlProvider} from 'react-intl' +import {IntlProvider} from 'react-intl' // Others import {watchOnlineStatus, flatten, isServer} from '@salesforce/retail-react-app/app/utils/utils' diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index f94b3c856c..960ff004e7 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -66,7 +66,7 @@ const DisplayPrice = ({priceData, currency}) => { other {} } {isASet, select, - true {From {salePrice}} + true {From {salePrice}} false { { diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index 336591a695..08c45711bc 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -29,9 +29,10 @@ describe('DisplayPrice', function () { const {container} = renderWithProviders() const currentPriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(currentPriceTag[0]).getByText(/£90\.00/i)).toBeDefined() + // From and salePrice are in two separate b tags + expect(within(currentPriceTag[1]).getByText(/£90\.00/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£100\.00/i)).toBeDefined() - expect(currentPriceTag).toHaveLength(1) + expect(currentPriceTag).toHaveLength(2) expect(strikethroughPriceTag).toHaveLength(1) }) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.jsx b/packages/template-retail-react-app/app/components/product-tile/index.jsx index 64c168c4b4..953b38e97c 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.jsx +++ b/packages/template-retail-react-app/app/components/product-tile/index.jsx @@ -5,7 +5,7 @@ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import React, {useMemo, useRef} from 'react' +import React, {useRef} from 'react' import PropTypes from 'prop-types' import {HeartIcon, HeartSolidIcon} from '@salesforce/retail-react-app/app/components/icons' import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price' @@ -26,12 +26,11 @@ import DynamicImage from '@salesforce/retail-react-app/app/components/dynamic-im import {useIntl} from 'react-intl' // Other -import {productUrlBuilder, rebuildPathWithParams} from '@salesforce/retail-react-app/app/utils/url' +import {productUrlBuilder} from '@salesforce/retail-react-app/app/utils/url' import Link from '@salesforce/retail-react-app/app/components/link' import withRegistration from '@salesforce/retail-react-app/app/components/with-registration' import {getPriceData} from '@salesforce/retail-react-app/app/utils/product-utils' import {useCurrency} from '@salesforce/retail-react-app/app/hooks' -import {PRICE_DISPLAY_FORMAT} from '@salesforce/retail-react-app/app/constants' const IconButtonWithRegistration = withRegistration(IconButton) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.test.js b/packages/template-retail-react-app/app/components/product-tile/index.test.js index 0b4a1df8b4..6398f2973b 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.test.js +++ b/packages/template-retail-react-app/app/components/product-tile/index.test.js @@ -9,6 +9,7 @@ import ProductTile, {Skeleton} from '@salesforce/retail-react-app/app/components import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' import {fireEvent, within} from '@testing-library/react' import { + mockMasterProductHitWithMultipleVariants, mockMasterProductHitWithOneVariant, mockProductSearchItem, mockProductSetHit, @@ -50,29 +51,30 @@ test('Remove from wishlist cannot be muti-clicked', () => { expect(onClick).toHaveBeenCalledTimes(1) }) -test('renders exact price with strikethrough price for master product can be filtered down to one variant ', () => { - const {getByText, container} = renderWithProviders( - +test('renders exact price with strikethrough price for master product can be has various variants', () => { + const {queryByText, getByText, container} = renderWithProviders( + ) - expect(getByText(/black flat front wool suit/i)).toBeInTheDocument() - expect(getByText(/£191\.99/i)).toBeInTheDocument() - expect(getByText(/£320\.00/i)).toBeInTheDocument() + expect(getByText(/Black Single Pleat Athletic Fit Wool Suit - Edit/i)).toBeInTheDocument() + expect(queryByText(/from/i)).toBeInTheDocument() const salePriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(salePriceTag[0]).getByText(/£191\.99/i)).toBeDefined() - expect(within(strikethroughPriceTag[0]).getByText(/£320\.00/i)).toBeDefined() - expect(salePriceTag).toHaveLength(1) + expect(within(salePriceTag[1]).getByText(/£191\.99/i)).toBeDefined() + expect(within(strikethroughPriceTag[0]).getByText(/£223\.99/i)).toBeDefined() + // From and price are in separate b tag + expect(salePriceTag).toHaveLength(2) expect(strikethroughPriceTag).toHaveLength(1) }) -test('renders exact price with strikethrough price for master product can be filtered down to one variant ', () => { - const {getByText, container} = renderWithProviders( +test('renders exact price with strikethrough price for master product can be filtered down to one variant', () => { + const {getByText, queryByText, container} = renderWithProviders( ) expect(getByText(/black flat front wool suit/i)).toBeInTheDocument() expect(getByText(/£191\.99/i)).toBeInTheDocument() expect(getByText(/£320\.00/i)).toBeInTheDocument() + expect(queryByText(/from/i)).not.toBeInTheDocument() const salePriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') @@ -82,13 +84,13 @@ test('renders exact price with strikethrough price for master product can be fil expect(strikethroughPriceTag).toHaveLength(1) }) -test('Product set - does not render strike through price', () => { - const {getByText, queryByText, container} = renderWithProviders( +test('Product set - shows range From X where X is the lowest price child', () => { + const {getByText, queryByText} = renderWithProviders( ) expect(getByText(/Winter Look/i)).toBeInTheDocument() - expect(queryByText(/from/i)).not.toBeInTheDocument() - expect(queryByText(/£40\.16/i)).not.toBeInTheDocument() + expect(queryByText(/from/i)).toBeInTheDocument() + expect(queryByText(/£40\.16/i)).toBeInTheDocument() expect(queryByText(/£44\.16/i)).not.toBeInTheDocument() }) diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index bb27225950..e12107f837 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -63,7 +63,8 @@ const ProductViewHeader = ({name, currency, priceData, category}) => { ProductViewHeader.propTypes = { name: PropTypes.string, currency: PropTypes.string, - category: PropTypes.array + category: PropTypes.array, + priceData: PropTypes.object } const ButtonWithRegistration = withRegistration(Button) diff --git a/packages/template-retail-react-app/app/components/product-view/index.test.js b/packages/template-retail-react-app/app/components/product-view/index.test.js index f9ab13682f..e13c583c94 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.test.js +++ b/packages/template-retail-react-app/app/components/product-view/index.test.js @@ -174,8 +174,9 @@ test('renders a product set properly - child item', () => { expect(variationAttributes).toHaveLength(2) expect(quantityPicker).toBeInTheDocument() - // What should _not_ exist: - expect(fromLabels).toHaveLength(0) + // since setProducts are master products, as pricing now display From X (cross) Y where X Y are sale and lis price respectively + // of the variant that has lowest price (including promotional price) + expect(fromLabels).toHaveLength(2) }) test('validateOrderability callback is called when adding a set to cart', async () => { diff --git a/packages/template-retail-react-app/app/mocks/product-search-hit-data.js b/packages/template-retail-react-app/app/mocks/product-search-hit-data.js index 6984cb7437..9970e08998 100644 --- a/packages/template-retail-react-app/app/mocks/product-search-hit-data.js +++ b/packages/template-retail-react-app/app/mocks/product-search-hit-data.js @@ -1,3 +1,10 @@ +/* + * Copyright (c) 2024, salesforce.com, 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 + */ + const mockProductSearchItem = { currency: 'USD', image: { diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 6b9d746054..77e69087e3 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -2815,8 +2815,14 @@ { "children": [ { - "type": 1, - "value": "salePrice" + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" } ], "type": 8, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 6b9d746054..77e69087e3 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -2815,8 +2815,14 @@ { "children": [ { - "type": 1, - "value": "salePrice" + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" } ], "type": 8, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index bd99089899..382f633a41 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -5627,8 +5627,14 @@ { "children": [ { - "type": 1, - "value": "salePrice" + "children": [ + { + "type": 1, + "value": "salePrice" + } + ], + "type": 8, + "value": "price" } ], "type": 8, diff --git a/packages/template-retail-react-app/app/utils/test-utils.js b/packages/template-retail-react-app/app/utils/test-utils.js index d9e9cabfd6..ba4c9ccf33 100644 --- a/packages/template-retail-react-app/app/utils/test-utils.js +++ b/packages/template-retail-react-app/app/utils/test-utils.js @@ -26,7 +26,6 @@ import {createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url' import {getSiteByReference} from '@salesforce/retail-react-app/app/utils/site-utils' import jwt from 'jsonwebtoken' import userEvent from '@testing-library/user-event' -import {Text} from '@chakra-ui/react' // This JWT's payload is special // it includes 3 fields that commerce-sdk-react cares: // exp, isb and sub diff --git a/packages/template-retail-react-app/app/utils/utils.test.js b/packages/template-retail-react-app/app/utils/utils.test.js index 76840ff6d0..f9388477b7 100644 --- a/packages/template-retail-react-app/app/utils/utils.test.js +++ b/packages/template-retail-react-app/app/utils/utils.test.js @@ -198,7 +198,7 @@ describe('getSmallestValByKey', function () { } ] const val = getSmallestValByProperty(data, 'price') - expect(val).toEqual(9) + expect(val).toBe(9) }) test('should undefined if array is not passed in', () => { const data = { diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 74ed825ced..42282a53a8 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -1067,7 +1067,7 @@ "defaultMessage": "Remove {product} from wishlist" }, "product_tile.price_display": { - "defaultMessage": "{isMaster, select, true { { isRange, select, true { { isOnSale, select, true {From} fales { { hasRepresentedProduct, select, true {From} false {From} other {} } } other {From} } } false {} other {} } } false {} other {} } {isASet, select, true {From {salePrice}} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" + "defaultMessage": "{isMaster, select, true { { isRange, select, true { { isOnSale, select, true {From} fales { { hasRepresentedProduct, select, true {From} false {From} other {} } } other {From} } } false {} other {} } } false {} other {} } {isASet, select, true {From {salePrice}} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 74ed825ced..42282a53a8 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -1067,7 +1067,7 @@ "defaultMessage": "Remove {product} from wishlist" }, "product_tile.price_display": { - "defaultMessage": "{isMaster, select, true { { isRange, select, true { { isOnSale, select, true {From} fales { { hasRepresentedProduct, select, true {From} false {From} other {} } } other {From} } } false {} other {} } } false {} other {} } {isASet, select, true {From {salePrice}} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" + "defaultMessage": "{isMaster, select, true { { isRange, select, true { { isOnSale, select, true {From} fales { { hasRepresentedProduct, select, true {From} false {From} other {} } } other {From} } } false {} other {} } } false {} other {} } {isASet, select, true {From {salePrice}} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" From e4cf00961a6d2d15bdcf79079d1a4cd3dd0bca6e Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 13 May 2024 12:26:09 -0700 Subject: [PATCH 29/45] replace formatted string with jsx logic --- .../app/components/_app/index.jsx | 9 - .../app/components/display-price/index.jsx | 184 +++++--- .../components/display-price/index.test.js | 4 +- .../app/components/product-tile/index.test.js | 4 +- .../app/components/product-view/index.jsx | 12 +- .../static/translations/compiled/en-GB.json | 366 ++-------------- .../static/translations/compiled/en-US.json | 366 ++-------------- .../static/translations/compiled/en-XA.json | 398 +++--------------- .../app/utils/product-utils.js | 4 +- .../app/utils/test-utils.js | 25 +- .../translations/en-GB.json | 12 +- .../translations/en-US.json | 12 +- 12 files changed, 255 insertions(+), 1141 deletions(-) diff --git a/packages/template-retail-react-app/app/components/_app/index.jsx b/packages/template-retail-react-app/app/components/_app/index.jsx index 4046ede86f..d06c54c1d3 100644 --- a/packages/template-retail-react-app/app/components/_app/index.jsx +++ b/packages/template-retail-react-app/app/components/_app/index.jsx @@ -280,15 +280,6 @@ const App = (props) => { )} {chunks}, - br: () =>
, - p: (chunks) => {chunks}, - s: (chunks) => {chunks}, - span: (chunks) => {chunks} - }} onError={(err) => { if (!messages) { // During the ssr prepass phase the messages object has not loaded, so we can suppress diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 960ff004e7..835160fa5d 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -8,7 +8,7 @@ import React from 'react' import PropTypes from 'prop-types' import {Box, Text} from '@salesforce/retail-react-app/app/components/shared/ui' -import {FormattedMessage, FormattedNumber} from 'react-intl' +import {useIntl} from 'react-intl' /** * @param priceData - price info extracted from a product @@ -24,76 +24,126 @@ import {FormattedMessage, FormattedNumber} from 'react-intl' * @param currency - currency */ const DisplayPrice = ({priceData, currency}) => { - const PRICE_DATA_CUSTOM_TAGS = { - // we re-defined here since they are specific for this component - s: (chunks) => ( - - {chunks} + const intl = useIntl() + const {listPrice, salePrice, isASet, isMaster, isOnSale, isRange, hasRepresentedProduct} = + priceData + const salePriceText = intl.formatNumber(salePrice, { + style: 'currency', + currency: currency + }) + const listPriceText = + listPrice && + intl.formatNumber(listPrice, { + style: 'currency', + currency: currency + }) + + const prefixText = intl.formatMessage({ + id: 'price_display.text.from', + defaultMessage: 'From ' + }) + + const ariaLabelSalePrice = intl.formatMessage( + { + id: 'display_price.assistive_msg.current_price', + defaultMessage: `current price {price}` + }, + { + price: intl.formatNumber(salePrice || 0, {style: 'currency', currency}) + } + ) + const ariaLabelListPrice = intl.formatMessage( + { + id: 'display_price.assistive_msg.strikethrough_price', + defaultMessage: `old price {price}` + }, + { + price: intl.formatNumber(listPrice || 0, {style: 'currency', currency}) + } + ) + if (isASet) { + return hasRepresentedProduct ? ( + + {prefixText} {salePriceText} - ), - price: (chunks) => + ) : ( + + {prefixText} {salePriceText} + + ) + } + if (isMaster) { + if (isRange) { + if (isOnSale) { + return ( + <> + + {prefixText} {salePriceText} + {' '} + + {listPriceText} + + + ) + } else { + // bold front on PDP, normal font on PLP + return hasRepresentedProduct ? ( + + {prefixText} {salePriceText} + + ) : ( + + {prefixText} {salePriceText} + + ) + } + } else { + if (isOnSale) { + return ( + <> + + {salePriceText} + {' '} + + {listPriceText} + + + ) + } else { + return hasRepresentedProduct ? ( + + {salePriceText} + + ) : ( + + {salePriceText} + + ) + } + } } return ( - + {isOnSale ? ( + <> + + {salePriceText} + {' '} + {listPriceText && ( + + {listPriceText} + + )} + + ) : ( + + {salePriceText} + + )} ) } diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index 08c45711bc..8ed5cb8282 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -30,9 +30,9 @@ describe('DisplayPrice', function () { const currentPriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') // From and salePrice are in two separate b tags - expect(within(currentPriceTag[1]).getByText(/£90\.00/i)).toBeDefined() + expect(within(currentPriceTag[0]).getByText(/£90\.00/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£100\.00/i)).toBeDefined() - expect(currentPriceTag).toHaveLength(2) + expect(currentPriceTag).toHaveLength(1) expect(strikethroughPriceTag).toHaveLength(1) }) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.test.js b/packages/template-retail-react-app/app/components/product-tile/index.test.js index 6398f2973b..6368a396f3 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.test.js +++ b/packages/template-retail-react-app/app/components/product-tile/index.test.js @@ -60,10 +60,10 @@ test('renders exact price with strikethrough price for master product can be has const salePriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(salePriceTag[1]).getByText(/£191\.99/i)).toBeDefined() + expect(within(salePriceTag[0]).getByText(/£191\.99/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£223\.99/i)).toBeDefined() // From and price are in separate b tag - expect(salePriceTag).toHaveLength(2) + expect(salePriceTag).toHaveLength(1) expect(strikethroughPriceTag).toHaveLength(1) }) diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index e12107f837..d669ca99a3 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -64,7 +64,17 @@ ProductViewHeader.propTypes = { name: PropTypes.string, currency: PropTypes.string, category: PropTypes.array, - priceData: PropTypes.object + priceData: PropTypes.shape({ + salePrice: PropTypes.number.isRequired, + isOnSale: PropTypes.bool.isRequired, + listPrice: PropTypes.number, + isASet: PropTypes.bool, + isMaster: PropTypes.bool, + isRange: PropTypes.bool, + hasRepresentedProduct: PropTypes.bool, + maxPrice: PropTypes.number, + tieredPrice: PropTypes.number + }) } const ButtonWithRegistration = withRegistration(Button) diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 77e69087e3..b04088df5f 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -1005,6 +1005,26 @@ "value": "Security code info" } ], + "display_price.assistive_msg.current_price": [ + { + "type": 0, + "value": "current price " + }, + { + "type": 1, + "value": "price" + } + ], + "display_price.assistive_msg.strikethrough_price": [ + { + "type": 0, + "value": "old price " + }, + { + "type": 1, + "value": "price" + } + ], "drawer_menu.button.account_details": [ { "type": 0, @@ -2273,6 +2293,12 @@ "value": "This is a secure SSL encrypted payment." } ], + "price_display.text.from": [ + { + "type": 0, + "value": "From" + } + ], "price_per_item.label.each": [ { "type": 0, @@ -2495,346 +2521,6 @@ "value": " from wishlist" } ], - "product_tile.price_display": [ - { - "options": { - "false": { - "value": [ - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "fales": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "From" - } - ], - "type": 8, - "value": "span" - } - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "From" - } - ], - "type": 8, - "value": "b" - } - ] - } - }, - "type": 5, - "value": "hasRepresentedProduct" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "From" - } - ], - "type": 8, - "value": "span" - } - ] - }, - "true": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "From" - } - ], - "type": 8, - "value": "b" - } - ] - } - }, - "type": 5, - "value": "isOnSale" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isRange" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isMaster" - }, - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "true": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "span" - } - ] - } - }, - "type": 5, - "value": "hasRepresentedProduct" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - }, - { - "type": 0, - "value": " " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "listPrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "s" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isOnSale" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": "From " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "span" - } - ] - } - }, - "type": 5, - "value": "isASet" - } - ], "product_view.button.add_set_to_cart": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 77e69087e3..b04088df5f 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -1005,6 +1005,26 @@ "value": "Security code info" } ], + "display_price.assistive_msg.current_price": [ + { + "type": 0, + "value": "current price " + }, + { + "type": 1, + "value": "price" + } + ], + "display_price.assistive_msg.strikethrough_price": [ + { + "type": 0, + "value": "old price " + }, + { + "type": 1, + "value": "price" + } + ], "drawer_menu.button.account_details": [ { "type": 0, @@ -2273,6 +2293,12 @@ "value": "This is a secure SSL encrypted payment." } ], + "price_display.text.from": [ + { + "type": 0, + "value": "From" + } + ], "price_per_item.label.each": [ { "type": 0, @@ -2495,346 +2521,6 @@ "value": " from wishlist" } ], - "product_tile.price_display": [ - { - "options": { - "false": { - "value": [ - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "fales": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "From" - } - ], - "type": 8, - "value": "span" - } - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "From" - } - ], - "type": 8, - "value": "b" - } - ] - } - }, - "type": 5, - "value": "hasRepresentedProduct" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "From" - } - ], - "type": 8, - "value": "span" - } - ] - }, - "true": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "From" - } - ], - "type": 8, - "value": "b" - } - ] - } - }, - "type": 5, - "value": "isOnSale" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isRange" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isMaster" - }, - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "true": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "span" - } - ] - } - }, - "type": 5, - "value": "hasRepresentedProduct" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - }, - { - "type": 0, - "value": " " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "listPrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "s" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isOnSale" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": "From " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "span" - } - ] - } - }, - "type": 5, - "value": "isASet" - } - ], "product_view.button.add_set_to_cart": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index 382f633a41..d89ef96ba8 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -2085,6 +2085,42 @@ "value": "]" } ], + "display_price.assistive_msg.current_price": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "ƈŭŭřřḗḗƞŧ ƥřīƈḗḗ " + }, + { + "type": 1, + "value": "price" + }, + { + "type": 0, + "value": "]" + } + ], + "display_price.assistive_msg.strikethrough_price": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "ǿǿŀḓ ƥřīƈḗḗ " + }, + { + "type": 1, + "value": "price" + }, + { + "type": 0, + "value": "]" + } + ], "drawer_menu.button.account_details": [ { "type": 0, @@ -4881,6 +4917,20 @@ "value": "]" } ], + "price_display.text.from": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ƒřǿǿḿ" + }, + { + "type": 0, + "value": "]" + } + ], "price_per_item.label.each": [ { "type": 0, @@ -5303,354 +5353,6 @@ "value": "]" } ], - "product_tile.price_display": [ - { - "type": 0, - "value": "[" - }, - { - "options": { - "false": { - "value": [ - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "fales": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "Ƒřǿǿḿ" - } - ], - "type": 8, - "value": "span" - } - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "Ƒřǿǿḿ" - } - ], - "type": 8, - "value": "b" - } - ] - } - }, - "type": 5, - "value": "hasRepresentedProduct" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "Ƒřǿǿḿ" - } - ], - "type": 8, - "value": "span" - } - ] - }, - "true": { - "value": [ - { - "children": [ - { - "type": 0, - "value": "Ƒřǿǿḿ" - } - ], - "type": 8, - "value": "b" - } - ] - } - }, - "type": 5, - "value": "isOnSale" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isRange" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isMaster" - }, - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "options": { - "false": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "true": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "span" - } - ] - } - }, - "type": 5, - "value": "hasRepresentedProduct" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - } - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": " " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "b" - }, - { - "type": 0, - "value": " " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "listPrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "s" - }, - { - "type": 0, - "value": " " - } - ] - } - }, - "type": 5, - "value": "isOnSale" - }, - { - "type": 0, - "value": " " - } - ] - }, - "other": { - "value": [ - ] - }, - "true": { - "value": [ - { - "type": 0, - "value": "Ƒřǿǿḿ " - }, - { - "children": [ - { - "children": [ - { - "type": 1, - "value": "salePrice" - } - ], - "type": 8, - "value": "price" - } - ], - "type": 8, - "value": "span" - } - ] - } - }, - "type": 5, - "value": "isASet" - }, - { - "type": 0, - "value": "]" - } - ], "product_view.button.add_set_to_cart": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 11301b3b78..15cbfbb095 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -72,9 +72,6 @@ export const getPriceData = (product, opts = {}) => { }, {minPrice: Infinity, variant: null} ) - } - - if (isMaster) { salePrice = variantWithLowestPrice?.minPrice } else { const promotionalPrice = getSmallestValByProperty( @@ -86,6 +83,7 @@ export const getPriceData = (product, opts = {}) => { ? promotionalPrice : product?.price } + // since the price is the lowest value among price books, each product will have at lease a single item tiered price at quantity 1 // the highest value of tieredPrices is presumptively the list price const tieredPrices = diff --git a/packages/template-retail-react-app/app/utils/test-utils.js b/packages/template-retail-react-app/app/utils/test-utils.js index ba4c9ccf33..c6105b6f37 100644 --- a/packages/template-retail-react-app/app/utils/test-utils.js +++ b/packages/template-retail-react-app/app/utils/test-utils.js @@ -79,17 +79,7 @@ export const DEFAULT_SITE = 'global' export const renderWithReactIntl = (node, locale = DEFAULT_LOCALE) => { return render( - {chunks}, - br: () =>
, - p: (chunks) =>

{chunks}

, - s: (chunks) => {chunks}, - span: (chunks) => {chunks} - }} - locale={locale} - defaultLocale={locale} - > + {node} ) @@ -132,18 +122,7 @@ export const TestProviders = ({ return ( - {chunks}, - br: () =>
, - p: (chunks) =>

{chunks}

, - s: (chunks) => {chunks}, - span: (chunks) => {chunks} - }} - locale={locale.id} - defaultLocale={DEFAULT_LOCALE} - messages={messages} - > + From} fales { { hasRepresentedProduct, select, true {From} false {From} other {} } } other {From} } } false {} other {} } } false {} other {} } {isASet, select, true {From {salePrice}} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" - }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" }, diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 42282a53a8..172b1aae0e 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -408,6 +408,12 @@ "credit_card_fields.tool_tip.security_code_aria_label": { "defaultMessage": "Security code info" }, + "display_price.assistive_msg.current_price": { + "defaultMessage": "current price {price}" + }, + "display_price.assistive_msg.strikethrough_price": { + "defaultMessage": "old price {price}" + }, "drawer_menu.button.account_details": { "defaultMessage": "Account Details" }, @@ -990,6 +996,9 @@ "payment_selection.tooltip.secure_payment": { "defaultMessage": "This is a secure SSL encrypted payment." }, + "price_display.text.from": { + "defaultMessage": "From" + }, "price_per_item.label.each": { "defaultMessage": "ea", "description": "Abbreviated 'each', follows price per item, like $10/ea" @@ -1066,9 +1075,6 @@ "product_tile.assistive_msg.remove_from_wishlist": { "defaultMessage": "Remove {product} from wishlist" }, - "product_tile.price_display": { - "defaultMessage": "{isMaster, select, true { { isRange, select, true { { isOnSale, select, true {From} fales { { hasRepresentedProduct, select, true {From} false {From} other {} } } other {From} } } false {} other {} } } false {} other {} } {isASet, select, true {From {salePrice}} false { { isOnSale, select, true { {salePrice} {listPrice} } false { { hasRepresentedProduct, select, true {{salePrice}} false {{salePrice}} other {{salePrice}} } } other {{salePrice}} } } other {} }" - }, "product_view.button.add_set_to_cart": { "defaultMessage": "Add Set to Cart" }, From 04a4ce81ac2f1f7ed0a0477451cd80ca4b996e52 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 13 May 2024 12:39:09 -0700 Subject: [PATCH 30/45] fix tests naming --- .../app/components/product-tile/index.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/template-retail-react-app/app/components/product-tile/index.test.js b/packages/template-retail-react-app/app/components/product-tile/index.test.js index 6368a396f3..b8d86f8906 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.test.js +++ b/packages/template-retail-react-app/app/components/product-tile/index.test.js @@ -51,7 +51,7 @@ test('Remove from wishlist cannot be muti-clicked', () => { expect(onClick).toHaveBeenCalledTimes(1) }) -test('renders exact price with strikethrough price for master product can be has various variants', () => { +test('renders exact price with strikethrough price for master product with multiple variants', () => { const {queryByText, getByText, container} = renderWithProviders( ) @@ -67,7 +67,7 @@ test('renders exact price with strikethrough price for master product can be has expect(strikethroughPriceTag).toHaveLength(1) }) -test('renders exact price with strikethrough price for master product can be filtered down to one variant', () => { +test('renders exact price with strikethrough price for master product with one variant', () => { const {getByText, queryByText, container} = renderWithProviders( ) From 9045fa427bc2733f0be978734d1ff71fac957da0 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 13 May 2024 13:38:35 -0700 Subject: [PATCH 31/45] fiz translations --- .../app/components/display-price/index.jsx | 2 +- .../app/static/translations/compiled/en-GB.json | 2 +- .../app/static/translations/compiled/en-US.json | 2 +- .../app/static/translations/compiled/en-XA.json | 2 +- packages/template-retail-react-app/translations/en-GB.json | 2 +- packages/template-retail-react-app/translations/en-US.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 835160fa5d..70a24e5cf5 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -39,7 +39,7 @@ const DisplayPrice = ({priceData, currency}) => { }) const prefixText = intl.formatMessage({ - id: 'price_display.text.from', + id: 'price_display.label.from', defaultMessage: 'From ' }) diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index b04088df5f..98d2406dce 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -2293,7 +2293,7 @@ "value": "This is a secure SSL encrypted payment." } ], - "price_display.text.from": [ + "price_display.label.from": [ { "type": 0, "value": "From" diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index b04088df5f..98d2406dce 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -2293,7 +2293,7 @@ "value": "This is a secure SSL encrypted payment." } ], - "price_display.text.from": [ + "price_display.label.from": [ { "type": 0, "value": "From" diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index d89ef96ba8..6967f26022 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -4917,7 +4917,7 @@ "value": "]" } ], - "price_display.text.from": [ + "price_display.label.from": [ { "type": 0, "value": "[" diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 172b1aae0e..646e32e80b 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -996,7 +996,7 @@ "payment_selection.tooltip.secure_payment": { "defaultMessage": "This is a secure SSL encrypted payment." }, - "price_display.text.from": { + "price_display.label.from": { "defaultMessage": "From" }, "price_per_item.label.each": { diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 172b1aae0e..646e32e80b 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -996,7 +996,7 @@ "payment_selection.tooltip.secure_payment": { "defaultMessage": "This is a secure SSL encrypted payment." }, - "price_display.text.from": { + "price_display.label.from": { "defaultMessage": "From" }, "price_per_item.label.each": { From 9f2150f7e8812f4f29084ad398bfecf2f367bfef Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 13 May 2024 16:50:55 -0700 Subject: [PATCH 32/45] Pr feedback --- .../app/components/display-price/index.jsx | 243 ++++++++++++------ .../components/display-price/index.test.js | 7 +- .../app/components/product-tile/index.test.js | 18 +- .../app/components/product-view/index.jsx | 5 +- .../app/hooks/use-add-to-cart-modal.js | 2 +- .../app/pages/home/index.jsx | 2 + .../static/translations/compiled/en-GB.json | 54 +++- .../static/translations/compiled/en-US.json | 54 +++- .../static/translations/compiled/en-XA.json | 102 ++++++-- .../app/utils/product-utils.js | 12 +- .../app/utils/product-utils.test.js | 8 +- .../translations/en-GB.json | 22 +- .../translations/en-US.json | 22 +- 13 files changed, 399 insertions(+), 152 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 70a24e5cf5..75dd7dbabc 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -8,7 +8,40 @@ import React from 'react' import PropTypes from 'prop-types' import {Box, Text} from '@salesforce/retail-react-app/app/components/shared/ui' -import {useIntl} from 'react-intl' +import {defineMessages, useIntl} from 'react-intl' + +const msg = defineMessages({ + // price display + currentPrice: { + id: 'display_price.label.current_price', + defaultMessage: '{currentPrice}' + }, + currentPriceWithRange: { + id: 'display_price.label.current_price_with_range', + defaultMessage: 'From {currentPrice}' + }, + listPrice: { + id: 'display_price.label.list_price', + defaultMessage: '{listPrice}' + }, + // aria-label + ariaLabelCurrentPrice: { + id: 'display_price.assistive_msg.current_price', + defaultMessage: `current price {currentPrice}` + }, + ariaLabelCurrentPriceWithRange: { + id: 'display_price.assistive_msg.current_price_with_range', + defaultMessage: `From current price {currentPrice}` + }, + ariaLabelListPrice: { + id: 'display_price.assistive_msg.strikethrough_price', + defaultMessage: `original price {listPrice}` + }, + ariaLabelListPriceWithRange: { + id: 'display_price.assistive_msg.strikethrough_price_with_range', + defaultMessage: `From original price {listPrice}` + } +}) /** * @param priceData - price info extracted from a product @@ -18,58 +51,20 @@ import {useIntl} from 'react-intl' * the set children will have it own price as From X (cross) Y * // if a product is a master * on PLP and PDP, show From X (cross) Y , the X and Y are - * sale and list price of variant that has the lowest price (including promotionalPrice) + * current and list price of variant that has the lowest price (including promotionalPrice) * // if a standard/bundle * show exact price on PLP and PDP as X (cross) Y * @param currency - currency */ const DisplayPrice = ({priceData, currency}) => { - const intl = useIntl() - const {listPrice, salePrice, isASet, isMaster, isOnSale, isRange, hasRepresentedProduct} = + const {listPrice, currentPrice, isASet, isMaster, isOnSale, isRange, hasRepresentedProduct} = priceData - const salePriceText = intl.formatNumber(salePrice, { - style: 'currency', - currency: currency - }) - const listPriceText = - listPrice && - intl.formatNumber(listPrice, { - style: 'currency', - currency: currency - }) - - const prefixText = intl.formatMessage({ - id: 'price_display.label.from', - defaultMessage: 'From ' - }) - const ariaLabelSalePrice = intl.formatMessage( - { - id: 'display_price.assistive_msg.current_price', - defaultMessage: `current price {price}` - }, - { - price: intl.formatNumber(salePrice || 0, {style: 'currency', currency}) - } - ) - const ariaLabelListPrice = intl.formatMessage( - { - id: 'display_price.assistive_msg.strikethrough_price', - defaultMessage: `old price {price}` - }, - { - price: intl.formatNumber(listPrice || 0, {style: 'currency', currency}) - } - ) if (isASet) { return hasRepresentedProduct ? ( - - {prefixText} {salePriceText} - + ) : ( - - {prefixText} {salePriceText} - + ) } if (isMaster) { @@ -77,51 +72,43 @@ const DisplayPrice = ({priceData, currency}) => { if (isOnSale) { return ( <> - - {prefixText} {salePriceText} - {' '} - - {listPriceText} - + {' '} + {listPrice && ( + + )} ) } else { // bold front on PDP, normal font on PLP return hasRepresentedProduct ? ( - - {prefixText} {salePriceText} - + ) : ( - - {prefixText} {salePriceText} - + ) } } else { if (isOnSale) { return ( <> - - {salePriceText} - {' '} - - {listPriceText} - + {' '} + ) } else { return hasRepresentedProduct ? ( - - {salePriceText} - + ) : ( - - {salePriceText} - + ) } } @@ -130,27 +117,117 @@ const DisplayPrice = ({priceData, currency}) => { {isOnSale ? ( <> - - {salePriceText} - {' '} - {listPriceText && ( - - {listPriceText} - - )} + {' '} + {listPrice && } ) : ( - - {salePriceText} - + )} ) } +/** + * @private + * Component that displays current price of a product with a11y + * @param price - price of the product + * @param as - an HTML tag or component to be rendered as + * @param isRange - show price as range or not + * @param currency - currency to show the price in + * @param extraProps - extra props to be passed into Text Component + * @returns {JSX.Element} + */ +const CurrentPrice = ({price, as, isRange = false, currency, ...extraProps}) => { + const intl = useIntl() + const currentPriceText = intl.formatNumber(price, { + style: 'currency', + currency + }) + return isRange ? ( + + {intl.formatMessage(msg.currentPriceWithRange, { + currentPrice: currentPriceText + })} + + ) : ( + + {intl.formatMessage(msg.currentPrice, { + currentPrice: currentPriceText + })} + + ) +} +CurrentPrice.propTypes = { + price: PropTypes.number.isRequired, + currency: PropTypes.string.isRequired, + as: PropTypes.string, + isRange: PropTypes.bool, + extraProps: PropTypes.object +} + +/** + * Component that displays list price of a product with a11y + * @param price - price of the product + * @param as - an HTML tag or component to be rendered as + * @param isRange - show price as range or not + * @param props - extra props to be passed into Text Component + * @param extraProps - extra props to be passed into Text Component + * @returns {JSX.Element} + */ +const ListPrice = ({price, isRange = false, as = 's', currency, ...extraProps}) => { + const intl = useIntl() + const listPriceText = intl.formatNumber(price, { + style: 'currency', + currency + }) + return isRange ? ( + + {intl.formatMessage(msg.listPrice, { + listPrice: listPriceText + })} + + ) : ( + + {intl.formatMessage(msg.listPrice, { + listPrice: listPriceText + })} + + ) +} +ListPrice.propTypes = { + price: PropTypes.number.isRequired, + currency: PropTypes.string.isRequired, + as: PropTypes.string, + isRange: PropTypes.bool, + extraProps: PropTypes.object +} DisplayPrice.propTypes = { priceData: PropTypes.shape({ - salePrice: PropTypes.number.isRequired, + currentPrice: PropTypes.number.isRequired, isOnSale: PropTypes.bool.isRequired, listPrice: PropTypes.number, isASet: PropTypes.bool, diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index 8ed5cb8282..0926448bf3 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -11,7 +11,7 @@ import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-u describe('DisplayPrice', function () { const data = { - salePrice: 90, + currentPrice: 90, listPrice: 100, isASet: false, isOnSale: true, @@ -38,7 +38,10 @@ describe('DisplayPrice', function () { test('should not render list price when price is not on sale', () => { renderWithProviders( - + ) expect(screen.queryByText(/£90\.`00/i)).not.toBeInTheDocument() expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() diff --git a/packages/template-retail-react-app/app/components/product-tile/index.test.js b/packages/template-retail-react-app/app/components/product-tile/index.test.js index b8d86f8906..b627ec47a5 100644 --- a/packages/template-retail-react-app/app/components/product-tile/index.test.js +++ b/packages/template-retail-react-app/app/components/product-tile/index.test.js @@ -58,12 +58,12 @@ test('renders exact price with strikethrough price for master product with multi expect(getByText(/Black Single Pleat Athletic Fit Wool Suit - Edit/i)).toBeInTheDocument() expect(queryByText(/from/i)).toBeInTheDocument() - const salePriceTag = container.querySelectorAll('b') + const currentPriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(salePriceTag[0]).getByText(/£191\.99/i)).toBeDefined() + expect(within(currentPriceTag[0]).getByText(/£191\.99/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£223\.99/i)).toBeDefined() // From and price are in separate b tag - expect(salePriceTag).toHaveLength(1) + expect(currentPriceTag).toHaveLength(1) expect(strikethroughPriceTag).toHaveLength(1) }) @@ -76,11 +76,11 @@ test('renders exact price with strikethrough price for master product with one v expect(getByText(/£320\.00/i)).toBeInTheDocument() expect(queryByText(/from/i)).not.toBeInTheDocument() - const salePriceTag = container.querySelectorAll('b') + const currentPriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(salePriceTag[0]).getByText(/£191\.99/i)).toBeDefined() + expect(within(currentPriceTag[0]).getByText(/£191\.99/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£320\.00/i)).toBeDefined() - expect(salePriceTag).toHaveLength(1) + expect(currentPriceTag).toHaveLength(1) expect(strikethroughPriceTag).toHaveLength(1) }) @@ -100,10 +100,10 @@ test('renders strike through price with standard product', () => { ) expect(getByText(/Laptop Briefcase with wheels \(37L\)/i)).toBeInTheDocument() expect(getByText(/£63\.99/i)).toBeInTheDocument() - const salePriceTag = container.querySelectorAll('b') + const currentPriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - expect(within(salePriceTag[0]).getByText(/£63\.99/i)).toBeDefined() + expect(within(currentPriceTag[0]).getByText(/£63\.99/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£67\.99/i)).toBeDefined() - expect(salePriceTag).toHaveLength(1) + expect(currentPriceTag).toHaveLength(1) expect(strikethroughPriceTag).toHaveLength(1) }) diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index d669ca99a3..0157814bf2 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -53,7 +53,7 @@ const ProductViewHeader = ({name, currency, priceData, category}) => { {`${name}`} - + @@ -65,7 +65,7 @@ ProductViewHeader.propTypes = { currency: PropTypes.string, category: PropTypes.array, priceData: PropTypes.shape({ - salePrice: PropTypes.number.isRequired, + currentPrice: PropTypes.number.isRequired, isOnSale: PropTypes.bool.isRequired, listPrice: PropTypes.number, isASet: PropTypes.bool, @@ -132,6 +132,7 @@ const ProductView = forwardRef( const priceData = useMemo(() => { return getPriceData(product, {quantity}) }, [product, quantity]) + console.log('priceData', priceData) const canAddToWishlist = !isProductLoading const isProductASet = product?.type.set const errorContainerRef = useRef(null) diff --git a/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js b/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js index c89599be54..882e79c820 100644 --- a/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js +++ b/packages/template-retail-react-app/app/hooks/use-add-to-cart-modal.js @@ -114,7 +114,7 @@ export const AddToCartModal = () => { viewType: 'small', selectedVariationAttributes: variant.variationValues })?.images?.[0] - const priceData = getPriceData(product) + const priceData = getPriceData(product, {quantity}) const variationAttributeValues = getDisplayVariationValues( product.variationAttributes, variant.variationValues diff --git a/packages/template-retail-react-app/app/pages/home/index.jsx b/packages/template-retail-react-app/app/pages/home/index.jsx index 27e891a1c0..7179504e8d 100644 --- a/packages/template-retail-react-app/app/pages/home/index.jsx +++ b/packages/template-retail-react-app/app/pages/home/index.jsx @@ -64,7 +64,9 @@ const Home = () => { const {data: productSearchResult, isLoading} = useProductSearch({ parameters: { refine: [`cgid=${HOME_SHOP_PRODUCTS_CATEGORY_ID}`, 'htype=master'], + expand: ['promotions', 'variations', 'prices', 'images', 'availability'], perPricebook: true, + allVariationProperties: true, limit: HOME_SHOP_PRODUCTS_LIMIT } }) diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 98d2406dce..94a34215db 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -1012,17 +1012,59 @@ }, { "type": 1, - "value": "price" + "value": "currentPrice" + } + ], + "display_price.assistive_msg.current_price_with_range": [ + { + "type": 0, + "value": "From current price " + }, + { + "type": 1, + "value": "currentPrice" } ], "display_price.assistive_msg.strikethrough_price": [ { "type": 0, - "value": "old price " + "value": "original price " + }, + { + "type": 1, + "value": "listPrice" + } + ], + "display_price.assistive_msg.strikethrough_price_with_range": [ + { + "type": 0, + "value": "From original price " + }, + { + "type": 1, + "value": "listPrice" + } + ], + "display_price.label.current_price": [ + { + "type": 1, + "value": "currentPrice" + } + ], + "display_price.label.current_price_with_range": [ + { + "type": 0, + "value": "From " }, { "type": 1, - "value": "price" + "value": "currentPrice" + } + ], + "display_price.label.list_price": [ + { + "type": 1, + "value": "listPrice" } ], "drawer_menu.button.account_details": [ @@ -2293,12 +2335,6 @@ "value": "This is a secure SSL encrypted payment." } ], - "price_display.label.from": [ - { - "type": 0, - "value": "From" - } - ], "price_per_item.label.each": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 98d2406dce..94a34215db 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -1012,17 +1012,59 @@ }, { "type": 1, - "value": "price" + "value": "currentPrice" + } + ], + "display_price.assistive_msg.current_price_with_range": [ + { + "type": 0, + "value": "From current price " + }, + { + "type": 1, + "value": "currentPrice" } ], "display_price.assistive_msg.strikethrough_price": [ { "type": 0, - "value": "old price " + "value": "original price " + }, + { + "type": 1, + "value": "listPrice" + } + ], + "display_price.assistive_msg.strikethrough_price_with_range": [ + { + "type": 0, + "value": "From original price " + }, + { + "type": 1, + "value": "listPrice" + } + ], + "display_price.label.current_price": [ + { + "type": 1, + "value": "currentPrice" + } + ], + "display_price.label.current_price_with_range": [ + { + "type": 0, + "value": "From " }, { "type": 1, - "value": "price" + "value": "currentPrice" + } + ], + "display_price.label.list_price": [ + { + "type": 1, + "value": "listPrice" } ], "drawer_menu.button.account_details": [ @@ -2293,12 +2335,6 @@ "value": "This is a secure SSL encrypted payment." } ], - "price_display.label.from": [ - { - "type": 0, - "value": "From" - } - ], "price_per_item.label.each": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index 6967f26022..4d1571e945 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -2096,7 +2096,25 @@ }, { "type": 1, - "value": "price" + "value": "currentPrice" + }, + { + "type": 0, + "value": "]" + } + ], + "display_price.assistive_msg.current_price_with_range": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ƒřǿǿḿ ƈŭŭřřḗḗƞŧ ƥřīƈḗḗ " + }, + { + "type": 1, + "value": "currentPrice" }, { "type": 0, @@ -2110,11 +2128,75 @@ }, { "type": 0, - "value": "ǿǿŀḓ ƥřīƈḗḗ " + "value": "ǿǿřīɠīƞȧȧŀ ƥřīƈḗḗ " + }, + { + "type": 1, + "value": "listPrice" + }, + { + "type": 0, + "value": "]" + } + ], + "display_price.assistive_msg.strikethrough_price_with_range": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ƒřǿǿḿ ǿǿřīɠīƞȧȧŀ ƥřīƈḗḗ " + }, + { + "type": 1, + "value": "listPrice" + }, + { + "type": 0, + "value": "]" + } + ], + "display_price.label.current_price": [ + { + "type": 0, + "value": "[" + }, + { + "type": 1, + "value": "currentPrice" + }, + { + "type": 0, + "value": "]" + } + ], + "display_price.label.current_price_with_range": [ + { + "type": 0, + "value": "[" + }, + { + "type": 0, + "value": "Ƒřǿǿḿ " }, { "type": 1, - "value": "price" + "value": "currentPrice" + }, + { + "type": 0, + "value": "]" + } + ], + "display_price.label.list_price": [ + { + "type": 0, + "value": "[" + }, + { + "type": 1, + "value": "listPrice" }, { "type": 0, @@ -4917,20 +4999,6 @@ "value": "]" } ], - "price_display.label.from": [ - { - "type": 0, - "value": "[" - }, - { - "type": 0, - "value": "Ƒřǿǿḿ" - }, - { - "type": 0, - "value": "]" - } - ], "price_per_item.label.each": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 15cbfbb095..2869b3a7d2 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -39,7 +39,7 @@ export const getDisplayVariationValues = (variationAttributes, values = {}) => { /** * This function extract the price information of a given product * If a product is a master, - * salePrice: get the lowest price (including promotional prices) among variants + * currentPrice: get the lowest price (including promotional prices) among variants * listPrice: get the list price of the variant that has lowest price (including promotional price) * maxPrice: the max price in tieredPrices of variant that has lowest price * @param {object} product - product detail object @@ -50,7 +50,7 @@ export const getPriceData = (product, opts = {}) => { const isASet = product?.hitType === 'set' || !!product?.type?.set const isMaster = product?.hitType === 'master' || !!product?.type?.master const hasRepresentedProduct = !!product?.representedProduct?.id - let salePrice + let currentPrice let variantWithLowestPrice // grab the variant that has the lowest price (including promotional price) if (isMaster) { @@ -72,13 +72,13 @@ export const getPriceData = (product, opts = {}) => { }, {minPrice: Infinity, variant: null} ) - salePrice = variantWithLowestPrice?.minPrice + currentPrice = variantWithLowestPrice?.minPrice } else { const promotionalPrice = getSmallestValByProperty( product?.productPromotions, 'promotionalPrice' ) - salePrice = + currentPrice = promotionalPrice && promotionalPrice < product?.price ? promotionalPrice : product?.price @@ -104,9 +104,9 @@ export const getPriceData = (product, opts = {}) => { : prev }) return { - salePrice, + currentPrice, listPrice, - isOnSale: salePrice < listPrice, + isOnSale: currentPrice < listPrice, isASet, isMaster, // For a product, set price is the lowest price of its children, so the price should be considered a range diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index c9c515a213..c4c1bad398 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -65,7 +65,7 @@ describe('getPriceData', function () { test('returns price data for master product that has more than one variant', () => { const priceData = getPriceData(mockMasterProductHitWithMultipleVariants) expect(priceData).toEqual({ - salePrice: 191.99, + currentPrice: 191.99, listPrice: 223.99, isOnSale: true, isASet: false, @@ -80,7 +80,7 @@ describe('getPriceData', function () { test('returns price data for master product that has ONLY one variant', () => { const priceData = getPriceData(mockMasterProductHitWithOneVariant) expect(priceData).toEqual({ - salePrice: 191.99, + currentPrice: 191.99, listPrice: 320, isOnSale: true, isASet: false, @@ -95,7 +95,7 @@ describe('getPriceData', function () { test('returns correct priceData for product set', () => { const priceData = getPriceData(mockProductSetHit) expect(priceData).toEqual({ - salePrice: 40.16, + currentPrice: 40.16, listPrice: undefined, isOnSale: false, isASet: true, @@ -110,7 +110,7 @@ describe('getPriceData', function () { test('returns correct priceData for standard product', () => { const priceData = getPriceData(mockStandardProductHit) expect(priceData).toEqual({ - salePrice: 63.99, + currentPrice: 63.99, listPrice: 67.99, isOnSale: true, isASet: false, diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 646e32e80b..041a88a347 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -409,10 +409,25 @@ "defaultMessage": "Security code info" }, "display_price.assistive_msg.current_price": { - "defaultMessage": "current price {price}" + "defaultMessage": "current price {currentPrice}" + }, + "display_price.assistive_msg.current_price_with_range": { + "defaultMessage": "From current price {currentPrice}" }, "display_price.assistive_msg.strikethrough_price": { - "defaultMessage": "old price {price}" + "defaultMessage": "original price {listPrice}" + }, + "display_price.assistive_msg.strikethrough_price_with_range": { + "defaultMessage": "From original price {listPrice}" + }, + "display_price.label.current_price": { + "defaultMessage": "{currentPrice}" + }, + "display_price.label.current_price_with_range": { + "defaultMessage": "From {currentPrice}" + }, + "display_price.label.list_price": { + "defaultMessage": "{listPrice}" }, "drawer_menu.button.account_details": { "defaultMessage": "Account Details" @@ -996,9 +1011,6 @@ "payment_selection.tooltip.secure_payment": { "defaultMessage": "This is a secure SSL encrypted payment." }, - "price_display.label.from": { - "defaultMessage": "From" - }, "price_per_item.label.each": { "defaultMessage": "ea", "description": "Abbreviated 'each', follows price per item, like $10/ea" diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 646e32e80b..041a88a347 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -409,10 +409,25 @@ "defaultMessage": "Security code info" }, "display_price.assistive_msg.current_price": { - "defaultMessage": "current price {price}" + "defaultMessage": "current price {currentPrice}" + }, + "display_price.assistive_msg.current_price_with_range": { + "defaultMessage": "From current price {currentPrice}" }, "display_price.assistive_msg.strikethrough_price": { - "defaultMessage": "old price {price}" + "defaultMessage": "original price {listPrice}" + }, + "display_price.assistive_msg.strikethrough_price_with_range": { + "defaultMessage": "From original price {listPrice}" + }, + "display_price.label.current_price": { + "defaultMessage": "{currentPrice}" + }, + "display_price.label.current_price_with_range": { + "defaultMessage": "From {currentPrice}" + }, + "display_price.label.list_price": { + "defaultMessage": "{listPrice}" }, "drawer_menu.button.account_details": { "defaultMessage": "Account Details" @@ -996,9 +1011,6 @@ "payment_selection.tooltip.secure_payment": { "defaultMessage": "This is a secure SSL encrypted payment." }, - "price_display.label.from": { - "defaultMessage": "From" - }, "price_per_item.label.each": { "defaultMessage": "ea", "description": "Abbreviated 'each', follows price per item, like $10/ea" From 47552a71f80f6c3363c0622f280bcba272a11aa2 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 13 May 2024 16:52:56 -0700 Subject: [PATCH 33/45] fix template --- .../assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs index 93366bcdce..6682d1678c 100644 --- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs +++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs @@ -57,7 +57,9 @@ const Home = () => { const {data: productSearchResult, isLoading} = useProductSearch({ parameters: { refine: [`cgid=${HOME_SHOP_PRODUCTS_CATEGORY_ID}`, 'htype=master'], + expand: ['promotions', 'variations', 'prices', 'images', 'availability'], perPricebook: true, + allVariationProperties: true, limit: HOME_SHOP_PRODUCTS_LIMIT } }) From 9882e1a62050fd176af7dfc04de29c0d29b24b01 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Mon, 13 May 2024 16:56:33 -0700 Subject: [PATCH 34/45] tweak proptype --- .../app/components/display-price/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 75dd7dbabc..0a166e651f 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -237,7 +237,7 @@ DisplayPrice.propTypes = { maxPrice: PropTypes.number, tieredPrice: PropTypes.number }), - currency: PropTypes.string + currency: PropTypes.string.isRequired } export default DisplayPrice From 06f8202b8cfb1fc7c0f0d9244cf862b4dd6e0c31 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 14 May 2024 09:44:45 -0700 Subject: [PATCH 35/45] let bold font for PLP --- .../app/components/display-price/index.jsx | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 0a166e651f..3ba35c4108 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -61,11 +61,7 @@ const DisplayPrice = ({priceData, currency}) => { priceData if (isASet) { - return hasRepresentedProduct ? ( - - ) : ( - - ) + return } if (isMaster) { if (isRange) { @@ -84,15 +80,7 @@ const DisplayPrice = ({priceData, currency}) => { ) } else { - // bold front on PDP, normal font on PLP - return hasRepresentedProduct ? ( - - ) : ( + return ( ) } @@ -105,11 +93,7 @@ const DisplayPrice = ({priceData, currency}) => { ) } else { - return hasRepresentedProduct ? ( - - ) : ( - - ) + return } } } @@ -121,7 +105,7 @@ const DisplayPrice = ({priceData, currency}) => { {listPrice && } ) : ( - + )}
) From 063a7f5d011333216f0d4aff27fb9ac17f46fd76 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 14 May 2024 11:17:42 -0700 Subject: [PATCH 36/45] optimizing display price logic --- .../app/components/display-price/index.jsx | 62 +++++-------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 3ba35c4108..328bf6fbe2 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -59,56 +59,28 @@ const msg = defineMessages({ const DisplayPrice = ({priceData, currency}) => { const {listPrice, currentPrice, isASet, isMaster, isOnSale, isRange, hasRepresentedProduct} = priceData + const renderCurrentPrice = (isRange) => ( + + ) + + const renderListPrice = (isRange) => + listPrice && + + const renderPriceSet = (isRange) => ( + <> + {renderCurrentPrice(isRange)} {isOnSale && renderListPrice(isRange)} + + ) if (isASet) { - return + return renderCurrentPrice(true) } + if (isMaster) { - if (isRange) { - if (isOnSale) { - return ( - <> - {' '} - {listPrice && ( - - )} - - ) - } else { - return ( - - ) - } - } else { - if (isOnSale) { - return ( - <> - {' '} - - - ) - } else { - return - } - } + return renderPriceSet(isRange) } - return ( - - {isOnSale ? ( - <> - {' '} - {listPrice && } - - ) : ( - - )} - - ) + + return {renderPriceSet(false)} } /** * @private From 59b33abc6409f25074bfa7ffcf497d5deb13a923 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 14 May 2024 12:03:17 -0700 Subject: [PATCH 37/45] edit jsx --- .../app/components/display-price/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 328bf6fbe2..3c66a7d131 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -83,7 +83,6 @@ const DisplayPrice = ({priceData, currency}) => { return {renderPriceSet(false)} } /** - * @private * Component that displays current price of a product with a11y * @param price - price of the product * @param as - an HTML tag or component to be rendered as From 8dac001b9d7113fc341664e6ee2a520aa7ee9972 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 14 May 2024 13:09:03 -0700 Subject: [PATCH 38/45] PR feedback --- .../app/components/display-price/index.jsx | 24 ++++++------------- .../app/utils/product-utils.js | 22 ++++++++++++++--- .../app/utils/utils.js | 17 ------------- 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index 3c66a7d131..a7d3fe2a67 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -12,18 +12,10 @@ import {defineMessages, useIntl} from 'react-intl' const msg = defineMessages({ // price display - currentPrice: { - id: 'display_price.label.current_price', - defaultMessage: '{currentPrice}' - }, currentPriceWithRange: { id: 'display_price.label.current_price_with_range', defaultMessage: 'From {currentPrice}' }, - listPrice: { - id: 'display_price.label.list_price', - defaultMessage: '{listPrice}' - }, // aria-label ariaLabelCurrentPrice: { id: 'display_price.assistive_msg.current_price', @@ -112,13 +104,12 @@ const CurrentPrice = ({price, as, isRange = false, currency, ...extraProps}) => ) : ( - {intl.formatMessage(msg.currentPrice, { - currentPrice: currentPriceText - })} + {currentPriceText} ) } @@ -155,24 +146,22 @@ const ListPrice = ({price, isRange = false, as = 's', currency, ...extraProps}) })} color="gray.600" > - {intl.formatMessage(msg.listPrice, { - listPrice: listPriceText - })} + {listPriceText}
) : ( - {intl.formatMessage(msg.listPrice, { - listPrice: listPriceText - })} + {listPriceText} ) } + ListPrice.propTypes = { price: PropTypes.number.isRequired, currency: PropTypes.string.isRequired, @@ -180,6 +169,7 @@ ListPrice.propTypes = { isRange: PropTypes.bool, extraProps: PropTypes.object } + DisplayPrice.propTypes = { priceData: PropTypes.shape({ currentPrice: PropTypes.number.isRequired, diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index 2869b3a7d2..c3a842650e 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -1,12 +1,10 @@ /* - * Copyright (c) 2023, Salesforce, Inc. + * Copyright (c) 2024, 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 {getSmallestValByProperty} from '@salesforce/retail-react-app/app/utils/utils' - /** * Get the human-friendly version of the variation values that users have selected. * Useful for displaying these values in the UI. @@ -119,3 +117,21 @@ export const getPriceData = (product, opts = {}) => { maxPrice: product?.priceMax || maxTieredPrice } } + +/** + * @private + * Find the smallest value by key from a given array + * @param arr + * @param key + */ +const getSmallestValByProperty = (arr, key) => { + if (!arr || !arr.length) return undefined + if (!key) { + throw new Error('Please specify a key.') + } + const vals = arr + .map((item) => item[key]) + .filter(Boolean) + .filter(Number) + return vals.length ? Math.min(...vals) : undefined +} diff --git a/packages/template-retail-react-app/app/utils/utils.js b/packages/template-retail-react-app/app/utils/utils.js index 21dc9ef68a..ce30d449d2 100644 --- a/packages/template-retail-react-app/app/utils/utils.js +++ b/packages/template-retail-react-app/app/utils/utils.js @@ -199,20 +199,3 @@ export const mergeMatchedItems = (arr1 = [], arr2 = []) => { * @return {boolean} */ export const isHydrated = () => typeof window !== 'undefined' && !window.__HYDRATING__ - -/** - * Find the smallest value by key from a given array - * @param arr - * @param key - */ -export const getSmallestValByProperty = (arr, key) => { - if (!arr || !arr.length) return undefined - if (!key) { - throw new Error('Please specify a key.') - } - const vals = arr - .map((item) => item[key]) - .filter(Boolean) - .filter(Number) - return vals.length ? Math.min(...vals) : undefined -} From 513707963fc242e726bf93cbda31b38f07559b96 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 14 May 2024 13:30:57 -0700 Subject: [PATCH 39/45] lint --- .../template-retail-react-app/app/components/_app/index.jsx | 1 - .../app/components/display-price/index.jsx | 3 +-- .../app/components/product-view/index.jsx | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/template-retail-react-app/app/components/_app/index.jsx b/packages/template-retail-react-app/app/components/_app/index.jsx index d06c54c1d3..708e707057 100644 --- a/packages/template-retail-react-app/app/components/_app/index.jsx +++ b/packages/template-retail-react-app/app/components/_app/index.jsx @@ -74,7 +74,6 @@ import { import Seo from '@salesforce/retail-react-app/app/components/seo' import {Helmet} from 'react-helmet' -import {Text} from '@chakra-ui/react' const PlaceholderComponent = () => (
diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index a7d3fe2a67..aa21aeae16 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -49,8 +49,7 @@ const msg = defineMessages({ * @param currency - currency */ const DisplayPrice = ({priceData, currency}) => { - const {listPrice, currentPrice, isASet, isMaster, isOnSale, isRange, hasRepresentedProduct} = - priceData + const {listPrice, currentPrice, isASet, isMaster, isOnSale, isRange} = priceData const renderCurrentPrice = (isRange) => ( ) diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index 0157814bf2..e6838e8950 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -132,7 +132,6 @@ const ProductView = forwardRef( const priceData = useMemo(() => { return getPriceData(product, {quantity}) }, [product, quantity]) - console.log('priceData', priceData) const canAddToWishlist = !isProductLoading const isProductASet = product?.type.set const errorContainerRef = useRef(null) From c866572ee96f440820e357fdd4c5bd4bf2f60b99 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 14 May 2024 13:32:20 -0700 Subject: [PATCH 40/45] fix test --- .../app/utils/utils.test.js | 43 +------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/packages/template-retail-react-app/app/utils/utils.test.js b/packages/template-retail-react-app/app/utils/utils.test.js index f9388477b7..c6729b1e72 100644 --- a/packages/template-retail-react-app/app/utils/utils.test.js +++ b/packages/template-retail-react-app/app/utils/utils.test.js @@ -6,11 +6,7 @@ */ import * as utils from '@salesforce/retail-react-app/app/utils/utils' import EventEmitter from 'events' -import { - flatten, - getSmallestValByProperty, - shallowEquals -} from '@salesforce/retail-react-app/app/utils/utils' +import {flatten, shallowEquals} from '@salesforce/retail-react-app/app/utils/utils' afterEach(() => { jest.clearAllMocks() @@ -184,40 +180,3 @@ describe('keysToCamel', () => { }) }) }) - -describe('getSmallestValByKey', function () { - test('should return the smallest value by key', () => { - const data = [ - { - name: 'Product 1', - price: 10 - }, - { - name: 'Product 2', - price: 9 - } - ] - const val = getSmallestValByProperty(data, 'price') - expect(val).toBe(9) - }) - test('should undefined if array is not passed in', () => { - const data = { - name: 'Cowl neck top' - } - const val = getSmallestValByProperty(data, 'price') - expect(val).toBeUndefined() - }) - test('should throw an error if key name is undefined', () => { - const data = [ - { - name: 'Product 1', - price: 10 - }, - { - name: 'Product 2', - price: 9 - } - ] - expect(() => getSmallestValByProperty(data)).toThrow() - }) -}) From 8dc7e96c6dfc395d46d18f8f5bd1aff65a17832f Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Tue, 14 May 2024 14:26:36 -0700 Subject: [PATCH 41/45] build translations --- .../static/translations/compiled/en-GB.json | 12 -------- .../static/translations/compiled/en-US.json | 12 -------- .../static/translations/compiled/en-XA.json | 28 ------------------- .../translations/en-GB.json | 6 ---- .../translations/en-US.json | 6 ---- 5 files changed, 64 deletions(-) diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json index 94a34215db..ba89cd9507 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json @@ -1045,12 +1045,6 @@ "value": "listPrice" } ], - "display_price.label.current_price": [ - { - "type": 1, - "value": "currentPrice" - } - ], "display_price.label.current_price_with_range": [ { "type": 0, @@ -1061,12 +1055,6 @@ "value": "currentPrice" } ], - "display_price.label.list_price": [ - { - "type": 1, - "value": "listPrice" - } - ], "drawer_menu.button.account_details": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json index 94a34215db..ba89cd9507 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json @@ -1045,12 +1045,6 @@ "value": "listPrice" } ], - "display_price.label.current_price": [ - { - "type": 1, - "value": "currentPrice" - } - ], "display_price.label.current_price_with_range": [ { "type": 0, @@ -1061,12 +1055,6 @@ "value": "currentPrice" } ], - "display_price.label.list_price": [ - { - "type": 1, - "value": "listPrice" - } - ], "drawer_menu.button.account_details": [ { "type": 0, diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json index 4d1571e945..0ec844414d 100644 --- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json +++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json @@ -2157,20 +2157,6 @@ "value": "]" } ], - "display_price.label.current_price": [ - { - "type": 0, - "value": "[" - }, - { - "type": 1, - "value": "currentPrice" - }, - { - "type": 0, - "value": "]" - } - ], "display_price.label.current_price_with_range": [ { "type": 0, @@ -2189,20 +2175,6 @@ "value": "]" } ], - "display_price.label.list_price": [ - { - "type": 0, - "value": "[" - }, - { - "type": 1, - "value": "listPrice" - }, - { - "type": 0, - "value": "]" - } - ], "drawer_menu.button.account_details": [ { "type": 0, diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json index 041a88a347..ce74a9ce01 100644 --- a/packages/template-retail-react-app/translations/en-GB.json +++ b/packages/template-retail-react-app/translations/en-GB.json @@ -420,15 +420,9 @@ "display_price.assistive_msg.strikethrough_price_with_range": { "defaultMessage": "From original price {listPrice}" }, - "display_price.label.current_price": { - "defaultMessage": "{currentPrice}" - }, "display_price.label.current_price_with_range": { "defaultMessage": "From {currentPrice}" }, - "display_price.label.list_price": { - "defaultMessage": "{listPrice}" - }, "drawer_menu.button.account_details": { "defaultMessage": "Account Details" }, diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json index 041a88a347..ce74a9ce01 100644 --- a/packages/template-retail-react-app/translations/en-US.json +++ b/packages/template-retail-react-app/translations/en-US.json @@ -420,15 +420,9 @@ "display_price.assistive_msg.strikethrough_price_with_range": { "defaultMessage": "From original price {listPrice}" }, - "display_price.label.current_price": { - "defaultMessage": "{currentPrice}" - }, "display_price.label.current_price_with_range": { "defaultMessage": "From {currentPrice}" }, - "display_price.label.list_price": { - "defaultMessage": "{listPrice}" - }, "drawer_menu.button.account_details": { "defaultMessage": "Account Details" }, From d5c94ffc711ed9913b63520c76b6c8e98cd4464e Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Wed, 15 May 2024 14:55:51 -0700 Subject: [PATCH 42/45] PR feedback --- .../app/components/display-price/index.jsx | 2 +- .../app/components/display-price/index.test.js | 3 +-- .../app/components/product-view/index.jsx | 1 - packages/template-retail-react-app/app/utils/product-utils.js | 2 -- .../template-retail-react-app/app/utils/product-utils.test.js | 4 ---- 5 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index aa21aeae16..b9d7c76bc0 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -177,7 +177,6 @@ DisplayPrice.propTypes = { isASet: PropTypes.bool, isMaster: PropTypes.bool, isRange: PropTypes.bool, - hasRepresentedProduct: PropTypes.bool, maxPrice: PropTypes.number, tieredPrice: PropTypes.number }), @@ -185,3 +184,4 @@ DisplayPrice.propTypes = { } export default DisplayPrice +export {ListPrice, CurrentPrice, msg} diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index 0926448bf3..37a6ce62d5 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -16,8 +16,7 @@ describe('DisplayPrice', function () { isASet: false, isOnSale: true, isMaster: true, - isRange: true, - hasRepresentedProduct: false + isRange: true } test('should render without error', () => { renderWithProviders() diff --git a/packages/template-retail-react-app/app/components/product-view/index.jsx b/packages/template-retail-react-app/app/components/product-view/index.jsx index e6838e8950..11abe6ba8c 100644 --- a/packages/template-retail-react-app/app/components/product-view/index.jsx +++ b/packages/template-retail-react-app/app/components/product-view/index.jsx @@ -71,7 +71,6 @@ ProductViewHeader.propTypes = { isASet: PropTypes.bool, isMaster: PropTypes.bool, isRange: PropTypes.bool, - hasRepresentedProduct: PropTypes.bool, maxPrice: PropTypes.number, tieredPrice: PropTypes.number }) diff --git a/packages/template-retail-react-app/app/utils/product-utils.js b/packages/template-retail-react-app/app/utils/product-utils.js index c3a842650e..44a456601c 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.js +++ b/packages/template-retail-react-app/app/utils/product-utils.js @@ -47,7 +47,6 @@ export const getPriceData = (product, opts = {}) => { const {quantity = 1} = opts const isASet = product?.hitType === 'set' || !!product?.type?.set const isMaster = product?.hitType === 'master' || !!product?.type?.master - const hasRepresentedProduct = !!product?.representedProduct?.id let currentPrice let variantWithLowestPrice // grab the variant that has the lowest price (including promotional price) @@ -111,7 +110,6 @@ export const getPriceData = (product, opts = {}) => { // For a master product, when it has more than 2 variants, we use the lowest priced variant, so it is considered a range price // but for master that has one variant, it is not considered range isRange: (isMaster && product?.variants?.length > 1) || isASet || false, - hasRepresentedProduct, // priceMax is for product set tieredPrice: closestTieredPrice?.price, maxPrice: product?.priceMax || maxTieredPrice diff --git a/packages/template-retail-react-app/app/utils/product-utils.test.js b/packages/template-retail-react-app/app/utils/product-utils.test.js index c4c1bad398..077af750a4 100644 --- a/packages/template-retail-react-app/app/utils/product-utils.test.js +++ b/packages/template-retail-react-app/app/utils/product-utils.test.js @@ -71,7 +71,6 @@ describe('getPriceData', function () { isASet: false, isMaster: true, isRange: true, - hasRepresentedProduct: true, tieredPrice: 223.99, maxPrice: 223.99 }) @@ -86,7 +85,6 @@ describe('getPriceData', function () { isASet: false, isMaster: true, isRange: false, - hasRepresentedProduct: true, tieredPrice: 320, maxPrice: 320 }) @@ -101,7 +99,6 @@ describe('getPriceData', function () { isASet: true, isMaster: false, isRange: true, - hasRepresentedProduct: true, tieredPrice: undefined, maxPrice: 71.03 }) @@ -116,7 +113,6 @@ describe('getPriceData', function () { isASet: false, isMaster: false, isRange: false, - hasRepresentedProduct: true, tieredPrice: 67.99, maxPrice: 67.99 }) From 5a9a04adddbc902296529640b2ac4edf5174b722 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Wed, 15 May 2024 16:06:49 -0700 Subject: [PATCH 43/45] separate pricing components into its own files --- .../display-price/current-price.jsx | 61 +++++++++ .../app/components/display-price/index.jsx | 126 +----------------- .../components/display-price/index.test.js | 36 ++++- .../components/display-price/list-price.jsx | 62 +++++++++ .../app/components/display-price/messages.js | 28 ++++ 5 files changed, 189 insertions(+), 124 deletions(-) create mode 100644 packages/template-retail-react-app/app/components/display-price/current-price.jsx create mode 100644 packages/template-retail-react-app/app/components/display-price/list-price.jsx create mode 100644 packages/template-retail-react-app/app/components/display-price/messages.js diff --git a/packages/template-retail-react-app/app/components/display-price/current-price.jsx b/packages/template-retail-react-app/app/components/display-price/current-price.jsx new file mode 100644 index 0000000000..31127c5fd8 --- /dev/null +++ b/packages/template-retail-react-app/app/components/display-price/current-price.jsx @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024, 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 {Text} from '@salesforce/retail-react-app/app/components/shared/ui' +import {useIntl} from 'react-intl' +import msg from './messages' + +/** + * Component that displays current price of a product with a11y + * @param price - price of the product + * @param as - an HTML tag or component to be rendered as + * @param isRange - show price as range or not + * @param currency - currency to show the price in + * @param extraProps - extra props to be passed into Text Component + * @returns {JSX.Element} + */ +const CurrentPrice = ({price, as, isRange = false, currency, ...extraProps}) => { + const intl = useIntl() + const currentPriceText = intl.formatNumber(price, { + style: 'currency', + currency + }) + return isRange ? ( + + {intl.formatMessage(msg.currentPriceWithRange, { + currentPrice: currentPriceText + })} + + ) : ( + + {currentPriceText} + + ) +} +CurrentPrice.propTypes = { + price: PropTypes.number.isRequired, + currency: PropTypes.string.isRequired, + as: PropTypes.string, + isRange: PropTypes.bool, + extraProps: PropTypes.object +} + +export default CurrentPrice diff --git a/packages/template-retail-react-app/app/components/display-price/index.jsx b/packages/template-retail-react-app/app/components/display-price/index.jsx index b9d7c76bc0..39fe65b3c4 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.jsx +++ b/packages/template-retail-react-app/app/components/display-price/index.jsx @@ -7,33 +7,9 @@ import React from 'react' import PropTypes from 'prop-types' -import {Box, Text} from '@salesforce/retail-react-app/app/components/shared/ui' -import {defineMessages, useIntl} from 'react-intl' - -const msg = defineMessages({ - // price display - currentPriceWithRange: { - id: 'display_price.label.current_price_with_range', - defaultMessage: 'From {currentPrice}' - }, - // aria-label - ariaLabelCurrentPrice: { - id: 'display_price.assistive_msg.current_price', - defaultMessage: `current price {currentPrice}` - }, - ariaLabelCurrentPriceWithRange: { - id: 'display_price.assistive_msg.current_price_with_range', - defaultMessage: `From current price {currentPrice}` - }, - ariaLabelListPrice: { - id: 'display_price.assistive_msg.strikethrough_price', - defaultMessage: `original price {listPrice}` - }, - ariaLabelListPriceWithRange: { - id: 'display_price.assistive_msg.strikethrough_price_with_range', - defaultMessage: `From original price {listPrice}` - } -}) +import {Box} from '@salesforce/retail-react-app/app/components/shared/ui' +import CurrentPrice from '@salesforce/retail-react-app/app/components/display-price/current-price' +import ListPrice from '@salesforce/retail-react-app/app/components/display-price/list-price' /** * @param priceData - price info extracted from a product @@ -73,101 +49,6 @@ const DisplayPrice = ({priceData, currency}) => { return {renderPriceSet(false)} } -/** - * Component that displays current price of a product with a11y - * @param price - price of the product - * @param as - an HTML tag or component to be rendered as - * @param isRange - show price as range or not - * @param currency - currency to show the price in - * @param extraProps - extra props to be passed into Text Component - * @returns {JSX.Element} - */ -const CurrentPrice = ({price, as, isRange = false, currency, ...extraProps}) => { - const intl = useIntl() - const currentPriceText = intl.formatNumber(price, { - style: 'currency', - currency - }) - return isRange ? ( - - {intl.formatMessage(msg.currentPriceWithRange, { - currentPrice: currentPriceText - })} - - ) : ( - - {currentPriceText} - - ) -} -CurrentPrice.propTypes = { - price: PropTypes.number.isRequired, - currency: PropTypes.string.isRequired, - as: PropTypes.string, - isRange: PropTypes.bool, - extraProps: PropTypes.object -} - -/** - * Component that displays list price of a product with a11y - * @param price - price of the product - * @param as - an HTML tag or component to be rendered as - * @param isRange - show price as range or not - * @param props - extra props to be passed into Text Component - * @param extraProps - extra props to be passed into Text Component - * @returns {JSX.Element} - */ -const ListPrice = ({price, isRange = false, as = 's', currency, ...extraProps}) => { - const intl = useIntl() - const listPriceText = intl.formatNumber(price, { - style: 'currency', - currency - }) - - return isRange ? ( - - {listPriceText} - - ) : ( - - {listPriceText} - - ) -} - -ListPrice.propTypes = { - price: PropTypes.number.isRequired, - currency: PropTypes.string.isRequired, - as: PropTypes.string, - isRange: PropTypes.bool, - extraProps: PropTypes.object -} DisplayPrice.propTypes = { priceData: PropTypes.shape({ @@ -184,4 +65,3 @@ DisplayPrice.propTypes = { } export default DisplayPrice -export {ListPrice, CurrentPrice, msg} diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index 37a6ce62d5..987e7417af 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -8,6 +8,8 @@ import React from 'react' import {screen, within} from '@testing-library/react' import DisplayPrice from '@salesforce/retail-react-app/app/components/display-price/index' import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils' +import CurrentPrice from '@salesforce/retail-react-app/app/components/display-price/current-price' +import ListPrice from '@salesforce/retail-react-app/app/components/display-price/list-price' describe('DisplayPrice', function () { const data = { @@ -28,7 +30,6 @@ describe('DisplayPrice', function () { const {container} = renderWithProviders() const currentPriceTag = container.querySelectorAll('b') const strikethroughPriceTag = container.querySelectorAll('s') - // From and salePrice are in two separate b tags expect(within(currentPriceTag[0]).getByText(/£90\.00/i)).toBeDefined() expect(within(strikethroughPriceTag[0]).getByText(/£100\.00/i)).toBeDefined() expect(currentPriceTag).toHaveLength(1) @@ -46,3 +47,36 @@ describe('DisplayPrice', function () { expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() }) }) + +describe('CurrentPrice', function () { + test('should render exact price with correct aria-label', () => { + const {container} = renderWithProviders() + expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() + expect(screen.getByLabelText(/current price £100\.00/i)).toBeInTheDocument() + }) + + test('should render range price', () => { + renderWithProviders() + expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() + expect(screen.getByText(/from/i)).toBeInTheDocument() + expect(screen.getByLabelText(/from current price £100\.00/i)).toBeInTheDocument() + }) +}) + +describe('ListPrice', function () { + test('should render strikethrough price with exact price in aria-label', () => { + const {container} = renderWithProviders() + const strikethroughPriceTag = container.querySelectorAll('s') + expect(screen.getByLabelText(/original price £100\.00/i)).toBeInTheDocument() + expect(within(strikethroughPriceTag[0]).getByText(/£100\.00/i)).toBeDefined() + }) + + test('should render strikethrough price with range price in aria-label', () => { + const {container} = renderWithProviders( + + ) + const strikethroughPriceTag = container.querySelectorAll('s') + expect(screen.getByLabelText(/from original price £100\.00/i)).toBeInTheDocument() + expect(within(strikethroughPriceTag[0]).getByText(/£100\.00/i)).toBeDefined() + }) +}) diff --git a/packages/template-retail-react-app/app/components/display-price/list-price.jsx b/packages/template-retail-react-app/app/components/display-price/list-price.jsx new file mode 100644 index 0000000000..893d7db1d8 --- /dev/null +++ b/packages/template-retail-react-app/app/components/display-price/list-price.jsx @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023, 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 {Text} from '@salesforce/retail-react-app/app/components/shared/ui' +import {useIntl} from 'react-intl' +import msg from './messages' +/** + * Component that displays list price of a product with a11y + * @param price - price of the product + * @param as - an HTML tag or component to be rendered as + * @param isRange - show price as range or not + * @param props - extra props to be passed into Text Component + * @param extraProps - extra props to be passed into Text Component + * @returns {JSX.Element} + */ +const ListPrice = ({price, isRange = false, as = 's', currency, ...extraProps}) => { + const intl = useIntl() + const listPriceText = intl.formatNumber(price, { + style: 'currency', + currency + }) + + return isRange ? ( + + {listPriceText} + + ) : ( + + {listPriceText} + + ) +} + +ListPrice.propTypes = { + price: PropTypes.number.isRequired, + currency: PropTypes.string.isRequired, + as: PropTypes.string, + isRange: PropTypes.bool, + extraProps: PropTypes.object +} + +export default ListPrice diff --git a/packages/template-retail-react-app/app/components/display-price/messages.js b/packages/template-retail-react-app/app/components/display-price/messages.js new file mode 100644 index 0000000000..a3700f74f0 --- /dev/null +++ b/packages/template-retail-react-app/app/components/display-price/messages.js @@ -0,0 +1,28 @@ +import {defineMessages} from 'react-intl' + +const messages = defineMessages({ + // price display + currentPriceWithRange: { + id: 'display_price.label.current_price_with_range', + defaultMessage: 'From {currentPrice}' + }, + // aria-label + ariaLabelCurrentPrice: { + id: 'display_price.assistive_msg.current_price', + defaultMessage: `current price {currentPrice}` + }, + ariaLabelCurrentPriceWithRange: { + id: 'display_price.assistive_msg.current_price_with_range', + defaultMessage: `From current price {currentPrice}` + }, + ariaLabelListPrice: { + id: 'display_price.assistive_msg.strikethrough_price', + defaultMessage: `original price {listPrice}` + }, + ariaLabelListPriceWithRange: { + id: 'display_price.assistive_msg.strikethrough_price_with_range', + defaultMessage: `From original price {listPrice}` + } +}) + +export default messages From e43dccbd0a92770ef09abfb329fad9c760573f35 Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Wed, 15 May 2024 16:28:21 -0700 Subject: [PATCH 44/45] lint --- .../app/components/display-price/current-price.jsx | 2 +- .../app/components/display-price/index.test.js | 2 +- .../app/components/display-price/list-price.jsx | 4 ++-- .../app/components/display-price/messages.js | 7 +++++++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/template-retail-react-app/app/components/display-price/current-price.jsx b/packages/template-retail-react-app/app/components/display-price/current-price.jsx index 31127c5fd8..2db3db73d3 100644 --- a/packages/template-retail-react-app/app/components/display-price/current-price.jsx +++ b/packages/template-retail-react-app/app/components/display-price/current-price.jsx @@ -9,7 +9,7 @@ import React from 'react' import PropTypes from 'prop-types' import {Text} from '@salesforce/retail-react-app/app/components/shared/ui' import {useIntl} from 'react-intl' -import msg from './messages' +import msg from '@salesforce/retail-react-app/app/components/display-price/messages' /** * Component that displays current price of a product with a11y diff --git a/packages/template-retail-react-app/app/components/display-price/index.test.js b/packages/template-retail-react-app/app/components/display-price/index.test.js index 987e7417af..d0a6fe057f 100644 --- a/packages/template-retail-react-app/app/components/display-price/index.test.js +++ b/packages/template-retail-react-app/app/components/display-price/index.test.js @@ -50,7 +50,7 @@ describe('DisplayPrice', function () { describe('CurrentPrice', function () { test('should render exact price with correct aria-label', () => { - const {container} = renderWithProviders() + renderWithProviders() expect(screen.getByText(/£100\.00/i)).toBeInTheDocument() expect(screen.getByLabelText(/current price £100\.00/i)).toBeInTheDocument() }) diff --git a/packages/template-retail-react-app/app/components/display-price/list-price.jsx b/packages/template-retail-react-app/app/components/display-price/list-price.jsx index 893d7db1d8..8aa443d69b 100644 --- a/packages/template-retail-react-app/app/components/display-price/list-price.jsx +++ b/packages/template-retail-react-app/app/components/display-price/list-price.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Salesforce, Inc. + * Copyright (c) 2024, 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 @@ -9,7 +9,7 @@ import React from 'react' import PropTypes from 'prop-types' import {Text} from '@salesforce/retail-react-app/app/components/shared/ui' import {useIntl} from 'react-intl' -import msg from './messages' +import msg from '@salesforce/retail-react-app/app/components/display-price/messages' /** * Component that displays list price of a product with a11y * @param price - price of the product diff --git a/packages/template-retail-react-app/app/components/display-price/messages.js b/packages/template-retail-react-app/app/components/display-price/messages.js index a3700f74f0..8b9ae9e038 100644 --- a/packages/template-retail-react-app/app/components/display-price/messages.js +++ b/packages/template-retail-react-app/app/components/display-price/messages.js @@ -1,3 +1,10 @@ +/* + * Copyright (c) 2023, 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 {defineMessages} from 'react-intl' const messages = defineMessages({ From f872153dec0c4bff5f4e194b11a54f31e2cea8fb Mon Sep 17 00:00:00 2001 From: Alex Vuong Date: Wed, 15 May 2024 17:44:24 -0700 Subject: [PATCH 45/45] fix flaky test --- .../index.test.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/template-retail-react-app/app/components/unavailable-product-confirmation-modal/index.test.js b/packages/template-retail-react-app/app/components/unavailable-product-confirmation-modal/index.test.js index 3488211698..206073056a 100644 --- a/packages/template-retail-react-app/app/components/unavailable-product-confirmation-modal/index.test.js +++ b/packages/template-retail-react-app/app/components/unavailable-product-confirmation-modal/index.test.js @@ -344,7 +344,7 @@ describe('UnavailableProductConfirmationModal', () => { test('opens confirmation modal when unavailable products are found', async () => { const mockProductIds = ['701642889899M', '701642889830M'] const mockFunc = jest.fn() - const {getByRole, user} = renderWithProviders( + const {getByText, queryByText, queryByRole, user} = renderWithProviders( { ) await waitFor(async () => { - const removeBtn = getByRole('button') - expect(removeBtn).toBeInTheDocument() - await user.click(removeBtn) - expect(mockFunc).toHaveBeenCalled() - expect(removeBtn).not.toBeInTheDocument() + expect(getByText(/Items Unavailable/i)).toBeInTheDocument() }) + const removeBtn = queryByRole('button') + + expect(removeBtn).toBeInTheDocument() + await user.click(removeBtn) + expect(mockFunc).toHaveBeenCalled() + await waitFor(async () => { + expect(queryByText(/Items Unavailable/i)).not.toBeInTheDocument() + }) + expect(removeBtn).not.toBeInTheDocument() }) })