From d1977c5d1576a74c102ac3030df551a6f1e7bc17 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Wed, 27 Oct 2021 12:26:30 +0100 Subject: [PATCH 1/5] add min and step to Store API --- bin/hook-docs/data/filters.json | 54 ++++++++++++++++++++++++ docs/extensibility/filters.md | 56 +++++++++++++++++++++++++ src/StoreApi/Schemas/CartItemSchema.php | 46 ++++++++++++++++++++ 3 files changed, 156 insertions(+) diff --git a/bin/hook-docs/data/filters.json b/bin/hook-docs/data/filters.json index efc36b4e156..53aa3f9b14a 100644 --- a/bin/hook-docs/data/filters.json +++ b/bin/hook-docs/data/filters.json @@ -877,6 +877,60 @@ "long_description_html": "

This can be used to disable the nonce check when testing API endpoints via a REST API client.

" } }, + { + "name": "woocommerce_store_api_item_quantity_increment", + "file": "StoreApi/Schemas/CartItemSchema.php", + "type": "filter", + "doc": { + "description": "Filters the quantity increment for a cart item in Store API.", + "long_description": "", + "tags": [ + { + "name": "param", + "content": "Cart item array.", + "types": [ + "array" + ], + "variable": "$cart_item" + }, + { + "name": "return", + "content": "", + "types": [ + "\\Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\number" + ] + } + ], + "long_description_html": "" + } + }, + { + "name": "woocommerce_store_api_item_quantity_minimum", + "file": "StoreApi/Schemas/CartItemSchema.php", + "type": "filter", + "doc": { + "description": "Filters the quantity minimum for a cart item in Store API.", + "long_description": "", + "tags": [ + { + "name": "param", + "content": "Cart item array.", + "types": [ + "array" + ], + "variable": "$cart_item" + }, + { + "name": "return", + "content": "", + "types": [ + "\\Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\number" + ] + } + ], + "long_description_html": "" + } + }, { "name": "woocommerce_store_api_product_quantity_limit", "file": "StoreApi/Schemas/ProductSchema.php", diff --git a/docs/extensibility/filters.md b/docs/extensibility/filters.md index 1b1ba88a2d1..bfa51c0c53d 100644 --- a/docs/extensibility/filters.md +++ b/docs/extensibility/filters.md @@ -32,6 +32,8 @@ - [woocommerce_shipping_package_name](#woocommerce_shipping_package_name) - [woocommerce_show_page_title](#woocommerce_show_page_title) - [woocommerce_store_api_disable_nonce_check](#woocommerce_store_api_disable_nonce_check) + - [woocommerce_store_api_item_quantity_increment](#woocommerce_store_api_item_quantity_increment) + - [woocommerce_store_api_item_quantity_minimum](#woocommerce_store_api_item_quantity_minimum) - [woocommerce_store_api_product_quantity_limit](#woocommerce_store_api_product_quantity_limit) - [woocommerce_variation_option_name](#woocommerce_variation_option_name) @@ -781,6 +783,60 @@ File: [StoreApi/Routes/AbstractCartRoute.php](../src/StoreApi/Routes/AbstractCar --- +## woocommerce_store_api_item_quantity_increment + + +Filters the quantity increment for a cart item in Store API. + +```php +apply_filters( 'woocommerce_store_api_item_quantity_increment', array $cart_item ) +``` + +### Parameters + +| Argument | Type | Description | +| -------- | ---- | ----------- | +| $cart_item | array | Cart item array. | + +### Returns + + +`\Automattic\WooCommerce\Blocks\StoreApi\Schemas\number` + +### Source + + +File: [StoreApi/Schemas/CartItemSchema.php](../src/StoreApi/Schemas/CartItemSchema.php) + +--- + +## woocommerce_store_api_item_quantity_minimum + + +Filters the quantity minimum for a cart item in Store API. + +```php +apply_filters( 'woocommerce_store_api_item_quantity_minimum', array $cart_item ) +``` + +### Parameters + +| Argument | Type | Description | +| -------- | ---- | ----------- | +| $cart_item | array | Cart item array. | + +### Returns + + +`\Automattic\WooCommerce\Blocks\StoreApi\Schemas\number` + +### Source + + +File: [StoreApi/Schemas/CartItemSchema.php](../src/StoreApi/Schemas/CartItemSchema.php) + +--- + ## woocommerce_store_api_product_quantity_limit diff --git a/src/StoreApi/Schemas/CartItemSchema.php b/src/StoreApi/Schemas/CartItemSchema.php index f384d970dff..c431735e50c 100644 --- a/src/StoreApi/Schemas/CartItemSchema.php +++ b/src/StoreApi/Schemas/CartItemSchema.php @@ -57,6 +57,18 @@ public function get_properties() { 'context' => [ 'view', 'edit' ], 'readonly' => true, ], + 'quantity_min' => [ + 'description' => __( 'The minimum quantity than can be added to the cart at once.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], + 'quantity_increment' => [ + 'description' => __( 'The amount quantity can change with.', 'woo-gutenberg-products-block' ), + 'type' => 'integer', + 'context' => [ 'view', 'edit' ], + 'readonly' => true, + ], 'name' => [ 'description' => __( 'Product name.', 'woo-gutenberg-products-block' ), 'type' => 'string', @@ -314,6 +326,8 @@ public function get_item_response( $cart_item ) { 'id' => $product->get_id(), 'quantity' => wc_stock_amount( $cart_item['quantity'] ), 'quantity_limit' => $this->get_product_quantity_limit( $product ), + 'quantity_min' => $this->get_item_quantity_min( $cart_item ), + 'quantity_increment' => $this->get_item_quantity_increment( $cart_item ), 'name' => $this->prepare_html_response( $product->get_title() ), 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ), 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ), @@ -451,6 +465,38 @@ protected function get_item_data( $cart_item ) { return array_map( [ $this, 'format_item_data_element' ], $item_data ); } + /** + * Return the minimum quantity that's allowed for this item. + * + * @param array $cart_item Cart item array. + * @return number + */ + protected function get_item_quantity_min( $cart_item ) { + /** + * Filters the quantity minimum for a cart item in Store API. + * + * @param array $cart_item Cart item array. + * @return number + */ + return apply_filters( 'woocommerce_store_api_item_quantity_minimum', 1, $cart_item ); + } + + /** + * Return the increment quantity that an item is allowed to change with. + * + * @param array $cart_item Cart item array. + * @return number + */ + protected function get_item_quantity_increment( $cart_item ) { + /** + * Filters the quantity increment for a cart item in Store API. + * + * @param array $cart_item Cart item array. + * @return number + */ + return apply_filters( 'woocommerce_store_api_item_quantity_increment', 1, $cart_item ); + } + /** * Remove HTML tags from cart item data and set the `hidden` property to * `__experimental_woocommerce_blocks_hidden`. From 2bde4c6bf61240190b854e76d385ccd52f4a6232 Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Thu, 28 Oct 2021 17:50:03 +0100 Subject: [PATCH 2/5] add min and step support --- .../components/quantity-selector/index.tsx | 77 ++++++++++++++----- .../cart-line-item-row.tsx | 4 + assets/js/types/type-defs/cart.ts | 2 + bin/hook-docs/data/filters.json | 8 +- docs/extensibility/filters.md | 14 ++-- src/StoreApi/Schemas/CartItemSchema.php | 6 +- 6 files changed, 79 insertions(+), 32 deletions(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index 91e195a68bf..3904eb85b3e 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -6,6 +6,7 @@ import { speak } from '@wordpress/a11y'; import classNames from 'classnames'; import { useCallback } from '@wordpress/element'; import { DOWN, UP } from '@wordpress/keycodes'; +import useDebouncedCallback from 'use-debounce/lib/useDebouncedCallback'; /** * Internal dependencies @@ -17,6 +18,7 @@ interface QuantitySelectorProps { quantity?: number; minimum?: number; maximum: number; + step?: number; onChange: ( newQuantity: number ) => void; itemName?: string; disabled: boolean; @@ -27,6 +29,7 @@ const QuantitySelector = ( { quantity = 1, minimum = 1, maximum, + step = 1, onChange = () => { /* Do nothing. */ }, @@ -39,8 +42,46 @@ const QuantitySelector = ( { ); const hasMaximum = typeof maximum !== 'undefined'; - const canDecrease = quantity > minimum; - const canIncrease = ! hasMaximum || quantity < maximum; + const canDecrease = quantity - step > minimum; + const canIncrease = ! hasMaximum || quantity + step < maximum; + + /** + * 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 expiernce, for example, if the minimun is 2 and + * the customer is trying to type "10", premature normalizing would + * always kick in at "1" and turn that into 2. + */ + const [ normalizeQuantity ] = useDebouncedCallback( + ( initialValue: number ) => { + // We copy the starting value. + let value = initialValue; + + // We check if we have a maximum value, and select the lowest between what was inserted, and the maximum. + if ( hasMaximum ) { + value = Math.min( + value, + // the maximum possible value in step increments. + Math.floor( maximum / step ) * step + ); + } + + // Select the biggest between what's inserted, the the minimum value in steps. + value = Math.max( value, Math.ceil( minimum / step ) * step ); + + // We round off the value to our steps. + value = Math.floor( value / step ) * step; + + // Only commit if the value has changed + if ( value !== initialValue ) { + onChange( value ); + } + }, + // This value is delibrtly smaller than what's in useStoreCartItemQuantity so we don't end up with two requests. + 300 + ); /** * Handles keyboard up and down keys to change quantity value. @@ -60,15 +101,15 @@ const QuantitySelector = ( { if ( isArrowDown && canDecrease ) { event.preventDefault(); - onChange( quantity - 1 ); + onChange( quantity - step ); } if ( isArrowUp && canIncrease ) { event.preventDefault(); - onChange( quantity + 1 ); + onChange( quantity + step ); } }, - [ quantity, onChange, canIncrease, canDecrease ] + [ quantity, onChange, canIncrease, canDecrease, step ] ); return ( @@ -77,22 +118,22 @@ const QuantitySelector = ( { className="wc-block-components-quantity-selector__input" disabled={ disabled } type="number" - step="1" - min="0" + step={ step } + min={ minimum } value={ quantity } onKeyDown={ quantityInputOnKeyDown } onChange={ ( event ) => { - let value = - Number.isNaN( event.target.value ) || - ! event.target.value - ? 0 - : parseInt( event.target.value, 10 ); - if ( hasMaximum ) { - value = Math.min( value, maximum ); - } - value = Math.max( value, minimum ); + // Inputs values are strings, we parse them here. + let value = parseInt( event.target.value, 10 ); + // parseInt would throw NaN for anything not a number, + // so we revert value to the quantity value. + value = isNaN( value ) ? quantity : value; + if ( value !== quantity ) { + // we commit this value immideatly. onChange( value ); + // but once the customer has stopped typing, we make sure his value is respecting the bounds (maximum value, minimum value, step value), and commit the normlized value. + normalizeQuantity( value ); } } } aria-label={ sprintf( @@ -112,7 +153,7 @@ const QuantitySelector = ( { className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus" disabled={ disabled || ! canDecrease } onClick={ () => { - const newQuantity = quantity - 1; + const newQuantity = quantity - step; onChange( newQuantity ); speak( sprintf( @@ -136,7 +177,7 @@ const QuantitySelector = ( { disabled={ disabled || ! canIncrease } className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus" onClick={ () => { - const newQuantity = quantity + 1; + const newQuantity = quantity + step; onChange( newQuantity ); speak( sprintf( diff --git a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx index 94bc2b0e05d..b6b4c3ab27f 100644 --- a/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx +++ b/assets/js/blocks/cart-checkout/cart/cart-line-items-table/cart-line-item-row.tsx @@ -71,6 +71,8 @@ const CartLineItemRow = forwardRef< HTMLTableRowElement, CartLineItemRowProps >( low_stock_remaining: lowStockRemaining = null, show_backorder_badge: showBackorderBadge = false, quantity_limit: quantityLimit = 99, + quantity_min: quantityMin = 1, + quantity_step: quantityStep = 1, permalink = '', images = [], variation = [], @@ -282,6 +284,8 @@ const CartLineItemRow = forwardRef< HTMLTableRowElement, CartLineItemRowProps >( disabled={ isPendingDelete } quantity={ quantity } maximum={ quantityLimit } + minimum={ quantityMin } + step={ quantityStep } onChange={ ( newQuantity ) => { setItemQuantity( newQuantity ); dispatchStoreEvent( 'cart-set-item-quantity', { diff --git a/assets/js/types/type-defs/cart.ts b/assets/js/types/type-defs/cart.ts index 31d17c53fea..59b4c80c562 100644 --- a/assets/js/types/type-defs/cart.ts +++ b/assets/js/types/type-defs/cart.ts @@ -118,6 +118,8 @@ export interface CartItem { quantity: number; catalog_visibility: CatalogVisibility; quantity_limit: number; + quantity_min: number; + quantity_step: number; name: string; summary: string; short_description: string; diff --git a/bin/hook-docs/data/filters.json b/bin/hook-docs/data/filters.json index 53aa3f9b14a..9ebbf7ab2a7 100644 --- a/bin/hook-docs/data/filters.json +++ b/bin/hook-docs/data/filters.json @@ -878,11 +878,11 @@ } }, { - "name": "woocommerce_store_api_item_quantity_increment", + "name": "woocommerce_store_api_item_quantity_minimum", "file": "StoreApi/Schemas/CartItemSchema.php", "type": "filter", "doc": { - "description": "Filters the quantity increment for a cart item in Store API.", + "description": "Filters the quantity minimum for a cart item in Store API.", "long_description": "", "tags": [ { @@ -905,11 +905,11 @@ } }, { - "name": "woocommerce_store_api_item_quantity_minimum", + "name": "woocommerce_store_api_item_quantity_step", "file": "StoreApi/Schemas/CartItemSchema.php", "type": "filter", "doc": { - "description": "Filters the quantity minimum for a cart item in Store API.", + "description": "Filters the quantity increment for a cart item in Store API.", "long_description": "", "tags": [ { diff --git a/docs/extensibility/filters.md b/docs/extensibility/filters.md index bfa51c0c53d..c8f283aa676 100644 --- a/docs/extensibility/filters.md +++ b/docs/extensibility/filters.md @@ -32,8 +32,8 @@ - [woocommerce_shipping_package_name](#woocommerce_shipping_package_name) - [woocommerce_show_page_title](#woocommerce_show_page_title) - [woocommerce_store_api_disable_nonce_check](#woocommerce_store_api_disable_nonce_check) - - [woocommerce_store_api_item_quantity_increment](#woocommerce_store_api_item_quantity_increment) - [woocommerce_store_api_item_quantity_minimum](#woocommerce_store_api_item_quantity_minimum) + - [woocommerce_store_api_item_quantity_step](#woocommerce_store_api_item_quantity_step) - [woocommerce_store_api_product_quantity_limit](#woocommerce_store_api_product_quantity_limit) - [woocommerce_variation_option_name](#woocommerce_variation_option_name) @@ -783,13 +783,13 @@ File: [StoreApi/Routes/AbstractCartRoute.php](../src/StoreApi/Routes/AbstractCar --- -## woocommerce_store_api_item_quantity_increment +## woocommerce_store_api_item_quantity_minimum -Filters the quantity increment for a cart item in Store API. +Filters the quantity minimum for a cart item in Store API. ```php -apply_filters( 'woocommerce_store_api_item_quantity_increment', array $cart_item ) +apply_filters( 'woocommerce_store_api_item_quantity_minimum', array $cart_item ) ``` ### Parameters @@ -810,13 +810,13 @@ File: [StoreApi/Schemas/CartItemSchema.php](../src/StoreApi/Schemas/CartItemSche --- -## woocommerce_store_api_item_quantity_minimum +## woocommerce_store_api_item_quantity_step -Filters the quantity minimum for a cart item in Store API. +Filters the quantity increment for a cart item in Store API. ```php -apply_filters( 'woocommerce_store_api_item_quantity_minimum', array $cart_item ) +apply_filters( 'woocommerce_store_api_item_quantity_step', array $cart_item ) ``` ### Parameters diff --git a/src/StoreApi/Schemas/CartItemSchema.php b/src/StoreApi/Schemas/CartItemSchema.php index c431735e50c..413fc871ef4 100644 --- a/src/StoreApi/Schemas/CartItemSchema.php +++ b/src/StoreApi/Schemas/CartItemSchema.php @@ -327,7 +327,7 @@ public function get_item_response( $cart_item ) { 'quantity' => wc_stock_amount( $cart_item['quantity'] ), 'quantity_limit' => $this->get_product_quantity_limit( $product ), 'quantity_min' => $this->get_item_quantity_min( $cart_item ), - 'quantity_increment' => $this->get_item_quantity_increment( $cart_item ), + 'quantity_step' => $this->get_item_quantity_step( $cart_item ), 'name' => $this->prepare_html_response( $product->get_title() ), 'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ), 'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ), @@ -487,14 +487,14 @@ protected function get_item_quantity_min( $cart_item ) { * @param array $cart_item Cart item array. * @return number */ - protected function get_item_quantity_increment( $cart_item ) { + protected function get_item_quantity_step( $cart_item ) { /** * Filters the quantity increment for a cart item in Store API. * * @param array $cart_item Cart item array. * @return number */ - return apply_filters( 'woocommerce_store_api_item_quantity_increment', 1, $cart_item ); + return apply_filters( 'woocommerce_store_api_item_quantity_step', 1, $cart_item ); } /** From 799a962b785c8dc4d9a05e631d39f5197635337f Mon Sep 17 00:00:00 2001 From: Nadir Seghir Date: Thu, 28 Oct 2021 19:09:56 +0100 Subject: [PATCH 3/5] typo --- src/StoreApi/Schemas/CartItemSchema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StoreApi/Schemas/CartItemSchema.php b/src/StoreApi/Schemas/CartItemSchema.php index 413fc871ef4..7d54cb96398 100644 --- a/src/StoreApi/Schemas/CartItemSchema.php +++ b/src/StoreApi/Schemas/CartItemSchema.php @@ -63,7 +63,7 @@ public function get_properties() { 'context' => [ 'view', 'edit' ], 'readonly' => true, ], - 'quantity_increment' => [ + 'quantity_step' => [ 'description' => __( 'The amount quantity can change with.', 'woo-gutenberg-products-block' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], From 7551f499af1454f345497ca1ac5d321d9cb6e58d Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 15 Dec 2021 11:08:32 +0000 Subject: [PATCH 4/5] Update assets/js/base/components/quantity-selector/index.tsx --- assets/js/base/components/quantity-selector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index 3904eb85b3e..5654203822b 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -50,7 +50,7 @@ const QuantitySelector = ( { * but after the customer has stopped typing. * * It's important to wait before normalizing or we end up with - * a frustrating expiernce, for example, if the minimun is 2 and + * 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. */ From 2882d3cdc1889248526e2326e0ac8a9409299510 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 15 Dec 2021 11:08:39 +0000 Subject: [PATCH 5/5] Update assets/js/base/components/quantity-selector/index.tsx --- assets/js/base/components/quantity-selector/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/components/quantity-selector/index.tsx b/assets/js/base/components/quantity-selector/index.tsx index 5654203822b..e5dda6d7f65 100644 --- a/assets/js/base/components/quantity-selector/index.tsx +++ b/assets/js/base/components/quantity-selector/index.tsx @@ -130,7 +130,7 @@ const QuantitySelector = ( { value = isNaN( value ) ? quantity : value; if ( value !== quantity ) { - // we commit this value immideatly. + // we commit this value immediately. onChange( value ); // but once the customer has stopped typing, we make sure his value is respecting the bounds (maximum value, minimum value, step value), and commit the normlized value. normalizeQuantity( value );