Add pricing tab for variations (#40642)

* Update blocks with postType context

* Add tax class

* Pass context into get_tax_class

* Add parent option

* Add changelog

* Update changelog

* Add isRequired attribute to regular price block for use in variations

* Add additional condition to avoid error in date time picker

* Add changelog

* Fix lint errors
This commit is contained in:
louwie17 2023-10-10 08:55:05 -03:00 committed by GitHub
parent 914a1dfd09
commit 98876f54d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 84 additions and 78 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Small condition change in the date time picker to avoid edge case where inputControl is null.

View File

@ -265,7 +265,11 @@ export const DateTimePickerControl = forwardRef(
}, [ onBlur ] ); }, [ onBlur ] );
const callOnBlurIfDropdownIsNotOpening = useCallback( ( willOpen ) => { const callOnBlurIfDropdownIsNotOpening = useCallback( ( willOpen ) => {
if ( ! willOpen && typeof onBlurRef.current === 'function' ) { if (
! willOpen &&
typeof onBlurRef.current === 'function' &&
inputControl.current
) {
// in case the component is blurred before a debounced // in case the component is blurred before a debounced
// change has been processed, immediately set the input string // change has been processed, immediately set the input string
// to the current value of the input field, so that // to the current value of the input field, so that

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update pricing, and schedule field blocks to use postType context value.

View File

@ -14,6 +14,10 @@
}, },
"help": { "help": {
"type": "string" "type": "string"
},
"isRequired": {
"type": "boolean",
"default": false
} }
}, },
"supports": { "supports": {

View File

@ -9,8 +9,12 @@ import { getNewPath } from '@woocommerce/navigation';
import { recordEvent } from '@woocommerce/tracks'; import { recordEvent } from '@woocommerce/tracks';
import { useInstanceId } from '@wordpress/compose'; import { useInstanceId } from '@wordpress/compose';
import { useEntityProp } from '@wordpress/core-data'; import { useEntityProp } from '@wordpress/core-data';
import { createElement, createInterpolateElement } from '@wordpress/element'; import {
import { __ } from '@wordpress/i18n'; createElement,
createInterpolateElement,
useEffect,
} from '@wordpress/element';
import { sprintf, __ } from '@wordpress/i18n';
import { import {
BaseControl, BaseControl,
// @ts-expect-error `__experimentalInputControl` does exist. // @ts-expect-error `__experimentalInputControl` does exist.
@ -31,7 +35,7 @@ export function Edit( {
context, context,
}: ProductEditorBlockEditProps< SalePriceBlockAttributes > ) { }: ProductEditorBlockEditProps< SalePriceBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes ); const blockProps = useWooBlockProps( attributes );
const { label, help } = attributes; const { label, help, isRequired } = attributes;
const [ regularPrice, setRegularPrice ] = useEntityProp< string >( const [ regularPrice, setRegularPrice ] = useEntityProp< string >(
'postType', 'postType',
context.postType || 'product', context.postType || 'product',
@ -89,11 +93,20 @@ export function Edit( {
'woocommerce' 'woocommerce'
); );
} }
} else if ( isRequired ) {
/* translators: label of required field. */
return sprintf( __( '%s is required.', 'woocommerce' ), label );
} }
}, },
[ regularPrice, salePrice ] [ regularPrice, salePrice ]
); );
useEffect( () => {
if ( isRequired ) {
validateRegularPrice();
}
}, [] );
return ( return (
<div { ...blockProps }> <div { ...blockProps }>
<BaseControl <BaseControl

View File

@ -6,4 +6,5 @@ import { BlockAttributes } from '@wordpress/blocks';
export interface SalePriceBlockAttributes extends BlockAttributes { export interface SalePriceBlockAttributes extends BlockAttributes {
label: string; label: string;
help?: string; help?: string;
isRequired?: boolean;
} }

View File

@ -25,5 +25,6 @@
"lock": false, "lock": false,
"__experimentalToolbar": false "__experimentalToolbar": false
}, },
"usesContext": [ "postType" ],
"editorStyle": "file:./editor.css" "editorStyle": "file:./editor.css"
} }

