Version Packages (canary)
#2810
Open
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/[email protected]
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
#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.#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.