diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cdc020f4b..595e18169a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Draft - - Updated accessibility features [2656](https://github.com/bigcommerce/cornerstone/pull/2656) +- Respect `available_to_sell` on PDP so the Sold Out alert is hidden and the Add to Cart button stays enabled for backorderable products, and is disabled when quantity exceeds `available_to_sell` [#2659](https://github.com/bigcommerce/cornerstone/pull/2659) +- Updated accessibility features [2656](https://github.com/bigcommerce/cornerstone/pull/2656) - Update 'Ship to' copy for multi address orders [#2655](https://github.com/bigcommerce/cornerstone/pull/2655) - Fixed typo in category page reset-filters live region handler [#2643](https://github.com/bigcommerce/cornerstone/pull/2643) - Swap content/data keys in onProductOptionsChanged event detail [#2640](https://github.com/bigcommerce/cornerstone/pull/2640) diff --git a/assets/js/theme/common/product-details-base.js b/assets/js/theme/common/product-details-base.js index e0cb5a8ebe..bbae88c77d 100644 --- a/assets/js/theme/common/product-details-base.js +++ b/assets/js/theme/common/product-details-base.js @@ -43,6 +43,10 @@ export default class ProductDetailsBase { this._makeProductVariantAccessible(value, type); }); + + if ((parseInt(this.context.availableToSell, 10) || 0) > 0) { + this.toggleSoldOutAlert(true); + } } _makeProductVariantAccessible(variantDomNode, variantType) { @@ -298,6 +302,31 @@ export default class ProductDetailsBase { } } + updateAddToCartForQty(qty, passedViewModel) { + const viewModel = passedViewModel || this.getViewModel(this.$scope); + const availableToSell = parseInt(this.context.availableToSell, 10) || 0; + + if (availableToSell <= 0) return; + + const $variantMessage = $('#add-to-cart-wrapper .productAttributes-message', this.$scope); + + if (qty > availableToSell) { + viewModel.$addToCart.prop('disabled', true); + + const template = this.context.quantityMaxMessage || 'The maximum purchasable quantity is __QTY__'; + const message = template.replace('__QTY__', availableToSell); + $('.alertBox-message', $variantMessage).text(message); + $variantMessage.attr('data-qty-limit', 'true').show(); + } else if (!viewModel.$increments.prop('disabled')) { + viewModel.$addToCart.prop('disabled', false); + + if ($variantMessage.attr('data-qty-limit') === 'true') { + $variantMessage.removeAttr('data-qty-limit').hide(); + $('.alertBox-message', $variantMessage).text(''); + } + } + } + updateBackorderContext(data) { if (typeof data.available_on_hand === 'number') { this.context.availableOnHand = data.available_on_hand; @@ -397,6 +426,7 @@ export default class ProductDetailsBase { this.updateBackorderMessage(viewModel); this.updateDefaultAttributesForOOS(data); + this.updateAddToCartForQty(currentQty, viewModel); this.updateWalletButtonsView(data); // If Bulk Pricing rendered HTML is available @@ -488,20 +518,42 @@ export default class ProductDetailsBase { updateDefaultAttributesForOOS(data) { const viewModel = this.getViewModel(this.$scope); - if (!data.purchasable || !data.instock) { + const dataAvailableToSell = typeof data.available_to_sell === 'number' + ? data.available_to_sell + : parseInt(this.context.availableToSell, 10) || 0; + const canSell = data.instock || dataAvailableToSell > 0; + if (!data.purchasable || !canSell) { viewModel.$addToCart.prop('disabled', true); viewModel.$increments.prop('disabled', true); } else { viewModel.$addToCart.prop('disabled', false); viewModel.$increments.prop('disabled', false); } + + this.toggleSoldOutAlert(canSell); + } + + toggleSoldOutAlert(canSell) { + const $soldOut = $('#add-to-cart-wrapper .alertBox--error', this.$scope).not('.productAttributes-message'); + const $variantMessage = $('#add-to-cart-wrapper .productAttributes-message', this.$scope); + + if (canSell) { + $soldOut.hide(); + $variantMessage.hide(); + } else if ($soldOut.length) { + $soldOut.show(); + } } updateWalletButtonsView(data) { const viewModel = this.getViewModel(this.$scope); const isValidForm = viewModel.$addToCartForm?.[0]?.checkValidity() ?? true; + const dataAvailableToSell = typeof data.available_to_sell === 'number' + ? data.available_to_sell + : parseInt(this.context.availableToSell, 10) || 0; + const canSell = data.instock || dataAvailableToSell > 0; - this.toggleWalletButtonsVisibility(isValidForm && data.purchasable && data.instock); + this.toggleWalletButtonsVisibility(isValidForm && data.purchasable && canSell); } toggleWalletButtonsVisibility(shouldShow) { diff --git a/assets/js/theme/common/product-details.js b/assets/js/theme/common/product-details.js index 0d52647339..e51d91a9c4 100644 --- a/assets/js/theme/common/product-details.js +++ b/assets/js/theme/common/product-details.js @@ -83,17 +83,17 @@ export default class ProductDetails extends ProductDetailsBase { this.setProductVariant(); }); - if (!$productOptionsElement.length) { - const simpleProductId = $('[name="product_id"]', $form).val(); - utils.api.productAttributes.optionChange(simpleProductId, $form.serialize(), (err, response) => { - if (err || !response || !response.data) return; - this.updateBackorderContext(response.data); - const vm = this.getViewModel(this.$scope); - const qty = parseInt(vm.quantity.$input.val(), 10) || 0; - this.updateQtyBackorderedMessage(qty, vm); - this.updateBackorderMessage(vm); - }); - } + const productId = $('[name="product_id"]', $form).val(); + utils.api.productAttributes.optionChange(productId, $form.serialize(), (err, response) => { + if (err || !response || !response.data) return; + this.updateBackorderContext(response.data); + const vm = this.getViewModel(this.$scope); + const qty = parseInt(vm.quantity.$input.val(), 10) || 0; + this.updateQtyBackorderedMessage(qty, vm); + this.updateBackorderMessage(vm); + this.updateDefaultAttributesForOOS(response.data); + this.updateAddToCartForQty(qty, vm); + }); $form.on('submit', event => { this.addToCartValidator.performCheck(); @@ -393,6 +393,7 @@ export default class ProductDetails extends ProductDetailsBase { this.updateProductDetailsData(); this.updateQtyBackorderedMessage(qty, viewModel); this.updateBackorderMessage(viewModel); + this.updateAddToCartForQty(qty, viewModel); }); // Prevent triggering quantity change when pressing enter @@ -411,6 +412,7 @@ export default class ProductDetails extends ProductDetailsBase { this.updateProductDetailsData(); this.updateQtyBackorderedMessage(qty, viewModel); this.updateBackorderMessage(viewModel); + this.updateAddToCartForQty(qty, viewModel); }); } diff --git a/templates/components/products/add-to-cart.html b/templates/components/products/add-to-cart.html index 63e67b91c3..e78ee04b88 100644 --- a/templates/components/products/add-to-cart.html +++ b/templates/components/products/add-to-cart.html @@ -36,11 +36,13 @@ {{/if}} {{#if product.out_of_stock}} - {{#if product.out_of_stock_message}} - {{> components/common/alert/alert-error product.out_of_stock_message}} - {{else}} - {{> components/common/alert/alert-error (lang 'products.sold_out')}} - {{/if}} + {{#unless product.available_to_sell}} + {{#if product.out_of_stock_message}} + {{> components/common/alert/alert-error product.out_of_stock_message}} + {{else}} + {{> components/common/alert/alert-error (lang 'products.sold_out')}} + {{/if}} + {{/unless}} {{/if}}