Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Add minimum quantity, maximum quantity, and step (multiple_of) to the Cart Block and Store API #5406

Merged
merged 17 commits into from
Jan 11, 2022

Conversation

mikejolley
Copy link
Member

@mikejolley mikejolley commented Dec 16, 2021

Closes #5037

Implements the following changes:

  1. quantity_limits returned from the Store API cart endpints, which contains minimum, maximum, and multiple_of. There is also an editable prop which can be set to false to disable inputs.
  2. For products, which are not yet in the cart remember, the product route returns minimum, maximum, and multiple_of as part of the add_to_cart param. I've updated the QuantityInput component to match and support step, in the same way @senadir added support to QuantitySelector.
  3. quantity_limits are validated by the cart controller when adding an item to the cart, or when updating an item already in the cart. The API returns error 400 if the provided value is out of bounds.
  4. Styles the quantity input for stepped (multiple_of) quantities so the +/- buttons clearly show what value comes next.
  5. Hides the quantity selector if the product is sold individually or if editable is false.

Fixes #3888

Caveats:

This functionality is implemented in the Store API. Therefore, minimum, maximum, and step attributes can be circumvented via core functionality e.g. Shop page. Core offers its own filters to control this.

List of hooks

  • woocommerce_store_api_product_quantity_multiple_of
  • woocommerce_store_api_product_quantity_minimum
  • woocommerce_store_api_product_quantity_maximum
  • woocommerce_store_api_product_quantity_editable (cart only)

Testing steps

Developer testing steps:

Smoke test adding items to cart and changing quantity, either via the + and - buttons, or directly via the input.

Disable Selectors

Add the following code to disable quantity inputs for cart items:

add_filter( 'woocommerce_store_api_product_quantity_editable', function( $quantity, $product, $cart_item )  {
	return false;
}, 10, 3 );

Test that no selectors are shown on the cart page. If you attempt to update a qty via the API for an existing cart item, you will get a 400 error.

Step Property

Add the following code to change the multiple/step property for all cart items:

add_filter( 'woocommerce_store_api_product_quantity_multiple_of', function( $quantity, $product, $cart_item )  {
	return 4;
}, 10, 3 );
  • Refresh your cart, change values, quantity should up and down by 4 (or whatever value you set above).
  • If the initial value isn't a multiplier of 4, after changing the quantity, your new quantity would be a multiplier of 4.
  • Try to type a number inside the quantity selector, trying something that isn't a multiplier of 4 would change to the nearest one, for example, typing 14 would yield back 12.

Min/Max for Cart Items

Add the following code to change the minimum and maximum for all cart items:

add_filter( 'woocommerce_store_api_product_quantity_minimum', function( $quantity, $product, $cart_item )  {
	return 5;
}, 10, 3 );

add_filter( 'woocommerce_store_api_product_quantity_maximum', function( $quantity, $product, $cart_item )  {
	return 30;
}, 10, 3 );
  • Refresh your cart, change values. You'll be restricted to a qty between 5 and 30.

Min/Max for Cart Products

Add the following code to change the minimum qty for products (this affects add to cart events):

add_filter( 'woocommerce_store_api_product_quantity_minimum', function( $quantity, $product, $cart_item )  {
	return 5;
}, 10, 3 );
  • Use the All Products block to add a product to the cart.
  • You should see 5 in cart after adding to the cart the first time.

User facing testing steps:

  1. Smoke test adding items to cart and changing quantity, either via the + and - buttons, or directly via the input.
  2. Edit a product and make it "sold individually"
  3. Confirm that no quantity box shows on the cart page for this product
  4. Edit a product and set stock to 6, no backorders.
  5. Confirm that you can only have 6 maximum for this product.

Screenshots

Qty vs qty disabled:

Screenshot 2021-12-21 at 14 39 20
Screenshot 2021-12-21 at 14 39 16

Changelog

Store API and Cart block now support defining a quantity stepper and a minimum quantity.

@mikejolley mikejolley self-assigned this Dec 16, 2021
@mikejolley mikejolley added category: extensibility Work involving adding or updating extensibility. Useful to combine with other scopes impacted. focus: rest api Work impacting REST api routes. block: cart Issues related to the cart block. type: enhancement The issue is a request for an enhancement. needs: dev note PR that has some text that needs to be included in the release notes. labels Dec 16, 2021
@github-actions
Copy link
Contributor

