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:
parent
e370f25c0c
commit
345ad58919
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add new pricing block to the product editor package.
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 } );
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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
|
||||
]
|
||||
: '';
|
||||
};
|
||||
|
|
|
@ -6,4 +6,3 @@ declare global {
|
|||
|
||||
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
|
||||
export {};
|
||||
|
||||
|
|
|
@ -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 ];
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update product template by adding the list price and sale price blocks.
|
|
@ -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' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
2237
pnpm-lock.yaml
2237
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue