Migrating pricing section in product editor to slot fills (#36500)
* Adding changelogs * Migrating pricing section in product editor to slot fills * Adding slot and plugarea to variation form * Removing obsolete pricing section files
This commit is contained in:
parent
0beb6b7503
commit
4341a53144
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: fix
|
||||||
|
|
||||||
|
Altering styles to correctly target fields within slot fills on product editor.
|
|
@ -11,9 +11,9 @@
|
||||||
&__body {
|
&__body {
|
||||||
padding: $gap-large;
|
padding: $gap-large;
|
||||||
|
|
||||||
> .components-base-control,
|
.components-base-control,
|
||||||
> .components-dropdown,
|
.components-dropdown,
|
||||||
> .woocommerce-rich-text-editor {
|
.woocommerce-rich-text-editor {
|
||||||
&:not(:first-child):not(.components-radio-control) {
|
&:not(:first-child):not(.components-radio-control) {
|
||||||
margin-top: $gap-large - $gap-smaller;
|
margin-top: $gap-large - $gap-smaller;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export const PRODUCT_DETAILS_SLUG = 'product-details';
|
export const PRICING_SECTION_BASIC_ID = 'pricing/basic';
|
||||||
|
export const PRICING_SECTION_TAXES_ID = 'pricing/taxes';
|
||||||
export const DETAILS_SECTION_ID = 'general/details';
|
export const DETAILS_SECTION_ID = 'general/details';
|
||||||
export const INVENTORY_SECTION_ID = 'inventory/inventory';
|
export const INVENTORY_SECTION_ID = 'inventory/inventory';
|
||||||
export const INVENTORY_SECTION_ADVANCED_ID = 'inventory/advanced';
|
export const INVENTORY_SECTION_ADVANCED_ID = 'inventory/advanced';
|
||||||
|
@ -11,5 +11,7 @@ export const SHIPPING_SECTION_DIMENSIONS_ID = 'shipping/dimensions';
|
||||||
export const TAB_INVENTORY_ID = 'tab/inventory';
|
export const TAB_INVENTORY_ID = 'tab/inventory';
|
||||||
export const TAB_GENERAL_ID = 'tab/general';
|
export const TAB_GENERAL_ID = 'tab/general';
|
||||||
export const TAB_SHIPPING_ID = 'tab/shipping';
|
export const TAB_SHIPPING_ID = 'tab/shipping';
|
||||||
|
export const TAB_PRICING_ID = 'tab/pricing';
|
||||||
|
|
||||||
export const PLUGIN_ID = 'woocommerce';
|
export const PLUGIN_ID = 'woocommerce';
|
||||||
|
export const PRODUCT_DETAILS_SLUG = 'product-details';
|
||||||
|
|
|
@ -8,3 +8,4 @@ export * from './details-section/details-section-fills';
|
||||||
export * from './images-section/images-section-fills';
|
export * from './images-section/images-section-fills';
|
||||||
export * from './attributes-section/attributes-section-fills';
|
export * from './attributes-section/attributes-section-fills';
|
||||||
export * from './inventory-section/inventory-section-fills';
|
export * from './inventory-section/inventory-section-fills';
|
||||||
|
export * from './pricing-section/pricing-section-fills';
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './pricing-field-list';
|
||||||
|
export * from './pricing-field-sale';
|
||||||
|
export * from './pricing-field-taxes-charge';
|
||||||
|
export * from './pricing-field-taxes-class';
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useFormContext, Link } from '@woocommerce/components';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
import { useContext } from '@wordpress/element';
|
||||||
|
import { Product, SETTINGS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import interpolateComponents from '@automattic/interpolate-components';
|
||||||
|
import {
|
||||||
|
BaseControl,
|
||||||
|
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||||
|
__experimentalInputControl as InputControl,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { CurrencyInputProps } from './pricing-section-fills';
|
||||||
|
import { formatCurrencyDisplayValue } from '../../sections/utils';
|
||||||
|
import { CurrencyContext } from '../../../lib/currency-context';
|
||||||
|
import { ADMIN_URL } from '~/utils/admin-settings';
|
||||||
|
|
||||||
|
type PricingListFieldProps = {
|
||||||
|
currencyInputProps: CurrencyInputProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PricingListField: React.FC< PricingListFieldProps > = ( {
|
||||||
|
currencyInputProps,
|
||||||
|
} ) => {
|
||||||
|
const { getInputProps } = useFormContext< Product >();
|
||||||
|
const context = useContext( CurrencyContext );
|
||||||
|
const { getCurrencyConfig, formatAmount } = context;
|
||||||
|
const currencyConfig = getCurrencyConfig();
|
||||||
|
|
||||||
|
const { isResolving: isTaxSettingsResolving, taxSettings } = useSelect(
|
||||||
|
( select ) => {
|
||||||
|
const { getSettings, hasFinishedResolution } =
|
||||||
|
select( SETTINGS_STORE_NAME );
|
||||||
|
return {
|
||||||
|
isResolving: ! hasFinishedResolution( 'getSettings', [
|
||||||
|
'tax',
|
||||||
|
] ),
|
||||||
|
taxSettings: getSettings( 'tax' ).tax || {},
|
||||||
|
taxesEnabled:
|
||||||
|
getSettings( 'general' )?.general
|
||||||
|
?.woocommerce_calc_taxes === 'yes',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const regularPriceProps = getInputProps(
|
||||||
|
'regular_price',
|
||||||
|
currencyInputProps
|
||||||
|
);
|
||||||
|
|
||||||
|
const taxIncludedInPriceText = __(
|
||||||
|
'Per your {{link}}store settings{{/link}}, tax is {{strong}}included{{/strong}} in the price.',
|
||||||
|
'woocommerce'
|
||||||
|
);
|
||||||
|
const taxNotIncludedInPriceText = __(
|
||||||
|
'Per your {{link}}store settings{{/link}}, tax is {{strong}}not included{{/strong}} in the price.',
|
||||||
|
'woocommerce'
|
||||||
|
);
|
||||||
|
const pricesIncludeTax =
|
||||||
|
taxSettings.woocommerce_prices_include_tax === 'yes';
|
||||||
|
|
||||||
|
const taxSettingsElement = interpolateComponents( {
|
||||||
|
mixedString: pricesIncludeTax
|
||||||
|
? taxIncludedInPriceText
|
||||||
|
: taxNotIncludedInPriceText,
|
||||||
|
components: {
|
||||||
|
link: (
|
||||||
|
<Link
|
||||||
|
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=tax` }
|
||||||
|
target="_blank"
|
||||||
|
type="external"
|
||||||
|
onClick={ () => {
|
||||||
|
recordEvent(
|
||||||
|
'product_pricing_list_price_help_tax_settings_click'
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
strong: <strong />,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseControl
|
||||||
|
id="product_pricing_regular_price"
|
||||||
|
help={ regularPriceProps?.help ?? '' }
|
||||||
|
>
|
||||||
|
<InputControl
|
||||||
|
{ ...regularPriceProps }
|
||||||
|
name="regular_price"
|
||||||
|
label={ __( 'List price', 'woocommerce' ) }
|
||||||
|
value={ formatCurrencyDisplayValue(
|
||||||
|
String( regularPriceProps?.value ),
|
||||||
|
currencyConfig,
|
||||||
|
formatAmount
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
</BaseControl>
|
||||||
|
{ ! isTaxSettingsResolving && (
|
||||||
|
<span className="woocommerce-product-form__secondary-text">
|
||||||
|
{ taxSettingsElement }
|
||||||
|
</span>
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,213 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
useFormContext,
|
||||||
|
Link,
|
||||||
|
__experimentalTooltip as Tooltip,
|
||||||
|
DateTimePickerControl,
|
||||||
|
} from '@woocommerce/components';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
import { useContext, useState, useEffect } from '@wordpress/element';
|
||||||
|
import { Product, OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import interpolateComponents from '@automattic/interpolate-components';
|
||||||
|
import { format as formatDate } from '@wordpress/date';
|
||||||
|
import moment from 'moment';
|
||||||
|
import {
|
||||||
|
BaseControl,
|
||||||
|
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||||
|
__experimentalInputControl as InputControl,
|
||||||
|
ToggleControl,
|
||||||
|
} from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { CurrencyInputProps } from './pricing-section-fills';
|
||||||
|
import { formatCurrencyDisplayValue } from '../../sections/utils';
|
||||||
|
import { CurrencyContext } from '../../../lib/currency-context';
|
||||||
|
|
||||||
|
type PricingListFieldProps = {
|
||||||
|
currencyInputProps: CurrencyInputProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRODUCT_SCHEDULED_SALE_SLUG = 'product-scheduled-sale';
|
||||||
|
|
||||||
|
export const PricingSaleField: React.FC< PricingListFieldProps > = ( {
|
||||||
|
currencyInputProps,
|
||||||
|
} ) => {
|
||||||
|
const { getInputProps, values, setValues } = useFormContext< Product >();
|
||||||
|
|
||||||
|
const { dateFormat, timeFormat } = useSelect( ( select ) => {
|
||||||
|
const { getOption } = select( OPTIONS_STORE_NAME );
|
||||||
|
return {
|
||||||
|
dateFormat: ( getOption( 'date_format' ) as string ) || 'F j, Y',
|
||||||
|
timeFormat: ( getOption( 'time_format' ) as string ) || 'H:i',
|
||||||
|
};
|
||||||
|
} );
|
||||||
|
|
||||||
|
const context = useContext( CurrencyContext );
|
||||||
|
const { getCurrencyConfig, formatAmount } = context;
|
||||||
|
const currencyConfig = getCurrencyConfig();
|
||||||
|
|
||||||
|
const [ showSaleSchedule, setShowSaleSchedule ] = useState( false );
|
||||||
|
const [ userToggledSaleSchedule, setUserToggledSaleSchedule ] =
|
||||||
|
useState( false );
|
||||||
|
const [ autoToggledSaleSchedule, setAutoToggledSaleSchedule ] =
|
||||||
|
useState( false );
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
if ( userToggledSaleSchedule || autoToggledSaleSchedule ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDateOnSaleFrom =
|
||||||
|
typeof values.date_on_sale_from_gmt === 'string' &&
|
||||||
|
values.date_on_sale_from_gmt.length > 0;
|
||||||
|
const hasDateOnSaleTo =
|
||||||
|
typeof values.date_on_sale_to_gmt === 'string' &&
|
||||||
|
values.date_on_sale_to_gmt.length > 0;
|
||||||
|
|
||||||
|
const hasSaleSchedule = hasDateOnSaleFrom || hasDateOnSaleTo;
|
||||||
|
|
||||||
|
if ( hasSaleSchedule ) {
|
||||||
|
setAutoToggledSaleSchedule( true );
|
||||||
|
setShowSaleSchedule( true );
|
||||||
|
}
|
||||||
|
}, [ userToggledSaleSchedule, autoToggledSaleSchedule, values ] );
|
||||||
|
|
||||||
|
const salePriceProps = getInputProps( 'sale_price', currencyInputProps );
|
||||||
|
|
||||||
|
const dateTimePickerProps = {
|
||||||
|
className: 'woocommerce-product__date-time-picker',
|
||||||
|
isDateOnlyPicker: true,
|
||||||
|
dateTimeFormat: dateFormat,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSaleScheduleToggleChange = ( value: boolean ) => {
|
||||||
|
recordEvent( 'product_pricing_schedule_sale_toggle_click', {
|
||||||
|
enabled: value,
|
||||||
|
} );
|
||||||
|
|
||||||
|
setUserToggledSaleSchedule( true );
|
||||||
|
setShowSaleSchedule( value );
|
||||||
|
|
||||||
|
if ( value ) {
|
||||||
|
setValues( {
|
||||||
|
date_on_sale_from_gmt: moment().startOf( 'day' ).toISOString(),
|
||||||
|
date_on_sale_to_gmt: null,
|
||||||
|
} as Product );
|
||||||
|
} else {
|
||||||
|
setValues( {
|
||||||
|
date_on_sale_from_gmt: null,
|
||||||
|
date_on_sale_to_gmt: null,
|
||||||
|
} as Product );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseControl
|
||||||
|
id="product_pricing_sale_price"
|
||||||
|
help={ salePriceProps?.help ?? '' }
|
||||||
|
>
|
||||||
|
<InputControl
|
||||||
|
{ ...salePriceProps }
|
||||||
|
name="sale_price"
|
||||||
|
label={ __( 'Sale price', 'woocommerce' ) }
|
||||||
|
value={ formatCurrencyDisplayValue(
|
||||||
|
String( salePriceProps?.value ),
|
||||||
|
currencyConfig,
|
||||||
|
formatAmount
|
||||||
|
) }
|
||||||
|
/>
|
||||||
|
</BaseControl>
|
||||||
|
|
||||||
|
<ToggleControl
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
{ __( 'Schedule sale', 'woocommerce' ) }
|
||||||
|
<Tooltip
|
||||||
|
text={ interpolateComponents( {
|
||||||
|
mixedString: __(
|
||||||
|
'The sale will start at the beginning of the "From" date ({{startTime/}}) and expire at the end of the "To" date ({{endTime/}}). {{moreLink/}}',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
components: {
|
||||||
|
startTime: (
|
||||||
|
<span>
|
||||||
|
{ formatDate(
|
||||||
|
timeFormat,
|
||||||
|
moment().startOf( 'day' )
|
||||||
|
) }
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
endTime: (
|
||||||
|
<span>
|
||||||
|
{ formatDate(
|
||||||
|
timeFormat,
|
||||||
|
moment().endOf( 'day' )
|
||||||
|
) }
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
moreLink: (
|
||||||
|
<Link
|
||||||
|
href="https://woocommerce.com/document/managing-products/#product-data"
|
||||||
|
target="_blank"
|
||||||
|
type="external"
|
||||||
|
onClick={ () =>
|
||||||
|
recordEvent(
|
||||||
|
'add_product_learn_more',
|
||||||
|
{
|
||||||
|
category:
|
||||||
|
PRODUCT_SCHEDULED_SALE_SLUG,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{ __(
|
||||||
|
'Learn more',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
} ) }
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
checked={ showSaleSchedule }
|
||||||
|
onChange={ onSaleScheduleToggleChange }
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore disabled prop exists
|
||||||
|
disabled={ ! ( values.sale_price?.length > 0 ) }
|
||||||
|
/>
|
||||||
|
|
||||||
|
{ showSaleSchedule && (
|
||||||
|
<>
|
||||||
|
<DateTimePickerControl
|
||||||
|
label={ __( 'From', 'woocommerce' ) }
|
||||||
|
placeholder={ __( 'Now', 'woocommerce' ) }
|
||||||
|
timeForDateOnly={ 'start-of-day' }
|
||||||
|
currentDate={ values.date_on_sale_from_gmt }
|
||||||
|
{ ...getInputProps( 'date_on_sale_from_gmt', {
|
||||||
|
...dateTimePickerProps,
|
||||||
|
} ) }
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateTimePickerControl
|
||||||
|
label={ __( 'To', 'woocommerce' ) }
|
||||||
|
placeholder={ __( 'No end date', 'woocommerce' ) }
|
||||||
|
timeForDateOnly={ 'end-of-day' }
|
||||||
|
currentDate={ values.date_on_sale_to_gmt }
|
||||||
|
{ ...getInputProps( 'date_on_sale_to_gmt', {
|
||||||
|
...dateTimePickerProps,
|
||||||
|
} ) }
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { useFormContext } from '@woocommerce/components';
|
||||||
|
import { Product } from '@woocommerce/data';
|
||||||
|
import { RadioControl } from '@wordpress/components';
|
||||||
|
|
||||||
|
export const PricingTaxesChargeField = () => {
|
||||||
|
const { getInputProps } = useFormContext< Product >();
|
||||||
|
|
||||||
|
const taxStatusProps = getInputProps( 'tax_status' );
|
||||||
|
// These properties cause issues with the RadioControl component.
|
||||||
|
// A fix to form upstream would help if we can identify what type of input is used.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
delete taxStatusProps.checked;
|
||||||
|
delete taxStatusProps.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioControl
|
||||||
|
{ ...taxStatusProps }
|
||||||
|
label={ __( 'Charge sales tax on', 'woocommerce' ) }
|
||||||
|
options={ [
|
||||||
|
{
|
||||||
|
label: __( 'Product and shipping', 'woocommerce' ),
|
||||||
|
value: 'taxable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Only shipping', 'woocommerce' ),
|
||||||
|
value: 'shipping',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( `Don't charge tax`, 'woocommerce' ),
|
||||||
|
value: 'none',
|
||||||
|
},
|
||||||
|
] }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
useFormContext,
|
||||||
|
CollapsibleContent,
|
||||||
|
Link,
|
||||||
|
} from '@woocommerce/components';
|
||||||
|
import {
|
||||||
|
Product,
|
||||||
|
EXPERIMENTAL_TAX_CLASSES_STORE_NAME,
|
||||||
|
TaxClass,
|
||||||
|
} from '@woocommerce/data';
|
||||||
|
import { useSelect } from '@wordpress/data';
|
||||||
|
import { RadioControl } from '@wordpress/components';
|
||||||
|
import interpolateComponents from '@automattic/interpolate-components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { STANDARD_RATE_TAX_CLASS_SLUG } from '../../constants';
|
||||||
|
|
||||||
|
export const PricingTaxesClassField = () => {
|
||||||
|
const { getInputProps } = useFormContext< Product >();
|
||||||
|
|
||||||
|
const { isResolving: isTaxClassesResolving, taxClasses } = useSelect(
|
||||||
|
( select ) => {
|
||||||
|
const { hasFinishedResolution, getTaxClasses } = select(
|
||||||
|
EXPERIMENTAL_TAX_CLASSES_STORE_NAME
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isResolving: ! hasFinishedResolution( 'getTaxClasses' ),
|
||||||
|
taxClasses: getTaxClasses< TaxClass[] >(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const taxClassProps = getInputProps( 'tax_class' );
|
||||||
|
// These properties cause issues with the RadioControl component.
|
||||||
|
// A fix to form upstream would help if we can identify what type of input is used.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
delete taxClassProps.checked;
|
||||||
|
delete taxClassProps.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsibleContent toggleText={ __( 'Advanced', 'woocommerce' ) }>
|
||||||
|
{ ! isTaxClassesResolving && taxClasses.length > 0 && (
|
||||||
|
<RadioControl
|
||||||
|
{ ...taxClassProps }
|
||||||
|
label={
|
||||||
|
<>
|
||||||
|
<span>{ __( 'Tax class', 'woocommerce' ) }</span>
|
||||||
|
<span className="woocommerce-product-form__secondary-text">
|
||||||
|
{ interpolateComponents( {
|
||||||
|
mixedString: __(
|
||||||
|
'Apply a tax rate if this product qualifies for tax reduction or exemption. {{link}}Learn more{{/link}}',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
components: {
|
||||||
|
link: (
|
||||||
|
<Link
|
||||||
|
href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class"
|
||||||
|
target="_blank"
|
||||||
|
type="external"
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
} ) }
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
options={ taxClasses.map( ( taxClass ) => ( {
|
||||||
|
label: taxClass.name,
|
||||||
|
value:
|
||||||
|
taxClass.slug === STANDARD_RATE_TAX_CLASS_SLUG
|
||||||
|
? ''
|
||||||
|
: taxClass.slug,
|
||||||
|
} ) ) }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</CollapsibleContent>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,187 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import {
|
||||||
|
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||||
|
__experimentalWooProductFieldItem as WooProductFieldItem,
|
||||||
|
__experimentalProductSectionLayout as ProductSectionLayout,
|
||||||
|
Link,
|
||||||
|
useFormContext,
|
||||||
|
} from '@woocommerce/components';
|
||||||
|
import { registerPlugin } from '@wordpress/plugins';
|
||||||
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
import { Product } from '@woocommerce/data';
|
||||||
|
import { useContext } from '@wordpress/element';
|
||||||
|
import { Card, CardBody } from '@wordpress/components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
PricingListField,
|
||||||
|
PricingSaleField,
|
||||||
|
PricingTaxesClassField,
|
||||||
|
PricingTaxesChargeField,
|
||||||
|
} from './index';
|
||||||
|
import { useProductHelper } from '../../use-product-helper';
|
||||||
|
import {
|
||||||
|
PRICING_SECTION_BASIC_ID,
|
||||||
|
PRICING_SECTION_TAXES_ID,
|
||||||
|
TAB_PRICING_ID,
|
||||||
|
PLUGIN_ID,
|
||||||
|
} from '../constants';
|
||||||
|
import { CurrencyContext } from '../../../lib/currency-context';
|
||||||
|
|
||||||
|
import './pricing-section.scss';
|
||||||
|
|
||||||
|
export type CurrencyInputProps = {
|
||||||
|
prefix: string;
|
||||||
|
className: string;
|
||||||
|
sanitize: ( value: Product[ keyof Product ] ) => string;
|
||||||
|
onFocus: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
||||||
|
onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PricingSection = () => {
|
||||||
|
const { setValues, values } = useFormContext< Product >();
|
||||||
|
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: ( value: Product[ keyof Product ] ) => {
|
||||||
|
return sanitizePrice( String( value ) );
|
||||||
|
},
|
||||||
|
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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) {
|
||||||
|
const name = event.currentTarget.name as keyof Pick<
|
||||||
|
Product,
|
||||||
|
'regular_price' | 'sale_price'
|
||||||
|
>;
|
||||||
|
const amount = Number.parseFloat(
|
||||||
|
sanitizePrice( values[ name ] || '0' )
|
||||||
|
);
|
||||||
|
const step = Number( event.currentTarget.step || '1' );
|
||||||
|
if ( event.code === 'ArrowUp' ) {
|
||||||
|
setValues( {
|
||||||
|
[ name ]: String( amount + step ),
|
||||||
|
} as unknown as Product );
|
||||||
|
}
|
||||||
|
if ( event.code === 'ArrowDown' ) {
|
||||||
|
setValues( {
|
||||||
|
[ name ]: String( amount - step ),
|
||||||
|
} as unknown as Product );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WooProductSectionItem
|
||||||
|
id={ PRICING_SECTION_BASIC_ID }
|
||||||
|
location={ TAB_PRICING_ID }
|
||||||
|
pluginId={ PLUGIN_ID }
|
||||||
|
order={ 1 }
|
||||||
|
>
|
||||||
|
<ProductSectionLayout
|
||||||
|
title={ __( 'Pricing', 'woocommerce' ) }
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{ __(
|
||||||
|
'Set a competitive price, put the product on sale, and manage tax calculations.',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</span>
|
||||||
|
<Link
|
||||||
|
className="woocommerce-form-section__header-link"
|
||||||
|
href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/"
|
||||||
|
target="_blank"
|
||||||
|
type="external"
|
||||||
|
onClick={ () => {
|
||||||
|
recordEvent( 'add_product_pricing_help' );
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ __(
|
||||||
|
'How to price your product: expert tips',
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<WooProductFieldItem.Slot
|
||||||
|
section={ PRICING_SECTION_BASIC_ID }
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<WooProductFieldItem.Slot
|
||||||
|
section={ PRICING_SECTION_TAXES_ID }
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</ProductSectionLayout>
|
||||||
|
</WooProductSectionItem>
|
||||||
|
<WooProductFieldItem
|
||||||
|
id="pricing/list"
|
||||||
|
section={ PRICING_SECTION_BASIC_ID }
|
||||||
|
pluginId={ PLUGIN_ID }
|
||||||
|
order={ 1 }
|
||||||
|
>
|
||||||
|
<PricingListField currencyInputProps={ currencyInputProps } />
|
||||||
|
</WooProductFieldItem>
|
||||||
|
<WooProductFieldItem
|
||||||
|
id="pricing/sale"
|
||||||
|
section={ PRICING_SECTION_BASIC_ID }
|
||||||
|
pluginId={ PLUGIN_ID }
|
||||||
|
order={ 3 }
|
||||||
|
>
|
||||||
|
<PricingSaleField currencyInputProps={ currencyInputProps } />
|
||||||
|
</WooProductFieldItem>
|
||||||
|
<WooProductFieldItem
|
||||||
|
id="pricing/taxes/charge"
|
||||||
|
section={ PRICING_SECTION_TAXES_ID }
|
||||||
|
pluginId={ PLUGIN_ID }
|
||||||
|
order={ 1 }
|
||||||
|
>
|
||||||
|
<PricingTaxesChargeField />
|
||||||
|
</WooProductFieldItem>
|
||||||
|
<WooProductFieldItem
|
||||||
|
id="pricing/taxes/class"
|
||||||
|
section={ PRICING_SECTION_TAXES_ID }
|
||||||
|
pluginId={ PLUGIN_ID }
|
||||||
|
order={ 3 }
|
||||||
|
>
|
||||||
|
<PricingTaxesClassField />
|
||||||
|
</WooProductFieldItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPlugin( 'wc-admin-product-editor-pricing-section', {
|
||||||
|
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
|
||||||
|
scope: 'woocommerce-product-editor',
|
||||||
|
render: () => <PricingSection />,
|
||||||
|
} );
|
|
@ -16,7 +16,6 @@ import { Ref } from 'react';
|
||||||
*/
|
*/
|
||||||
import { ProductFormHeader } from './layout/product-form-header';
|
import { ProductFormHeader } from './layout/product-form-header';
|
||||||
import { ProductFormLayout } from './layout/product-form-layout';
|
import { ProductFormLayout } from './layout/product-form-layout';
|
||||||
import { PricingSection } from './sections/pricing-section';
|
|
||||||
import { ProductVariationsSection } from './sections/product-variations-section';
|
import { ProductVariationsSection } from './sections/product-variations-section';
|
||||||
import { validate } from './product-validation';
|
import { validate } from './product-validation';
|
||||||
import { OptionsSection } from './sections/options-section';
|
import { OptionsSection } from './sections/options-section';
|
||||||
|
@ -26,6 +25,7 @@ import {
|
||||||
TAB_GENERAL_ID,
|
TAB_GENERAL_ID,
|
||||||
TAB_SHIPPING_ID,
|
TAB_SHIPPING_ID,
|
||||||
TAB_INVENTORY_ID,
|
TAB_INVENTORY_ID,
|
||||||
|
TAB_PRICING_ID,
|
||||||
} from './fills/constants';
|
} from './fills/constants';
|
||||||
|
|
||||||
export const ProductForm: React.FC< {
|
export const ProductForm: React.FC< {
|
||||||
|
@ -60,7 +60,9 @@ export const ProductForm: React.FC< {
|
||||||
title="Pricing"
|
title="Pricing"
|
||||||
disabled={ !! product?.variations?.length }
|
disabled={ !! product?.variations?.length }
|
||||||
>
|
>
|
||||||
<PricingSection />
|
<WooProductSectionItem.Slot
|
||||||
|
location={ TAB_PRICING_ID }
|
||||||
|
/>
|
||||||
</ProductFormTab>
|
</ProductFormTab>
|
||||||
<ProductFormTab
|
<ProductFormTab
|
||||||
name="inventory"
|
name="inventory"
|
||||||
|
|
|
@ -19,11 +19,14 @@ import PostsNavigation from './shared/posts-navigation';
|
||||||
import { ProductFormLayout } from './layout/product-form-layout';
|
import { ProductFormLayout } from './layout/product-form-layout';
|
||||||
import { ProductFormFooter } from './layout/product-form-footer';
|
import { ProductFormFooter } from './layout/product-form-footer';
|
||||||
import { ProductFormTab } from './product-form-tab';
|
import { ProductFormTab } from './product-form-tab';
|
||||||
import { PricingSection } from './sections/pricing-section';
|
|
||||||
import { ProductVariationDetailsSection } from './sections/product-variation-details-section';
|
import { ProductVariationDetailsSection } from './sections/product-variation-details-section';
|
||||||
import { ProductVariationFormHeader } from './layout/product-variation-form-header';
|
import { ProductVariationFormHeader } from './layout/product-variation-form-header';
|
||||||
import useProductVariationNavigation from './hooks/use-product-variation-navigation';
|
import useProductVariationNavigation from './hooks/use-product-variation-navigation';
|
||||||
import { TAB_INVENTORY_ID, TAB_SHIPPING_ID } from './fills/constants';
|
import {
|
||||||
|
TAB_INVENTORY_ID,
|
||||||
|
TAB_SHIPPING_ID,
|
||||||
|
TAB_PRICING_ID,
|
||||||
|
} from './fills/constants';
|
||||||
|
|
||||||
import './product-variation-form.scss';
|
import './product-variation-form.scss';
|
||||||
|
|
||||||
|
@ -62,7 +65,9 @@ export const ProductVariationForm: React.FC< {
|
||||||
<ProductVariationDetailsSection />
|
<ProductVariationDetailsSection />
|
||||||
</ProductFormTab>
|
</ProductFormTab>
|
||||||
<ProductFormTab name="pricing" title="Pricing">
|
<ProductFormTab name="pricing" title="Pricing">
|
||||||
<PricingSection />
|
<WooProductSectionItem.Slot
|
||||||
|
location={ TAB_PRICING_ID }
|
||||||
|
/>
|
||||||
</ProductFormTab>
|
</ProductFormTab>
|
||||||
<ProductFormTab name="inventory" title="Inventory">
|
<ProductFormTab name="inventory" title="Inventory">
|
||||||
<WooProductSectionItem.Slot
|
<WooProductSectionItem.Slot
|
||||||
|
|
|
@ -1,480 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import {
|
|
||||||
CollapsibleContent,
|
|
||||||
DateTimePickerControl,
|
|
||||||
Link,
|
|
||||||
useFormContext,
|
|
||||||
__experimentalTooltip as Tooltip,
|
|
||||||
} from '@woocommerce/components';
|
|
||||||
import {
|
|
||||||
Product,
|
|
||||||
OPTIONS_STORE_NAME,
|
|
||||||
SETTINGS_STORE_NAME,
|
|
||||||
EXPERIMENTAL_TAX_CLASSES_STORE_NAME,
|
|
||||||
TaxClass,
|
|
||||||
} from '@woocommerce/data';
|
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
|
||||||
import { useContext, useEffect, useState } from '@wordpress/element';
|
|
||||||
import { useSelect } from '@wordpress/data';
|
|
||||||
import { format as formatDate } from '@wordpress/date';
|
|
||||||
import moment from 'moment';
|
|
||||||
import interpolateComponents from '@automattic/interpolate-components';
|
|
||||||
import {
|
|
||||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
|
||||||
__experimentalInputControl as InputControl,
|
|
||||||
BaseControl,
|
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
ToggleControl,
|
|
||||||
RadioControl,
|
|
||||||
} from '@wordpress/components';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import './pricing-section.scss';
|
|
||||||
import { formatCurrencyDisplayValue } from './utils';
|
|
||||||
import { ProductSectionLayout } from '../layout/product-section-layout';
|
|
||||||
import { ADMIN_URL } from '../../utils/admin-settings';
|
|
||||||
import { CurrencyContext } from '../../lib/currency-context';
|
|
||||||
import { useProductHelper } from '../use-product-helper';
|
|
||||||
import { STANDARD_RATE_TAX_CLASS_SLUG } from '../constants';
|
|
||||||
|
|
||||||
const PRODUCT_SCHEDULED_SALE_SLUG = 'product-scheduled-sale';
|
|
||||||
|
|
||||||
export const PricingSection: React.FC = () => {
|
|
||||||
const { sanitizePrice } = useProductHelper();
|
|
||||||
const { getInputProps, setValues, values } = useFormContext< Product >();
|
|
||||||
const [ showSaleSchedule, setShowSaleSchedule ] = useState( false );
|
|
||||||
const [ userToggledSaleSchedule, setUserToggledSaleSchedule ] =
|
|
||||||
useState( false );
|
|
||||||
const [ autoToggledSaleSchedule, setAutoToggledSaleSchedule ] =
|
|
||||||
useState( false );
|
|
||||||
const {
|
|
||||||
isResolving: isTaxSettingsResolving,
|
|
||||||
taxSettings,
|
|
||||||
taxesEnabled,
|
|
||||||
} = useSelect( ( select ) => {
|
|
||||||
const { getSettings, hasFinishedResolution } =
|
|
||||||
select( SETTINGS_STORE_NAME );
|
|
||||||
return {
|
|
||||||
isResolving: ! hasFinishedResolution( 'getSettings', [ 'tax' ] ),
|
|
||||||
taxSettings: getSettings( 'tax' ).tax || {},
|
|
||||||
taxesEnabled:
|
|
||||||
getSettings( 'general' )?.general?.woocommerce_calc_taxes ===
|
|
||||||
'yes',
|
|
||||||
};
|
|
||||||
} );
|
|
||||||
|
|
||||||
const { isResolving: isTaxClassesResolving, taxClasses } = useSelect(
|
|
||||||
( select ) => {
|
|
||||||
const { hasFinishedResolution, getTaxClasses } = select(
|
|
||||||
EXPERIMENTAL_TAX_CLASSES_STORE_NAME
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
isResolving: ! hasFinishedResolution( 'getTaxClasses' ),
|
|
||||||
taxClasses: getTaxClasses< TaxClass[] >(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const pricesIncludeTax =
|
|
||||||
taxSettings.woocommerce_prices_include_tax === 'yes';
|
|
||||||
const context = useContext( CurrencyContext );
|
|
||||||
const { getCurrencyConfig, formatAmount } = context;
|
|
||||||
const currencyConfig = getCurrencyConfig();
|
|
||||||
|
|
||||||
const taxIncludedInPriceText = __(
|
|
||||||
'Per your {{link}}store settings{{/link}}, tax is {{strong}}included{{/strong}} in the price.',
|
|
||||||
'woocommerce'
|
|
||||||
);
|
|
||||||
const taxNotIncludedInPriceText = __(
|
|
||||||
'Per your {{link}}store settings{{/link}}, tax is {{strong}}not included{{/strong}} in the price.',
|
|
||||||
'woocommerce'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { dateFormat, timeFormat } = useSelect( ( select ) => {
|
|
||||||
const { getOption } = select( OPTIONS_STORE_NAME );
|
|
||||||
return {
|
|
||||||
dateFormat: ( getOption( 'date_format' ) as string ) || 'F j, Y',
|
|
||||||
timeFormat: ( getOption( 'time_format' ) as string ) || 'H:i',
|
|
||||||
};
|
|
||||||
} );
|
|
||||||
|
|
||||||
const onSaleScheduleToggleChange = ( value: boolean ) => {
|
|
||||||
recordEvent( 'product_pricing_schedule_sale_toggle_click', {
|
|
||||||
enabled: value,
|
|
||||||
} );
|
|
||||||
|
|
||||||
setUserToggledSaleSchedule( true );
|
|
||||||
setShowSaleSchedule( value );
|
|
||||||
|
|
||||||
if ( value ) {
|
|
||||||
setValues( {
|
|
||||||
date_on_sale_from_gmt: moment().startOf( 'day' ).toISOString(),
|
|
||||||
date_on_sale_to_gmt: null,
|
|
||||||
} as Product );
|
|
||||||
} else {
|
|
||||||
setValues( {
|
|
||||||
date_on_sale_from_gmt: null,
|
|
||||||
date_on_sale_to_gmt: null,
|
|
||||||
} as Product );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect( () => {
|
|
||||||
if ( userToggledSaleSchedule || autoToggledSaleSchedule ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasDateOnSaleFrom =
|
|
||||||
typeof values.date_on_sale_from_gmt === 'string' &&
|
|
||||||
values.date_on_sale_from_gmt.length > 0;
|
|
||||||
const hasDateOnSaleTo =
|
|
||||||
typeof values.date_on_sale_to_gmt === 'string' &&
|
|
||||||
values.date_on_sale_to_gmt.length > 0;
|
|
||||||
|
|
||||||
const hasSaleSchedule = hasDateOnSaleFrom || hasDateOnSaleTo;
|
|
||||||
|
|
||||||
if ( hasSaleSchedule ) {
|
|
||||||
setAutoToggledSaleSchedule( true );
|
|
||||||
setShowSaleSchedule( true );
|
|
||||||
}
|
|
||||||
}, [ userToggledSaleSchedule, autoToggledSaleSchedule, values ] );
|
|
||||||
|
|
||||||
const taxSettingsElement = interpolateComponents( {
|
|
||||||
mixedString: pricesIncludeTax
|
|
||||||
? taxIncludedInPriceText
|
|
||||||
: taxNotIncludedInPriceText,
|
|
||||||
components: {
|
|
||||||
link: (
|
|
||||||
<Link
|
|
||||||
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=tax` }
|
|
||||||
target="_blank"
|
|
||||||
type="external"
|
|
||||||
onClick={ () => {
|
|
||||||
recordEvent(
|
|
||||||
'product_pricing_list_price_help_tax_settings_click'
|
|
||||||
);
|
|
||||||
} }
|
|
||||||
>
|
|
||||||
<></>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
strong: <strong />,
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
|
|
||||||
const currencyInputProps = {
|
|
||||||
prefix: currencyConfig.symbol,
|
|
||||||
className: 'half-width-field components-currency-control',
|
|
||||||
sanitize: ( value: Product[ keyof Product ] ) => {
|
|
||||||
return sanitizePrice( String( value ) );
|
|
||||||
},
|
|
||||||
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
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) {
|
|
||||||
const name = event.currentTarget.name as keyof Pick<
|
|
||||||
Product,
|
|
||||||
'regular_price' | 'sale_price'
|
|
||||||
>;
|
|
||||||
const amount = Number.parseFloat(
|
|
||||||
sanitizePrice( values[ name ] || '0' )
|
|
||||||
);
|
|
||||||
const step = Number( event.currentTarget.step || '1' );
|
|
||||||
if ( event.code === 'ArrowUp' ) {
|
|
||||||
setValues( {
|
|
||||||
[ name ]: String( amount + step ),
|
|
||||||
} as unknown as Product );
|
|
||||||
}
|
|
||||||
if ( event.code === 'ArrowDown' ) {
|
|
||||||
setValues( {
|
|
||||||
[ name ]: String( amount - step ),
|
|
||||||
} as unknown as Product );
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const regularPriceProps = getInputProps(
|
|
||||||
'regular_price',
|
|
||||||
currencyInputProps
|
|
||||||
);
|
|
||||||
const salePriceProps = getInputProps( 'sale_price', currencyInputProps );
|
|
||||||
|
|
||||||
const dateTimePickerProps = {
|
|
||||||
className: 'woocommerce-product__date-time-picker',
|
|
||||||
isDateOnlyPicker: true,
|
|
||||||
dateTimeFormat: dateFormat,
|
|
||||||
};
|
|
||||||
|
|
||||||
const taxStatusProps = getInputProps( 'tax_status' );
|
|
||||||
// These properties cause issues with the RadioControl component.
|
|
||||||
// A fix to form upstream would help if we can identify what type of input is used.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
delete taxStatusProps.checked;
|
|
||||||
delete taxStatusProps.value;
|
|
||||||
|
|
||||||
const taxClassProps = getInputProps( 'tax_class' );
|
|
||||||
// These properties cause issues with the RadioControl component.
|
|
||||||
// A fix to form upstream would help if we can identify what type of input is used.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
delete taxClassProps.checked;
|
|
||||||
delete taxClassProps.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProductSectionLayout
|
|
||||||
title={ __( 'Pricing', 'woocommerce' ) }
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
{ __(
|
|
||||||
'Set a competitive price, put the product on sale, and manage tax calculations.',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</span>
|
|
||||||
<Link
|
|
||||||
className="woocommerce-form-section__header-link"
|
|
||||||
href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/"
|
|
||||||
target="_blank"
|
|
||||||
type="external"
|
|
||||||
onClick={ () => {
|
|
||||||
recordEvent( 'add_product_pricing_help' );
|
|
||||||
} }
|
|
||||||
>
|
|
||||||
{ __(
|
|
||||||
'How to price your product: expert tips',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Card>
|
|
||||||
<CardBody>
|
|
||||||
<BaseControl
|
|
||||||
id="product_pricing_regular_price"
|
|
||||||
help={ regularPriceProps?.help ?? '' }
|
|
||||||
>
|
|
||||||
<InputControl
|
|
||||||
{ ...regularPriceProps }
|
|
||||||
name="regular_price"
|
|
||||||
label={ __( 'List price', 'woocommerce' ) }
|
|
||||||
value={ formatCurrencyDisplayValue(
|
|
||||||
String( regularPriceProps?.value ),
|
|
||||||
currencyConfig,
|
|
||||||
formatAmount
|
|
||||||
) }
|
|
||||||
/>
|
|
||||||
</BaseControl>
|
|
||||||
{ ! isTaxSettingsResolving && (
|
|
||||||
<span className="woocommerce-product-form__secondary-text">
|
|
||||||
{ taxSettingsElement }
|
|
||||||
</span>
|
|
||||||
) }
|
|
||||||
|
|
||||||
<BaseControl
|
|
||||||
id="product_pricing_sale_price"
|
|
||||||
help={ salePriceProps?.help ?? '' }
|
|
||||||
>
|
|
||||||
<InputControl
|
|
||||||
{ ...salePriceProps }
|
|
||||||
name="sale_price"
|
|
||||||
label={ __( 'Sale price', 'woocommerce' ) }
|
|
||||||
value={ formatCurrencyDisplayValue(
|
|
||||||
String( salePriceProps?.value ),
|
|
||||||
currencyConfig,
|
|
||||||
formatAmount
|
|
||||||
) }
|
|
||||||
/>
|
|
||||||
</BaseControl>
|
|
||||||
|
|
||||||
<ToggleControl
|
|
||||||
label={
|
|
||||||
<>
|
|
||||||
{ __( 'Schedule sale', 'woocommerce' ) }
|
|
||||||
<Tooltip
|
|
||||||
text={ interpolateComponents( {
|
|
||||||
mixedString: __(
|
|
||||||
'The sale will start at the beginning of the "From" date ({{startTime/}}) and expire at the end of the "To" date ({{endTime/}}). {{moreLink/}}',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
components: {
|
|
||||||
startTime: (
|
|
||||||
<span>
|
|
||||||
{ formatDate(
|
|
||||||
timeFormat,
|
|
||||||
moment().startOf(
|
|
||||||
'day'
|
|
||||||
)
|
|
||||||
) }
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
endTime: (
|
|
||||||
<span>
|
|
||||||
{ formatDate(
|
|
||||||
timeFormat,
|
|
||||||
moment().endOf( 'day' )
|
|
||||||
) }
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
moreLink: (
|
|
||||||
<Link
|
|
||||||
href="https://woocommerce.com/document/managing-products/#product-data"
|
|
||||||
target="_blank"
|
|
||||||
type="external"
|
|
||||||
onClick={ () =>
|
|
||||||
recordEvent(
|
|
||||||
'add_product_learn_more',
|
|
||||||
{
|
|
||||||
category:
|
|
||||||
PRODUCT_SCHEDULED_SALE_SLUG,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ __(
|
|
||||||
'Learn more',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
} ) }
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
checked={ showSaleSchedule }
|
|
||||||
onChange={ onSaleScheduleToggleChange }
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore disabled prop exists
|
|
||||||
disabled={ ! ( values.sale_price?.length > 0 ) }
|
|
||||||
/>
|
|
||||||
|
|
||||||
{ showSaleSchedule && (
|
|
||||||
<>
|
|
||||||
<DateTimePickerControl
|
|
||||||
label={ __( 'From', 'woocommerce' ) }
|
|
||||||
placeholder={ __( 'Now', 'woocommerce' ) }
|
|
||||||
timeForDateOnly={ 'start-of-day' }
|
|
||||||
currentDate={ values.date_on_sale_from_gmt }
|
|
||||||
{ ...getInputProps( 'date_on_sale_from_gmt', {
|
|
||||||
...dateTimePickerProps,
|
|
||||||
} ) }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DateTimePickerControl
|
|
||||||
label={ __( 'To', 'woocommerce' ) }
|
|
||||||
placeholder={ __(
|
|
||||||
'No end date',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
timeForDateOnly={ 'end-of-day' }
|
|
||||||
currentDate={ values.date_on_sale_to_gmt }
|
|
||||||
{ ...getInputProps( 'date_on_sale_to_gmt', {
|
|
||||||
...dateTimePickerProps,
|
|
||||||
} ) }
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) }
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{ taxesEnabled && (
|
|
||||||
<Card>
|
|
||||||
<CardBody>
|
|
||||||
<RadioControl
|
|
||||||
{ ...taxStatusProps }
|
|
||||||
label={ __( 'Charge sales tax on', 'woocommerce' ) }
|
|
||||||
options={ [
|
|
||||||
{
|
|
||||||
label: __(
|
|
||||||
'Product and shipping',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
value: 'taxable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: __( 'Only shipping', 'woocommerce' ),
|
|
||||||
value: 'shipping',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: __(
|
|
||||||
'Don’t charge tax',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
value: 'none',
|
|
||||||
},
|
|
||||||
] }
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CollapsibleContent
|
|
||||||
toggleText={ __( 'Advanced', 'woocommerce' ) }
|
|
||||||
>
|
|
||||||
{ ! isTaxClassesResolving &&
|
|
||||||
taxClasses.length > 0 && (
|
|
||||||
<RadioControl
|
|
||||||
{ ...taxClassProps }
|
|
||||||
label={
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
{ __(
|
|
||||||
'Tax class',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</span>
|
|
||||||
<span className="woocommerce-product-form__secondary-text">
|
|
||||||
{ interpolateComponents( {
|
|
||||||
mixedString: __(
|
|
||||||
'Apply a tax rate if this product qualifies for tax reduction or exemption. {{link}}Learn more{{/link}}',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
components: {
|
|
||||||
link: (
|
|
||||||
<Link
|
|
||||||
href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class"
|
|
||||||
target="_blank"
|
|
||||||
type="external"
|
|
||||||
>
|
|
||||||
<></>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
} ) }
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
options={ taxClasses.map(
|
|
||||||
( taxClass ) => ( {
|
|
||||||
label: taxClass.name,
|
|
||||||
value:
|
|
||||||
taxClass.slug ===
|
|
||||||
STANDARD_RATE_TAX_CLASS_SLUG
|
|
||||||
? ''
|
|
||||||
: taxClass.slug,
|
|
||||||
} )
|
|
||||||
) }
|
|
||||||
/>
|
|
||||||
) }
|
|
||||||
</CollapsibleContent>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
) }
|
|
||||||
</ProductSectionLayout>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: update
|
||||||
|
|
||||||
|
Migrating product editor pricing section to slot fills.
|
Loading…
Reference in New Issue