github-actions bot commented Dec 17, 2021

Size Change: +1.51 kB (0%)

Total Size: 815 kB

Filename Size Change
build/all-products-frontend.js 18.6 kB +1 B (0%)
build/all-products.js 35.3 kB +180 B (+1%)
build/atomic-block-components/add-to-cart-frontend.js 7.05 kB +182 B (+3%)
build/atomic-block-components/add-to-cart.js 6.62 kB +180 B (+3%)
build/atomic-block-components/price.js 1.7 kB +2 B (0%)
build/atomic-block-components/summary.js 872 B +1 B (0%)
build/atomic-block-components/title.js 1.1 kB -1 B (0%)
build/cart-blocks/line-items-frontend.js 5.48 kB +161 B (+3%)
build/cart-frontend.js 45.4 kB -1 B (0%)
build/cart.js 44.5 kB +159 B (0%)
build/checkout.js 47.1 kB +3 B (0%)
build/mini-cart-contents.js 3.6 kB +3 B (0%)
build/product-new.js 8.44 kB -1 B (0%)
build/product-on-sale.js 8.81 kB -1 B (0%)
build/product-tag.js 8.5 kB -1 B (0%)
build/product-top-rated.js 8.41 kB -1 B (0%)
build/products-by-attribute.js 9.22 kB -1 B (0%)
build/single-product.js 10.4 kB -1 B (0%)
build/vendors--atomic-block-components/add-to-cart-frontend.js 7.43 kB +619 B (+9%) 🔍
build/wc-blocks-shared-context.js 1.52 kB +10 B (+1%)
build/wc-blocks-style-rtl.css 21.7 kB +9 B (0%)
build/wc-blocks-style.css 21.7 kB +9 B (0%)
ℹ️ View Unchanged
Filename Size
build/active-filters-frontend.js 6.21 kB
build/active-filters.js 7.1 kB
build/all-reviews.js 8.35 kB
build/atomic-block-components/add-to-cart--atomic-block-components/button--atomic-block-components/image---a7e2bb9b.js 2.76 kB
build/atomic-block-components/add-to-cart--atomic-block-components/button.js 1.48 kB
build/atomic-block-components/button-frontend.js 1.48 kB
build/atomic-block-components/button.js 852 B
build/atomic-block-components/category-list-frontend.js 458 B
build/atomic-block-components/category-list.js 458 B
build/atomic-block-components/image-frontend.js 1.37 kB
build/atomic-block-components/image.js 1.05 kB
build/atomic-block-components/price-frontend.js 1.74 kB
build/atomic-block-components/rating-frontend.js 553 B
build/atomic-block-components/rating.js 555 B
build/atomic-block-components/sale-badge-frontend.js 625 B
build/atomic-block-components/sale-badge.js 622 B
build/atomic-block-components/sku-frontend.js 386 B
build/atomic-block-components/sku.js 386 B
build/atomic-block-components/stock-indicator-frontend.js 585 B
build/atomic-block-components/stock-indicator.js 585 B
build/atomic-block-components/summary-frontend.js 874 B
build/atomic-block-components/tag-list-frontend.js 460 B
build/atomic-block-components/tag-list.js 458 B
build/atomic-block-components/title-frontend.js 1.11 kB
build/attribute-filter-frontend.js 16.3 kB
build/attribute-filter.js 12.6 kB
build/blocks-checkout.js 17.6 kB
build/cart-blocks/accepted-payment-methods-frontend.js 1.15 kB
build/cart-blocks/checkout-button-frontend.js 1.14 kB
build/cart-blocks/empty-cart-frontend.js 345 B
build/cart-blocks/express-payment-frontend.js 4.86 kB
build/cart-blocks/filled-cart-frontend.js 766 B
build/cart-blocks/items-frontend.js 298 B
build/cart-blocks/order-summary-frontend.js 8.98 kB
build/cart-blocks/totals-frontend.js 320 B
build/checkout-blocks/actions-frontend.js 1.44 kB
build/checkout-blocks/billing-address--checkout-blocks/shipping-address-frontend.js 4.22 kB
build/checkout-blocks/billing-address-frontend.js 884 B
build/checkout-blocks/contact-information-frontend.js 2.94 kB
build/checkout-blocks/express-payment-frontend.js 5.15 kB
build/checkout-blocks/fields-frontend.js 343 B
build/checkout-blocks/order-note-frontend.js 1.13 kB
build/checkout-blocks/order-summary-frontend.js 11.4 kB
build/checkout-blocks/payment-frontend.js 7.38 kB
build/checkout-blocks/shipping-address-frontend.js 971 B
build/checkout-blocks/shipping-methods-frontend.js 4.82 kB
build/checkout-blocks/terms-frontend.js 1.21 kB
build/checkout-blocks/totals-frontend.js 324 B
build/checkout-frontend.js 47.5 kB
build/featured-category.js 8.55 kB
build/featured-product.js 9.91 kB
build/handpicked-products.js 7.33 kB
build/legacy-template.js 2.18 kB
build/mini-cart-component-frontend.js 14.2 kB
build/mini-cart-frontend.js 1.76 kB
build/mini-cart.js 6.46 kB
build/price-filter-frontend.js 12.6 kB
build/price-filter.js 8.77 kB
build/price-format.js 1.18 kB
build/product-best-sellers.js 7.54 kB
build/product-categories.js 3.47 kB
build/product-category.js 8.38 kB
build/product-search.js 2.47 kB
build/reviews-by-category.js 11.8 kB
build/reviews-by-product.js 12.9 kB
build/reviews-frontend.js 7.24 kB
build/single-product-frontend.js 22.1 kB
build/stock-filter-frontend.js 6.81 kB
build/stock-filter.js 6.83 kB
build/vendors--atomic-block-components/add-to-cart--cart-blocks/order-summary--checkout-blocks/billing-ad--c5eb4dcd-frontend.js 19 kB
build/vendors--atomic-block-components/price--cart-blocks/line-items--cart-blocks/order-summary--checkout--8a3571de-frontend.js 5.71 kB
build/vendors--cart-blocks/line-items--checkout-blocks/order-summary-frontend.js 3.14 kB
build/vendors--cart-blocks/order-summary--checkout-blocks/billing-address--checkout-blocks/order-summary---eb4d2cec-frontend.js 4.75 kB
build/wc-blocks-data.js 8.84 kB
build/wc-blocks-editor-style-rtl.css 4.55 kB
build/wc-blocks-editor-style.css 4.55 kB
build/wc-blocks-google-analytics.js 1.56 kB
build/wc-blocks-middleware.js 949 B
build/wc-blocks-registry.js 2.7 kB
build/wc-blocks-shared-hocs.js 1.14 kB
build/wc-blocks-vendors-style-rtl.css 1.28 kB
build/wc-blocks-vendors-style.css 1.28 kB
build/wc-blocks-vendors.js 65.6 kB
build/wc-blocks.js 2.96 kB
build/wc-payment-method-bacs.js 816 B
build/wc-payment-method-cheque.js 811 B
build/wc-payment-method-cod.js 909 B
build/wc-payment-method-paypal.js 837 B
build/wc-settings.js 2.61 kB

