Add product pricing block (#37211)

* Adding initial pricing block

* Have price block render in form

* Make sure price is loaded correctly and fix template rendering

* Make pricing block abstract and add list and sale price to template

* Add changelogs

* Revert changes in wc/data package

* Fix lint issues

* Fix type error

* Add styling

* Fix styling lint issues

* Revert config change missed in rebase

* Make use of base control help text for field info

* Allow additional callbacks for onFocus and onKeyUp
This commit is contained in:
louwie17 2023-03-17 14:03:10 -03:00 committed by GitHub
parent e370f25c0c
commit 345ad58919
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1685 additions and 894 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add new pricing block to the product editor package.

View File

@ -36,6 +36,7 @@
"@woocommerce/data": "workspace:^4.1.0",
"@woocommerce/navigation": "workspace:^8.1.0",
"@woocommerce/number": "workspace:*",
"@woocommerce/settings": "^1.0.0",
"@woocommerce/tracks": "workspace:^1.3.0",
"@wordpress/block-editor": "^9.8.0",
"@wordpress/blocks": "^12.3.0",

View File

@ -1,12 +1,20 @@
/**
* External dependencies
*/
import { registerCoreBlocks } from '@wordpress/block-library';
/**
* Internal dependencies
*/
import { init as initName } from '../details-name-block';
import { init as initSection } from '../section';
import { init as initTab } from '../tab';
import { init as initPricing } from '../pricing-block';
export const initBlocks = () => {
registerCoreBlocks();
initName();
initSection();
initTab();
initPricing();
};

View File

@ -0,0 +1,29 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-pricing",
"description": "A product price block with currency display.",
"title": "Product pricing",
"category": "widgets",
"keywords": [ "products", "price" ],
"textdomain": "default",
"attributes": {
"name": {
"type": "string"
},
"label": {
"type": "string"
},
"showPricingSection": {
"type": "boolean"
}
},
"supports": {
"align": false,
"html": false,
"multiple": false,
"reusable": false,
"inserter": false,
"lock": false
}
}

View File

@ -0,0 +1,89 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createElement, useContext, Fragment } from '@wordpress/element';
import interpolateComponents from '@automattic/interpolate-components';
import { Link } from '@woocommerce/components';
import { useBlockProps } from '@wordpress/block-editor';
import { useEntityProp } from '@wordpress/core-data';
import { BlockAttributes } from '@wordpress/blocks';
import { CurrencyContext } from '@woocommerce/currency';
import { getSetting } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
import {
BaseControl,
// @ts-expect-error `__experimentalInputControl` does exist.
__experimentalInputControl as InputControl,
} from '@wordpress/components';
/**
* Internal dependencies
*/
import { formatCurrencyDisplayValue } from '../../utils';
import { useCurrencyInputProps } from '../../hooks/use-currency-input-props';
export function Edit( { attributes }: { attributes: BlockAttributes } ) {
const blockProps = useBlockProps();
const { name, label, showPricingSection = false } = attributes;
const [ regularPrice, setRegularPrice ] = useEntityProp< string >(
'postType',
'product',
name
);
const context = useContext( CurrencyContext );
const { getCurrencyConfig, formatAmount } = context;
const currencyConfig = getCurrencyConfig();
const inputProps = useCurrencyInputProps( {
value: regularPrice,
setValue: setRegularPrice,
} );
const taxSettingsElement = showPricingSection
? interpolateComponents( {
mixedString: __(
'Manage more settings in {{link}}Pricing.{{/link}}',
'woocommerce'
),
components: {
link: (
<Link
href={ `${ getSetting(
'adminUrl'
) }admin.php?page=wc-settings&tab=tax` }
target="_blank"
type="external"
onClick={ () => {
recordEvent(
'product_pricing_list_price_help_tax_settings_click'
);
} }
>
<></>
</Link>
),
},
} )
: null;
return (
<div { ...blockProps }>
<BaseControl
id={ 'product_pricing_' + name }
help={ taxSettingsElement ? taxSettingsElement : '' }
>
<InputControl
name={ name }
onChange={ setRegularPrice }
label={ label || __( 'Price', 'woocommerce' ) }
value={ formatCurrencyDisplayValue(
String( regularPrice ),
currencyConfig,
formatAmount
) }
{ ...inputProps }
/>
</BaseControl>
</div>
);
}

View File

@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
import { initBlock } from '../../utils';
import metadata from './block.json';
import { Edit } from './edit';
const { name } = metadata;
export { metadata, name };
export const settings = {
example: {},
edit: Edit,
};
export const init = () => initBlock( { name, metadata, settings } );

View File

@ -1,2 +1,3 @@
export { useProductHelper as __experimentalUseProductHelper } from './use-product-helper';
export { useVariationsOrder as __experimentalUseVariationsOrder } from './use-variations-order';
export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './use-currency-input-props';

View File

