-
Notifications
You must be signed in to change notification settings - Fork 352
Version Packages (canary)
#2810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
github-actions
wants to merge
1
commit into
canary
Choose a base branch
from
changeset-release/canary
base: canary
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
1f8db9b to
33028d2
Compare
33028d2 to
b582378
Compare
b582378 to
119b34d
Compare
119b34d to
99248b4
Compare
99248b4 to
f65fd8a
Compare
f65fd8a to
c1d0d57
Compare
c1d0d57 to
49237e6
Compare
b205605 to
c25ef95
Compare
c25ef95 to
b73be45
Compare
b73be45 to
c3f19e8
Compare
c3f19e8 to
cb28534
Compare
cb28534 to
fe828df
Compare
fe828df to
2ef3798
Compare
2ef3798 to
62ede6f
Compare
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to canary, this PR will be updated.
Releases
@bigcommerce/catalyst-core@1.5.0
Minor Changes
#2815
52ee85eThanks @jamesqquick! - Add default optional text to form input labels for inputs that are not required.Migration
The new required props are optional, so they are backwards compatible. However, this does mean that the
(optional)text will now show up on fields that aren't explicitly marked as required by passing the required prop to the Label component.Patch Changes
#2827
49b1097Thanks @jorgemoya! - Filter out child cart items (items withparentEntityId) from cart and cart analytics to prevent duplicate line items when products have parent-child relationships, such as product bundles.Migration steps
Step 1: GraphQL Fragment Updates
The
parentEntityIdfield has been added to both physical and digital cart item fragments to identify child items.Update
core/app/[locale]/(default)/cart/page-data.ts:export const PhysicalItemFragment = graphql(` fragment PhysicalItemFragment on CartPhysicalItem { entityId quantity productEntityId variantEntityId + parentEntityId listPrice { currencyCode value } } `); export const DigitalItemFragment = graphql(` fragment DigitalItemFragment on CartDigitalItem { entityId quantity productEntityId variantEntityId + parentEntityId listPrice { currencyCode value } } `);Step 2: Cart Display Filtering
Cart line items are now filtered to exclude child items when displaying the cart.
Update
core/app/[locale]/(default)/cart/page.tsx:const lineItems = [ ...cart.lineItems.giftCertificates, ...cart.lineItems.physicalItems, ...cart.lineItems.digitalItems, - ]; + ].filter((item) => !('parentEntityId' in item) || !item.parentEntityId);Step 3: Analytics Data Filtering
Analytics data collection now only includes top-level items to prevent duplicate tracking.
Update
core/app/[locale]/(default)/cart/page.tsxin thegetAnalyticsDatafunction:Step 4: Styling Update
Cart subtitle text color has been updated for improved contrast.
Update
core/vibes/soul/sections/cart/client.tsx:#2811
b57bffaThanks @chanceaclark! - Fix pagination cursor persistence when changing sort order. Thebeforeandafterquery parameters are now cleared when the sort option changes, preventing stale pagination cursors from causing incorrect results or empty pages.#2818
74e4dd1Thanks @jordanarldt! - Disable product filters that are no longer available based on the selection.Migration steps
Step 1
Update the
facetsTransformerfunction incore/data-transformers/facets-transformer.tsto handle disabled filters:return allFacets.map((facet) => { const refinedFacet = refinedFacets.find((f) => f.displayName === facet.displayName); + if (refinedFacet == null) { + return null; + } + if (facet.__typename === 'CategorySearchFilter') { const refinedCategorySearchFilter = - refinedFacet?.__typename === 'CategorySearchFilter' ? refinedFacet : null; + refinedFacet.__typename === 'CategorySearchFilter' ? refinedFacet : null; return { type: 'toggle-group' as const, paramName: 'categoryIn', label: facet.displayName, defaultCollapsed: facet.isCollapsedByDefault, options: facet.categories.map((category) => { const refinedCategory = refinedCategorySearchFilter?.categories.find( (c) => c.entityId === category.entityId, ); const isSelected = filters.categoryEntityIds?.includes(category.entityId) === true; + const disabled = refinedCategory == null && !isSelected; + const productCountLabel = disabled ? '' : ` (${category.productCount})`; + const label = facet.displayProductCount + ? `${category.name}${productCountLabel}` + : category.name; return { - label: facet.displayProductCount - ? `${category.name} (${category.productCount})` - : category.name, + label, value: category.entityId.toString(), - disabled: refinedCategory == null && !isSelected, + disabled, }; }), }; } if (facet.__typename === 'BrandSearchFilter') { const refinedBrandSearchFilter = - refinedFacet?.__typename === 'BrandSearchFilter' ? refinedFacet : null; + refinedFacet.__typename === 'BrandSearchFilter' ? refinedFacet : null; return { type: 'toggle-group' as const, paramName: 'brand', label: facet.displayName, defaultCollapsed: facet.isCollapsedByDefault, options: facet.brands.map((brand) => { const refinedBrand = refinedBrandSearchFilter?.brands.find( (b) => b.entityId === brand.entityId, ); const isSelected = filters.brandEntityIds?.includes(brand.entityId) === true; + const disabled = refinedBrand == null && !isSelected; + const productCountLabel = disabled ? '' : ` (${brand.productCount})`; + const label = facet.displayProductCount + ? `${brand.name}${productCountLabel}` + : brand.name; return { - label: facet.displayProductCount ? `${brand.name} (${brand.productCount})` : brand.name, + label, value: brand.entityId.toString(), - disabled: refinedBrand == null && !isSelected, + disabled, }; }), }; } if (facet.__typename === 'ProductAttributeSearchFilter') { const refinedProductAttributeSearchFilter = - refinedFacet?.__typename === 'ProductAttributeSearchFilter' ? refinedFacet : null; + refinedFacet.__typename === 'ProductAttributeSearchFilter' ? refinedFacet : null; return { type: 'toggle-group' as const, paramName: `attr_${facet.filterKey}`, label: facet.displayName, defaultCollapsed: facet.isCollapsedByDefault, options: facet.attributes.map((attribute) => { const refinedAttribute = refinedProductAttributeSearchFilter?.attributes.find( (a) => a.value === attribute.value, ); const isSelected = filters.productAttributes?.some((attr) => attr.values.includes(attribute.value)) === true; + const disabled = refinedAttribute == null && !isSelected; + const productCountLabel = disabled ? '' : ` (${attribute.productCount})`; + const label = facet.displayProductCount + ? `${attribute.value}${productCountLabel}` + : attribute.value; + return { - label: facet.displayProductCount - ? `${attribute.value} (${attribute.productCount})` - : attribute.value, + label, value: attribute.value, - disabled: refinedAttribute == null && !isSelected, + disabled, }; }), }; } if (facet.__typename === 'RatingSearchFilter') { const refinedRatingSearchFilter = - refinedFacet?.__typename === 'RatingSearchFilter' ? refinedFacet : null; + refinedFacet.__typename === 'RatingSearchFilter' ? refinedFacet : null; const isSelected = filters.rating?.minRating != null; return { type: 'rating' as const, paramName: 'minRating', label: facet.displayName, disabled: refinedRatingSearchFilter == null && !isSelected, defaultCollapsed: facet.isCollapsedByDefault, }; } if (facet.__typename === 'PriceSearchFilter') { const refinedPriceSearchFilter = - refinedFacet?.__typename === 'PriceSearchFilter' ? refinedFacet : null; + refinedFacet.__typename === 'PriceSearchFilter' ? refinedFacet : null; const isSelected = filters.price?.minPrice != null || filters.price?.maxPrice != null; return { type: 'range' as const, minParamName: 'minPrice', maxParamName: 'maxPrice', label: facet.displayName, min: facet.selected?.minPrice ?? undefined, max: facet.selected?.maxPrice ?? undefined, disabled: refinedPriceSearchFilter == null && !isSelected, defaultCollapsed: facet.isCollapsedByDefault, }; } if (facet.freeShipping) { const refinedFreeShippingSearchFilter = - refinedFacet?.__typename === 'OtherSearchFilter' && refinedFacet.freeShipping + refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.freeShipping ? refinedFacet : null; const isSelected = filters.isFreeShipping === true; return { type: 'toggle-group' as const, paramName: `shipping`, label: t('freeShippingLabel'), defaultCollapsed: facet.isCollapsedByDefault, options: [ { label: t('freeShippingLabel'), value: 'free_shipping', disabled: refinedFreeShippingSearchFilter == null && !isSelected, }, ], }; } if (facet.isFeatured) { const refinedIsFeaturedSearchFilter = - refinedFacet?.__typename === 'OtherSearchFilter' && refinedFacet.isFeatured + refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.isFeatured ? refinedFacet : null; const isSelected = filters.isFeatured === true; return { type: 'toggle-group' as const, paramName: `isFeatured`, label: t('isFeaturedLabel'), defaultCollapsed: facet.isCollapsedByDefault, options: [ { label: t('isFeaturedLabel'), value: 'on', disabled: refinedIsFeaturedSearchFilter == null && !isSelected, }, ], }; } if (facet.isInStock) { const refinedIsInStockSearchFilter = - refinedFacet?.__typename === 'OtherSearchFilter' && refinedFacet.isInStock + refinedFacet.__typename === 'OtherSearchFilter' && refinedFacet.isInStock ? refinedFacet : null; const isSelected = filters.hideOutOfStock === true; return { type: 'toggle-group' as const, paramName: `stock`, label: t('inStockLabel'), defaultCollapsed: facet.isCollapsedByDefault, options: [ { label: t('inStockLabel'), value: 'in_stock', disabled: refinedIsInStockSearchFilter == null && !isSelected, }, ], }; } return null; });Step 2
Fix the disabled state CSS classes in
core/vibes/soul/form/toggle-group/index.tsx:<ToggleGroupPrimitive.Item aria-label={option.label} className={clsx( - 'data-disabled:pointer-events-none data-disabled:opacity-50 h-12 whitespace-nowrap rounded-full border px-4 font-body text-sm font-normal leading-normal transition-colors focus-visible:outline-0 focus-visible:ring-2', + 'h-12 whitespace-nowrap rounded-full border px-4 font-body text-sm font-normal leading-normal transition-colors focus-visible:outline-0 focus-visible:ring-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50', { light: 'border-[var(--toggle-group-light-border,hsl(var(--contrast-100)))] ring-[var(--toggle-group-light-focus,hsl(var(--primary)))] data-[state=on]:border-[var(--toggle-group-light-on-border,hsl(var(--foreground)))] data-[state=off]:bg-[var(--toggle-group-light-off-background,hsl(var(--background)))] data-[state=on]:bg-[var(--toggle-group-light-on-background,hsl(var(--foreground)))] data-[state=off]:text-[var(--toggle-group-light-off-text,hsl(var(--foreground)))] data-[state=on]:text-[var(--toggle-group-light-on-text,hsl(var(--background)))] data-[state=off]:hover:border-[var(--toggle-group-light-off-border-hover,hsl(var(--contrast-200)))] data-[state=off]:hover:bg-[var(--toggle-group-light-off-background-hover,hsl(var(--contrast-100)))]',Step 3
Update the FiltersPanel component in
core/vibes/soul/sections/products-list-section/filters-panel.tsximport { clsx } from 'clsx'; import { parseAsString, useQueryStates } from 'nuqs'; -import { Suspense, useOptimistic, useState, useTransition } from 'react'; +import { useOptimistic, useState, useTransition } from 'react'; import { Checkbox } from '@/vibes/soul/form/checkbox'; import { RangeInput } from '@/vibes/soul/form/range-input'; import { ToggleGroup } from '@/vibes/soul/form/toggle-group'; -import { Streamable, useStreamable } from '@/vibes/soul/lib/streamable'; +import { Stream, Streamable, useStreamable } from '@/vibes/soul/lib/streamable'; import { Accordion, AccordionItem } from '@/vibes/soul/primitives/accordion'; import { Button } from '@/vibes/soul/primitives/button'; import { CursorPaginationInfo } from '@/vibes/soul/primitives/cursor-pagination'; import { Rating } from '@/vibes/soul/primitives/rating'; import { Link } from '~/components/link'; import { getFilterParsers } from './filter-parsers';))} <Accordion - onValueChange={(items) => - setAccordionItems((prevItems) => - prevItems.map((prevItem) => ({ - ...prevItem, - expanded: items.includes(prevItem.value), - })), - ) - } + onValueChange={(items) => { + setExpandedItems(new Set(items)); + }} type="multiple" value={accordionItems.filter((item) => item.expanded).map((item) => item.value)} >#2813
ea9d633Thanks @jorgemoya! - Delete duplicate Select component.#2823
dcad856Thanks @jorgemoya! - Refactor DynamicForm actions to decouple fields and passwordComplexity from state, passing them as separate arguments instead. This reduces state payload size by removing fields from state objects and stripping options from fields before passing them to actions (options are only needed for rendering, not processing). All form actions now accept aDynamicFormActionArgsobject as the first parameter containing fields and optional passwordComplexity, followed by the previous state and formData.Migration steps
Step 1: Changes to DynamicForm component
The
DynamicFormcomponent and related utilities have been updated to support the new action signature pattern:core/vibes/soul/form/dynamic-form/index.tsx:DynamicFormActionArgs<F>interface that containsfieldsand optionalpasswordComplexityDynamicFormAction<F>type to acceptDynamicFormActionArgs<F>as the first parameterfieldsandpasswordComplexityfrom theStateinterfaceoptionsfrom fields before passing to actions (options are only needed for rendering)action.bind(null, { fields: fieldsWithoutOptions, passwordComplexity })core/vibes/soul/form/dynamic-form/utils.ts(new file):removeOptionsFromFields()utility function that strips theoptionsproperty from field definitions before passing them to actions, reducing the state payload sizeStep 2: Update DynamicForm action signatures
All form actions that use
DynamicFormmust be updated to acceptDynamicFormActionArgs<F>as the first parameter instead of including fields in the state.Update your form action function signature:
Step 2: Remove fields and passwordComplexity from state interfaces
Update state interfaces to remove fields and passwordComplexity properties:
interface State { lastResult: SubmissionResult | null; - fields: Array<Field | FieldGroup<Field>>; - passwordComplexity?: PasswordComplexitySettings | null; }Step 3: Update action implementations
Remove references to
prevState.fieldsandprevState.passwordComplexityin action implementations:const submission = parseWithZod(formData, { - schema: schema(prevState.fields, prevState.passwordComplexity), + schema: schema(fields, passwordComplexity), }); if (submission.status !== 'success') { return { lastResult: submission.reply(), - fields: prevState.fields, - passwordComplexity: prevState.passwordComplexity, }; }Step 4: Update action calls in components
For actions used with
AddressListSection, update the action signature to accept fields as the first parameter:Step 5: Update DynamicForm usage
No changes needed to
DynamicFormcomponent usage. The component automatically handles binding fields and passwordComplexity to actions. TheDynamicFormcomponent now:action.bind()Affected files
The following files were updated in this refactor:
core/vibes/soul/form/dynamic-form/index.tsx- AddedDynamicFormActionArgstype and updated action bindingcore/vibes/soul/form/dynamic-form/utils.ts- AddedremoveOptionsFromFieldsutility functioncore/app/[locale]/(default)/(auth)/register/_actions/register-customer.tscore/app/[locale]/(default)/account/addresses/_actions/address-action.tscore/app/[locale]/(default)/account/addresses/_actions/create-address.tscore/app/[locale]/(default)/account/addresses/_actions/update-address.tscore/app/[locale]/(default)/account/addresses/_actions/delete-address.tscore/app/[locale]/(default)/gift-certificates/purchase/_actions/add-to-cart.tsxcore/app/[locale]/(default)/webpages/[id]/contact/_actions/submit-contact-form.tscore/vibes/soul/sections/address-list-section/index.tsx#2816
b4b87a3Thanks @chanceaclark! - Add support for additional HTML attributes on script tags. The scripts transformer now extracts and passes through attributes likeasync,defer,crossorigin, anddata-*attributes from BigCommerce script tags to the C15T consent manager, ensuring scripts load with their intended behavior.#2817
d469078Thanks @jorgemoya! - Persist the checkbox product modifier since it can modify pricing and other product data. By persisting this and tracking in the url, this will trigger a product refetch when added or removed. Incidentally, now we manually control what fields are persisted, sinceoption.isVariantOptiondoesn't apply tocheckbox, additionally multi options modifiers that are not variant options can also modify price and other product data.Migration
Step 1
Update
product-options-transformer.tsto manually track persisted fields:Fields that persist and can affect product pricing when selected:
Step 2
Remove
isVariantOptionfrom GQL query since we no longer use it:Step 3
Update
product-detail-form.tsxto include separate handing of the checkbox field:Step 4
Update schema in
core/vibes/soul/sections/product-detail/schema.ts:#2820
a50fa6fThanks @jordanarldt! - Fix WishlistDetails page from exceeding GraphQL complexity limit, and fix wishlist e2e tests.Additionally, add the
requiredprop tocore/components/wishlist/modals/new.tsxandcore/components/wishlist/modals/rename.tsxMigration
Step 1: Update wishlist GraphQL fragments
In
core/components/wishlist/fragment.ts, replace theWishlistItemProductFragmentto use explicit fields instead ofProductCardFragment:Remove
ProductCardFragmentfrom all fragment dependencies in the same file.Step 2: Update product card transformer
In
core/data-transformers/product-card-transformer.ts:WishlistItemProductFragment:singleProductCardTransformerfunction signature to accept both fragment types:inventoryMessagefield:productCardTransformerfunction signature similarly:Step 3: Fix wishlist e2e tests
In
core/tests/ui/e2e/account/wishlists.spec.ts, update label selectors to use{ exact: true }for specificity:Update all locators for the wishlist name input selectors:
Step 4: Fix mobile wishlist e2e tests
In
core/tests/ui/e2e/account/wishlists.mobile.spec.ts, update translation calls to use namespace prefixes:Step 5: Add
requiredprop to wishlist modalsUpdate the modal forms to include the
requiredprop on the name input field:In
core/components/wishlist/modals/new.tsx:<Input {...getInputProps(fields.wishlistName, { type: 'text' })} defaultValue={defaultValue.current} errors={fields.wishlistName.errors} key={fields.wishlistName.id} label={nameLabel} onChange={(e) => { defaultValue.current = e.target.value; }} + required />In
core/components/wishlist/modals/rename.tsx:<Input {...getInputProps(fields.wishlistName, { type: 'text' })} defaultValue={defaultValue.current} errors={fields.wishlistName.errors} key={fields.wishlistName.id} label={nameLabel} onChange={(e) => { defaultValue.current = e.target.value; }} + required />#2814
fcb946eThanks @matthewvolk! - Shoppers will now see the store's actual password complexity requirements in the tooltip on the new customer registration form, preventing confusion and failed registration attempts. The schema() function in core/vibes/soul/form/dynamic-form/schema.ts now accepts an optional second parameter passwordComplexity to enable dynamic password validation. The DynamicForm, DynamicFormSection components and their associated server actions also accept an optional passwordComplexity prop that flows through to the schema. Action Required: If you have custom registration or password forms and want to use store-specific password complexity settings, fetch passwordComplexitySettings from the GraphQL API (under site.settings.customers.passwordComplexitySettings) and pass it to your DynamicFormSection component and maintain it in your server action's state. If you don't pass it, password validation defaults to: minimum 8 characters, at least one number, and at least one special character. Conflict Resolution: If merging into custom forms, ensure the passwordComplexity prop is threaded through: Page → DynamicFormSection → DynamicForm → useActionState → schema(). In server actions, add passwordComplexity?: Parameters[1] to your state type and include it in all return statements to maintain state consistency.#2821
e5a03f6Thanks @jordanarldt! - Fix data-disabled class selectors in UI componentsMigration
Updated Tailwind CSS class selectors from
data-disabled:todata-[disabled]:in the following components:vibes/soul/form/button-radio-group/index.tsxvibes/soul/form/card-radio-group/index.tsxvibes/soul/form/radio-group/index.tsxvibes/soul/form/rating-radio-group/index.tsxvibes/soul/form/swatch-radio-group/index.tsxvibes/soul/form/switch/index.tsxvibes/soul/primitives/dropdown-menu/index.tsxIf you have customized any of these components, update your class names:
This change ensures proper styling of disabled states using the correct Tailwind CSS data attribute syntax.
#2819
a1f1ed8Thanks @jamesqquick! - The login form input data will no longer reset on a failed login attempt.#2826
b5f460cThanks @bc-svc-local! - Update translations.#2829
8096cc5Thanks @jorgemoya! - Improve accessibility for price displays by adding screen reader announcements for original prices, sale prices, and price ranges. Visual price elements are hidden from assistive technologies usingaria-hidden="true"to prevent duplicate announcements, while visually hidden text provides context about pricing information.Migration steps
Step 1: Update Cart Price Display
Update
core/vibes/soul/sections/cart/client.tsxto add accessibility labels for sale prices:{lineItem.salePrice && lineItem.salePrice !== lineItem.price ? ( <span className="font-medium @xl:ml-auto"> - <span className="line-through">{lineItem.price}</span> {lineItem.salePrice} + <span className="sr-only">{t('originalPrice', { price: lineItem.price })}</span> + <span aria-hidden="true" className="line-through"> + {lineItem.price} + </span>{' '} + <span className="sr-only">{t('currentPrice', { price: lineItem.salePrice })}</span> + <span aria-hidden="true">{lineItem.salePrice}</span> </span> ) : ( <span className="font-medium @xl:ml-auto">{lineItem.price}</span> )}Step 2: Update PriceLabel Component
Update
core/vibes/soul/primitives/price-label/index.tsxto add accessibility improvements for sale prices and price ranges:import { clsx } from 'clsx'; + import { useTranslations } from 'next-intl'; export function PriceLabel({ className, colorScheme = 'light', price }: Props) { + const t = useTranslations('Components.Price'); if (typeof price === 'string') { return ( ... ); } switch (price.type) { case 'range': return ( <span ...> - {price.minValue} - – - {price.maxValue} + <span className="sr-only"> + {t('range', { minValue: price.minValue, maxValue: price.maxValue })} + </span> + <span aria-hidden="true"> + {price.minValue} - {price.maxValue} + </span> </span> ); case 'sale': return ( <span className={clsx('block font-semibold', className)}> + <span className="sr-only">{t('originalPrice', { price: price.previousValue })}</span> <span + aria-hidden="true" className={clsx( 'font-normal line-through opacity-50', ... )} > {price.previousValue} </span>{' '} + <span className="sr-only">{t('currentPrice', { price: price.currentValue })}</span> <span + aria-hidden="true" className={clsx( ... )} > {price.currentValue} </span> </span> ); } }Step 3: Add Translation Keys
Update
core/messages/en.jsonto include new translation keys for price accessibility:"Cart": { "title": "Cart", "heading": "Your cart", "proceedToCheckout": "Proceed to checkout", "increment": "Increase quantity", "decrement": "Decrease quantity", "removeItem": "Remove item", "cartCombined": "We noticed you had items saved in a previous cart, so we've added them to your current cart for you.", "cartRestored": "You started a cart on another device, and we've restored it here so you can pick up where you left off.", "cartUpdateInProgress": "You have a cart update in progress. Are you sure you want to leave this page? Your changes may be lost.", + "originalPrice": "Original price was {price}.", + "currentPrice": "Current price is {price}.",#2809
dd559b2Thanks @jorgemoya! - Minor UX improvements for the Reviews section:totalCountfor reviews.averageRatingup to the first decimal.averageRatingnext to rating stars when there are no reviews.