compressed-size-action

@mikejolley mikejolley marked this pull request as ready for review December 17, 2021 17:34
Copy link
Member

@senadir senadir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this Mike! absolutely great effort. I have some comments, UX wise only, @vivialice is also going to add a comment here later regarding design. Feel free to merge after you handle my review, even if don't come to the same conclusion, I trust you have a better vision than me.

  • When an item quantity doesn't match its step, you get this weird edge case (limit is 4, quantity was 2):
    image.
    Once I clicked 6, the new quantity was 4, bound by 0 and 8. 0 didn't make a lot of sense to me
    image

It's hard to know if the number is disabled or enabled.
Should we try to correct quantity to multipleOf so in the case above, the item quantity is increased to 4 on load.
I know you mentioned that

If the initial value isn't a multiplier of 4, after changing the quantity, your new quantity would be a multiplier of 4.

But I think we should correct it nevertheless? I managed to checkout no problem with that quantity.

The same is for limits, should the quantity be corrected to min or max in store API before returning? or would this complicate things?

For adding to cart, for items that already existed in cart and their quantity was less the minimum, I got an error:
Min is 5. I'd assume adding to cart would correct that to 5?
image
image

I know that most of those limitation are only showing given I'm adding filters to the fly while I have items in cart. I believe we should support this nevertheless, given a lot of limits are going to be conditional which mean they would kick in even if an item is already in cart.