@ -0,0 +1,77 @@
/**
* External dependencies
*/
import { CurrencyContext } from '@woocommerce/currency';
import { useContext } from '@wordpress/element';
/**
* Internal dependencies
*/
import { useProductHelper } from './use-product-helper';
export type CurrencyInputProps = {
prefix: string;
className: string;
sanitize: ( value: string | number ) => string;
onFocus: ( event: React.FocusEvent< HTMLInputElement > ) => void;
onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
};
type Props = {
value: string;
setValue: ( value: string ) => void;
onFocus?: ( event: React.FocusEvent< HTMLInputElement > ) => void;
onKeyUp?: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
};
export const useCurrencyInputProps = ( {
value,
setValue,
onFocus,
onKeyUp,
}: Props ) => {
const { sanitizePrice } = useProductHelper();
const context = useContext( CurrencyContext );
const { getCurrencyConfig } = context;
const currencyConfig = getCurrencyConfig();
const currencyInputProps: CurrencyInputProps = {
prefix: currencyConfig.symbol,
className: 'half-width-field components-currency-control',
sanitize: ( val: string | number ) => {
return sanitizePrice( String( val ) );
},
onFocus( event: React.FocusEvent< HTMLInputElement > ) {
// In some browsers like safari .select() function inside
// the onFocus event doesn't work as expected because it
// conflicts with onClick the first time user click the
// input. Using setTimeout defers the text selection and
// avoid the unexpected behaviour.
setTimeout(
function deferSelection( element: HTMLInputElement ) {
element.select();
},
0,
event.currentTarget
);
if ( onFocus ) {
onFocus( event );
}
},
onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) {
const amount = Number.parseFloat( sanitizePrice( value || '0' ) );
const step = Number( event.currentTarget.step || '1' );
if ( event.code === 'ArrowUp' ) {
setValue( String( amount + step ) );
}
if ( event.code === 'ArrowDown' ) {
setValue( String( amount - step ) );
}
if ( onKeyUp ) {
onKeyUp( event );
}
},
};
return currencyInputProps;
};

View File

@ -51,7 +51,9 @@ export const getProductStockStatus = (
}
if ( product.stock_status ) {
return PRODUCT_STOCK_STATUS_LABELS[ product.stock_status ];
return PRODUCT_STOCK_STATUS_LABELS[
product.stock_status as PRODUCT_STOCK_STATUS_KEYS
];
}
return PRODUCT_STOCK_STATUS_LABELS.instock;
@ -77,6 +79,8 @@ export const getProductStockStatusClass = (
return PRODUCT_STOCK_STATUS_CLASSES.outofstock;
}
return product.stock_status
? PRODUCT_STOCK_STATUS_CLASSES[ product.stock_status ]
? PRODUCT_STOCK_STATUS_CLASSES[
product.stock_status as PRODUCT_STOCK_STATUS_KEYS
]
: '';
};

View File

@ -6,4 +6,3 @@ declare global {
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
export {};

View File

@ -0,0 +1,18 @@
declare module '@woocommerce/settings' {
export declare function getAdminLink( path: string ): string;
export declare function getSetting< T >(
name: string,
fallback?: unknown,
filter = ( val: unknown, fb: unknown ) =>
typeof val !== 'undefined' ? val : fb
): T;
}
declare module '@wordpress/core-data' {
function useEntityProp< T = unknown >(
kind: string,
name: string,
prop: string,
id?: string
): [ T, ( value: T ) => void, T ];
}

View File

@ -0,0 +1,47 @@
.woocommerce-product-block-editor {
.components-input-control {
&__prefix {
margin-left: $gap-smaller;
}
&__suffix {
margin-right: $gap-smaller;
}
}
.components-currency-control {
.components-input-control__prefix {
color: $gray-700;
}
.components-input-control__input {
text-align: right;
}
}
.woocommerce-product-form {
&__custom-label-input {
display: flex;
flex-direction: column;
label {
display: block;
margin-bottom: $gap-smaller;
}
}
&__optional-input {
color: $gray-700;
}
}
.wp-block-columns {
gap: $gap-large;
}
.wp-block-woocommerce-product-section {
> .block-editor-inner-blocks > .block-editor-block-list__layout > .wp-block:not(:first-child) {
margin-top: $gap-large;
}
}
}

View File

@ -16,6 +16,7 @@ import { useParams } from 'react-router-dom';
* Internal dependencies
*/
import './product-page.scss';
import './product-block-page.scss';
declare const productBlockEditorSettings: ProductEditorSettings;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update product template by adding the list price and sale price blocks.

View File

@ -387,6 +387,43 @@ class WC_Post_Types {
'name' => 'Product name',
),
),
array(
'core/columns',
array(),
array(
array(
'core/column',
array(
'templateLock' => 'all',
),
array(
array(
'woocommerce/product-pricing',
array(
'name' => 'regular_price',
'label' => __( 'List price', 'woocommerce' ),
'showPricingSection' => true,
),
),
),
),
array(
'core/column',
array(
'templateLock' => 'all',
),
array(
array(
'woocommerce/product-pricing',
array(
'name' => 'sale_price',
'label' => __( 'Sale price', 'woocommerce' ),
),
),
),
),
),
),
),
),
),

File diff suppressed because it is too large Load Diff