View File

@ -25,17 +25,18 @@ import { ProductEditorBlockEditProps } from '../../../types';
export function Edit( { export function Edit( {
attributes, attributes,
clientId, clientId,
context,
}: ProductEditorBlockEditProps< SalePriceBlockAttributes > ) { }: ProductEditorBlockEditProps< SalePriceBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes ); const blockProps = useWooBlockProps( attributes );
const { label, help } = attributes; const { label, help } = attributes;
const [ regularPrice ] = useEntityProp< string >( const [ regularPrice ] = useEntityProp< string >(
'postType', 'postType',
'product', context.postType || 'product',
'regular_price' 'regular_price'
); );
const [ salePrice, setSalePrice ] = useEntityProp< string >( const [ salePrice, setSalePrice ] = useEntityProp< string >(
'postType', 'postType',
'product', context.postType || 'product',
'sale_price' 'sale_price'
); );
const inputProps = useCurrencyInputProps( { const inputProps = useCurrencyInputProps( {

View File

@ -22,5 +22,6 @@
"lock": false, "lock": false,
"__experimentalToolbar": false "__experimentalToolbar": false
}, },
"editorStyle": "file:./editor.css" "editorStyle": "file:./editor.css",
"usesContext": [ "postType" ]
} }

View File

@ -26,6 +26,7 @@ import { ProductEditorBlockEditProps } from '../../../types';
export function Edit( { export function Edit( {
attributes, attributes,
clientId, clientId,
context,
}: ProductEditorBlockEditProps< ScheduleSalePricingBlockAttributes > ) { }: ProductEditorBlockEditProps< ScheduleSalePricingBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes ); const blockProps = useWooBlockProps( attributes );
const { hasEdit } = useProductEdits(); const { hasEdit } = useProductEdits();
@ -36,7 +37,7 @@ export function Edit( {
const [ salePrice ] = useEntityProp< string | null >( const [ salePrice ] = useEntityProp< string | null >(
'postType', 'postType',
'product', context.postType || 'product',
'sale_price' 'sale_price'
); );
@ -45,11 +46,11 @@ export function Edit( {
const [ dateOnSaleFromGmt, setDateOnSaleFromGmt ] = useEntityProp< const [ dateOnSaleFromGmt, setDateOnSaleFromGmt ] = useEntityProp<
string | null string | null
>( 'postType', 'product', 'date_on_sale_from_gmt' ); >( 'postType', context.postType || 'product', 'date_on_sale_from_gmt' );
const [ dateOnSaleToGmt, setDateOnSaleToGmt ] = useEntityProp< const [ dateOnSaleToGmt, setDateOnSaleToGmt ] = useEntityProp<
string | null string | null
>( 'postType', 'product', 'date_on_sale_to_gmt' ); >( 'postType', context.postType || 'product', 'date_on_sale_to_gmt' );
const today = moment().startOf( 'minute' ).toISOString(); const today = moment().startOf( 'minute' ).toISOString();

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update variation API to adhere tax class to context, and updated variation template to use tax class field.

View File

@ -40,19 +40,19 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
'/' . $this->rest_base . '/generate', '/' . $this->rest_base . '/generate',
array( array(
'args' => array( 'args' => array(
'product_id' => array( 'product_id' => array(
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
'type' => 'integer', 'type' => 'integer',
), ),
'delete' => array( 'delete' => array(
'description' => __( 'Deletes unused variations.', 'woocommerce' ), 'description' => __( 'Deletes unused variations.', 'woocommerce' ),
'type' => 'boolean', 'type' => 'boolean',
), ),
'default_values' => array( 'default_values' => array(
'description' => __( 'Default values for generated variations.', 'woocommerce' ), 'description' => __( 'Default values for generated variations.', 'woocommerce' ),
'type' => 'object', 'type' => 'object',
'properties' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 'properties' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
) ),
), ),
array( array(
'methods' => WP_REST_Server::CREATABLE, 'methods' => WP_REST_Server::CREATABLE,
@ -99,7 +99,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1,
'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1,
'tax_status' => $object->get_tax_status(), 'tax_status' => $object->get_tax_status(),
'tax_class' => $object->get_tax_class(), 'tax_class' => $object->get_tax_class( $context ),
'manage_stock' => $object->managing_stock(), 'manage_stock' => $object->managing_stock(),
'stock_quantity' => $object->get_stock_quantity(), 'stock_quantity' => $object->get_stock_quantity(),
'stock_status' => $object->get_stock_status(), 'stock_status' => $object->get_stock_status(),
@ -132,6 +132,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
* The dynamic portion of the hook name, $this->post_type, * The dynamic portion of the hook name, $this->post_type,
* refers to object type being prepared for the response. * refers to object type being prepared for the response.
* *
* @since 4.5.0
* @param WP_REST_Response $response The response object. * @param WP_REST_Response $response The response object.
* @param WC_Data $object Object data. * @param WC_Data $object Object data.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
@ -350,6 +351,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
* The dynamic portion of the hook name, `$this->post_type`, * The dynamic portion of the hook name, `$this->post_type`,
* refers to the object type slug. * refers to the object type slug.
* *
* @since 4.5.0
* @param WC_Data $variation Object object. * @param WC_Data $variation Object object.
* @param WP_REST_Request $request Request object. * @param WP_REST_Request $request Request object.
* @param bool $creating If is creating a new object. * @param bool $creating If is creating a new object.
@ -410,6 +412,15 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
$upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) );
if ( is_wp_error( $upload ) ) { if ( is_wp_error( $upload ) ) {
/**
* Filter to check if it should supress the image upload error, false by default.
*
* @since 4.5.0
* @param bool false If it should suppress.
* @param array $upload Uploaded image array.
* @param int id Variation id.
* @param array Array of image to set.
*/
if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) { if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) {
throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 ); throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 );
} }
@ -648,7 +659,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
'readonly' => true, 'readonly' => true,
), ),
'low_stock_amount' => array( 'low_stock_amount' => array(
'description' => __( 'Low Stock amount for the variation.', 'woocommerce' ), 'description' => __( 'Low Stock amount for the variation.', 'woocommerce' ),
'type' => array( 'integer', 'null' ), 'type' => array( 'integer', 'null' ),
'context' => array( 'view', 'edit' ), 'context' => array( 'view', 'edit' ),
@ -971,16 +982,16 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
); );
$params['attributes'] = array( $params['attributes'] = array(
'description' => __( 'Limit result set to products with specified attributes.', 'woocommerce' ), 'description' => __( 'Limit result set to products with specified attributes.', 'woocommerce' ),
'type' => 'array', 'type' => 'array',
'items' => array( 'items' => array(
'type' => 'object', 'type' => 'object',
'properties' => array( 'properties' => array(
'attribute' => array( 'attribute' => array(
'type' => 'string', 'type' => 'string',
'description' => __( 'Attribute slug.', 'woocommerce' ), 'description' => __( 'Attribute slug.', 'woocommerce' ),
), ),
'term' => array( 'term' => array(
'type' => 'string', 'type' => 'string',
'description' => __( 'Attribute term.', 'woocommerce' ), 'description' => __( 'Attribute term.', 'woocommerce' ),
), ),
@ -1013,7 +1024,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
foreach ( $existing_variations as $existing_variation ) { foreach ( $existing_variations as $existing_variation ) {
$matching_attribute_key = array_search( $existing_variation->get_attributes(), $possible_attribute_combinations ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict $matching_attribute_key = array_search( $existing_variation->get_attributes(), $possible_attribute_combinations ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
if ( $matching_attribute_key !== false ) { if ( false !== $matching_attribute_key ) {
// We only want one possible variation for each possible attribute combination. // We only want one possible variation for each possible attribute combination.
unset( $possible_attribute_combinations[ $matching_attribute_key ] ); unset( $possible_attribute_combinations[ $matching_attribute_key ] );
continue; continue;
@ -1043,12 +1054,12 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
$response = array(); $response = array();
$product = wc_get_product( $product_id ); $product = wc_get_product( $product_id );
$default_values = isset( $request['default_values'] ) ? $request['default_values'] : array(); $default_values = isset( $request['default_values'] ) ? $request['default_values'] : array();
$data_store = $product->get_data_store(); $data_store = $product->get_data_store();
$response['count'] = $data_store->create_all_product_variations( $product, Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ), $default_values ); $response['count'] = $data_store->create_all_product_variations( $product, Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ), $default_values );
if ( isset( $request['delete'] ) && $request['delete'] ) { if ( isset( $request['delete'] ) && $request['delete'] ) {
$deleted_count = $this->delete_unmatched_product_variations( $product ); $deleted_count = $this->delete_unmatched_product_variations( $product );
$response['deleted_count'] = $deleted_count; $response['deleted_count'] = $deleted_count;
} }

View File

@ -156,18 +156,6 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
*/ */
private function add_pricing_group_blocks() { private function add_pricing_group_blocks() {
$pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] ); $pricing_group = $this->get_group_by_id( $this::GROUP_IDS['PRICING'] );
$pricing_group->add_block(
[
'id' => 'pricing-has-variations-notice',
'blockName' => 'woocommerce/product-has-variations-notice',
'order' => 10,
'attributes' => [
'content' => __( 'This product has options, such as size or color. You can now manage each variation\'s price and other details individually.', 'woocommerce' ),
'buttonText' => __( 'Go to Variations', 'woocommerce' ),
'type' => 'info',
],
]
);
// Product Pricing Section. // Product Pricing Section.
$product_pricing_section = $pricing_group->add_section( $product_pricing_section = $pricing_group->add_section(
[ [
@ -208,8 +196,9 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'blockName' => 'woocommerce/product-regular-price-field', 'blockName' => 'woocommerce/product-regular-price-field',
'order' => 10, 'order' => 10,
'attributes' => [ 'attributes' => [
'name' => 'regular_price', 'name' => 'regular_price',
'label' => __( 'List price', 'woocommerce' ), 'label' => __( 'Regular price', 'woocommerce' ),
'isRequired' => true,
], ],
] ]
); );
@ -240,48 +229,12 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
'order' => 20, 'order' => 20,
] ]
); );
$product_pricing_section->add_block( $product_pricing_section->add_block(
[
'id' => 'product-sale-tax',
'blockName' => 'woocommerce/product-radio-field',
'order' => 30,
'attributes' => [
'title' => __( 'Charge sales tax on', 'woocommerce' ),
'property' => 'tax_status',
'options' => [
[
'label' => __( 'Product and shipping', 'woocommerce' ),
'value' => 'taxable',
],
[
'label' => __( 'Only shipping', 'woocommerce' ),
'value' => 'shipping',
],
[
'label' => __( "Don't charge tax", 'woocommerce' ),
'value' => 'none',
],
],
],
]
);
$pricing_advanced_block = $product_pricing_section->add_block(
[
'id' => 'product-pricing-advanced',
'blockName' => 'woocommerce/product-collapsible',
'order' => 40,
'attributes' => [
'toggleText' => __( 'Advanced', 'woocommerce' ),
'initialCollapsed' => true,
'persistRender' => true,
],
]
);
$pricing_advanced_block->add_block(
[ [
'id' => 'product-tax-class', 'id' => 'product-tax-class',
'blockName' => 'woocommerce/product-radio-field', 'blockName' => 'woocommerce/product-radio-field',
'order' => 10, 'order' => 40,
'attributes' => [ 'attributes' => [
'title' => __( 'Tax class', 'woocommerce' ), 'title' => __( 'Tax class', 'woocommerce' ),
'description' => sprintf( 'description' => sprintf(
@ -292,6 +245,10 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
), ),
'property' => 'tax_class', 'property' => 'tax_class',
'options' => [ 'options' => [
[
'label' => __( 'Same as main product', 'woocommerce' ),
'value' => 'parent',
],
[ [
'label' => __( 'Standard', 'woocommerce' ), 'label' => __( 'Standard', 'woocommerce' ),
'value' => '', 'value' => '',