Comment on lines +17 to +55
const QuantityInput = ( { disabled, min, max, step = 1, value, onChange } ) => {
const hasMaximum = typeof max !== 'undefined';

/**
* The goal of this function is to normalize what was inserted,
* but after the customer has stopped typing.
*
* It's important to wait before normalizing or we end up with
* a frustrating experience, for example, if the minimum is 2 and
* the customer is trying to type "10", premature normalizing would
* always kick in at "1" and turn that into 2.
*
* Copied from <QuantitySelector>
*/
const normalizeQuantity = useDebouncedCallback( ( initialValue ) => {
// We copy the starting value.
let newValue = initialValue;

// We check if we have a maximum value, and select the lowest between what was inserted and the maximum.
if ( hasMaximum ) {
newValue = Math.min(
newValue,
// the maximum possible value in step increments.
Math.floor( max / step ) * step
);
}

// Select the biggest between what's inserted, the the minimum value in steps.
newValue = Math.max( newValue, Math.ceil( min / step ) * step );

// We round off the value to our steps.
newValue = Math.floor( newValue / step ) * step;

// Only commit if the value has changed
if ( newValue !== initialValue ) {
onChange( newValue );
}
}, 300 );

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we used this in a lot of places that it might be worth abstracting up?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the same component in both places (follow up) rather than abstracting this one function.

src/StoreApi/Schemas/CartItemSchema.php Show resolved Hide resolved
src/StoreApi/Utilities/QuantityLimits.php Show resolved Hide resolved
@mikejolley
Copy link
Member Author

Since this has some follow up work Im punting to next release.

@mikejolley mikejolley modified the milestones: 6.6.0, 6.7.0 Dec 20, 2021
@vivialice
Copy link

Heya @mikejolley ! I just have a comment about the stepper UI. It seems like it should be a selector instead to me, like this:

selector

Is this stepped solution displayed on the product page as well?

@mikejolley
Copy link
Member Author

@vivialice no, it's just on the cart right now but I think they will be unified eventually.

A dropdown would force us to populate all possible options. That's fine for small numbers, but flawed for lots of steps or when there is no upper limit. The step attribute we're using comes from number inputs ref.

I'm fine with putting the + / - back, but this will need a design solution because it's not clear visually what the + and - do when step is greater than 1.

@vivialice
Copy link

Ahh yes that's true. The original + / - is probably the better solution overall.

this will need a design solution because it's not clear visually what the + and - do when step is greater than 1.

Do you mean it might be unclear it increases in multiples?

@mikejolley
Copy link
Member Author

Do you mean it might be unclear it increases in multiples?

@vivialice yes, because the UI for all + - buttons look the same, but the qty increment or decrement may differ.

@mikejolley
Copy link
Member Author

I've removed the custom step indicator so now all increments use + / - with no indication of qty.

2021-12-21 14 54 39

When an item quantity doesn't match its step, you get this weird edge case (limit is 4, quantity was 2):

I've mitigated this by running normalize on mount.

The same is for limits, should the quantity be corrected to min or max in store API before returning? or would this complicate things?

More difficult server side because we'd have to update cart items, but I think it's unlikely to change randomly like we're doing in testing, so we can leave for the normalization function in the client.

For adding to cart, for items that already existed in cart and their quantity was less the minimum, I got an error:
Min is 5. I'd assume adding to cart would correct that to 5?

I didn't account for the case of min changing unexpectedly. For items not in the cart, 5 would be the inital number added. I think it might be useful to support but as it's edge case I won't work on this yet.

@ralucaStan
Copy link
Contributor

does woocommerce_store_api_product_quantity_editable work for the Mini cart block as well?

@mikejolley
Copy link
Member Author

@ralucaStan Yes, I just confirmed so. The Mini Cart Block uses the same qty component, and same endpoints, so yes, these new props are respected there too.

@ralucaStan ralucaStan modified the milestones: 6.7.0, 6.8.0 Jan 3, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
block: cart Issues related to the cart block. category: extensibility Work involving adding or updating extensibility. Useful to combine with other scopes impacted. focus: rest api Work impacting REST api routes. needs: dev note PR that has some text that needs to be included in the release notes. type: enhancement The issue is a request for an enhancement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Hide quantity selector if product is sold individually
4 participants