Create shipping fee field block and initial shipping section (#37642)

* Setting up the Fees & dimensions section

* Create product shipping fee block

* Register product shipping fee block

* Add changelog files

* Fix php linter errors

* Add reusable radio field and move the radio block to the blocks folder

* Remove manually set block className because is autogenerated base on the block name
This commit is contained in:
Maikel David Pérez Gómez 2023-04-14 22:44:28 -04:00 committed by GitHub
parent a3cb1735b3
commit 42cc482ebc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 437 additions and 96 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add product shipping fee block

View File

@ -34,6 +34,5 @@
"inserter": false,
"lock": false,
"__experimentalToolbar": false
},
"editorStyle": "file:./editor.css"
}
}

View File

@ -0,0 +1,35 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { BlockEditProps } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import { useEntityProp } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { RadioField } from '../../components/radio-field';
import { RadioBlockAttributes } from './types';
export function Edit( { attributes }: BlockEditProps< RadioBlockAttributes > ) {
const blockProps = useBlockProps();
const { description, options, property, title } = attributes;
const [ value, setValue ] = useEntityProp< string >(
'postType',
'product',
property
);
return (
<div { ...blockProps }>
<RadioField
title={ title }
description={ description }
selected={ value }
options={ options }
onChange={ ( selected ) => setValue( selected || '' ) }
/>
</div>
);
}

View File

@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { initBlock } from '../../utils/init-blocks';
import blockConfiguration from './block.json';
import { Edit } from './edit';
import { RadioBlockAttributes } from './types';
const { name, ...metadata } =
blockConfiguration as BlockConfiguration< RadioBlockAttributes >;
export { metadata, name };
export const settings: Partial< BlockConfiguration< RadioBlockAttributes > > = {
example: {},
edit: Edit,
};
export function init() {
return initBlock( { name, metadata, settings } );
}

View File

@ -0,0 +1,11 @@
/**
* External dependencies
*/
import { BlockAttributes } from '@wordpress/blocks';
export interface RadioBlockAttributes extends BlockAttributes {
title: string;
description: string;
property: string;
options: [];
}

View File

@ -0,0 +1,25 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "woocommerce/product-shipping-fee-fields",
"title": "Product shipping fee fields",
"category": "woocommerce",
"description": "The product shipping fee fields.",
"keywords": [ "products", "shipping", "fee" ],
"textdomain": "default",
"attributes": {
"title": {
"type": "string",
"__experimentalRole": "content"
}
},
"supports": {
"align": false,
"html": false,
"multiple": true,
"reusable": false,
"inserter": false,
"lock": false
},
"editorStyle": "file:./editor.css"
}

View File

@ -0,0 +1,184 @@
/**
* External dependencies
*/
import { BlockEditProps } from '@wordpress/blocks';
import { Link } from '@woocommerce/components';
import {
EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME,
ProductShippingClass,
} from '@woocommerce/data';
import { getNewPath } from '@woocommerce/navigation';
import { recordEvent } from '@woocommerce/tracks';
import { useBlockProps } from '@wordpress/block-editor';
import { BaseControl, SelectControl } from '@wordpress/components';
import { useInstanceId } from '@wordpress/compose';
import { useEntityProp } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import {
Fragment,
createElement,
createInterpolateElement,
useEffect,
useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { ShippingFeeBlockAttributes } from './types';
import { useValidation } from '../../hooks/use-validation';
import { RadioField } from '../../components/radio-field';
const FOLLOW_CLASS_OPTION_VALUE = 'follow_class';
const FREE_SHIPPING_OPTION_VALUE = 'free_shipping';
const options = [
{
label: __( 'Follow class', 'woocommerce' ),
value: FOLLOW_CLASS_OPTION_VALUE,
},
{
label: __( 'Free shipping', 'woocommerce' ),
value: FREE_SHIPPING_OPTION_VALUE,
},
];
export function Edit( {
attributes,
}: BlockEditProps< ShippingFeeBlockAttributes > ) {
const { title } = attributes;
const blockProps = useBlockProps();
const [ option, setOption ] = useState< string >(
FREE_SHIPPING_OPTION_VALUE
);
const [ shippingClass, setShippingClass ] = useEntityProp< string >(
'postType',
'product',
'shipping_class'
);
const { shippingClasses } = useSelect( ( select ) => {
const { getProductShippingClasses } = select(
EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME
);
return {
shippingClasses:
getProductShippingClasses< ProductShippingClass[] >() ?? [],
};
}, [] );
const shippingClassControlId = useInstanceId( BaseControl ) as string;
const isShippingClassValid = useValidation(
'product/shipping_class',
function shippingClassValidator() {
if ( option === FOLLOW_CLASS_OPTION_VALUE && ! shippingClass ) {
return false;
}
return true;
}
);
function handleOptionChange( value: string ) {
setOption( value );
if ( value === FOLLOW_CLASS_OPTION_VALUE ) {
const [ firstShippingClass ] = shippingClasses;
setShippingClass( firstShippingClass?.slug ?? '' );
} else {
setShippingClass( '' );
}
}
useEffect( () => {
if ( shippingClass === '' ) {
setOption( FREE_SHIPPING_OPTION_VALUE );
} else if ( shippingClass ) {
setOption( FOLLOW_CLASS_OPTION_VALUE );
}
}, [ shippingClass ] );
return (
<div { ...blockProps }>
<div className="wp-block-columns">
<div className="wp-block-column">
<RadioField
title={ title }
selected={ option }
options={ options }
onChange={ handleOptionChange }
/>
</div>
</div>
{ option === FOLLOW_CLASS_OPTION_VALUE && (
<div className="wp-block-columns">
<div
className={ classNames( 'wp-block-column', {
'has-error': ! isShippingClassValid,
} ) }
>
<SelectControl
id={ shippingClassControlId }
name="shipping_class"
value={ shippingClass }
onChange={ setShippingClass }
label={ __( 'Shipping class', 'woocommerce' ) }
help={
isShippingClassValid
? createInterpolateElement(
__(
'Manage shipping classes and rates in <Link>global settings</Link>.',
'woocommerce'
),
{
Link: (
<Link
href={ getNewPath(
{
tab: 'shipping',
section:
'classes',
},
'',
{},
'wc-settings'
) }
target="_blank"
type="external"
onClick={ () => {
recordEvent(
'product_shipping_global_settings_link_click'
);
} }
>
<Fragment />
</Link>
),
}
)
: __(
'The shipping class is required.',
'woocommerce'
)
}
>
{ shippingClasses.map( ( { slug, name } ) => (
<option key={ slug } value={ slug }>
{ name }
</option>
) ) }
</SelectControl>
</div>
<div className="wp-block-column"></div>
</div>
) }
</div>
);
}

View File

@ -0,0 +1,11 @@
.wp-block-woocommerce-product-shipping-fee-fields {
.has-error {
.components-base-control .components-input-control__backdrop {
border-color: $studio-red-50;
}
.components-base-control__help {
color: $studio-red-50;
}
}
}

View File

@ -0,0 +1,29 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { BlockConfiguration } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { initBlock } from '../../utils/init-blocks';
import blockConfiguration from './block.json';
import { Edit } from './edit';
import { ShippingFeeBlockAttributes } from './types';
const { name, ...metadata } =
blockConfiguration as BlockConfiguration< ShippingFeeBlockAttributes >;
export { metadata, name };
export const settings: Partial<
BlockConfiguration< ShippingFeeBlockAttributes >
> = {
example: {},
edit: Edit,
};
export function init() {
return initBlock( { name, metadata, settings } );
}

View File

@ -0,0 +1,8 @@
/**
* External dependencies
*/
import { BlockAttributes } from '@wordpress/blocks';
export interface ShippingFeeBlockAttributes extends BlockAttributes {
title: string;
}

View File

@ -14,7 +14,7 @@ import {
*/
import { init as initImages } from '../images';
import { init as initName } from '../details-name-block';
import { init as initRadio } from '../radio';
import { init as initRadio } from '../../blocks/radio';
import { init as initSummary } from '../details-summary-block';
import { init as initSection } from '../section';
import { init as initTab } from '../tab';
@ -27,6 +27,7 @@ import { init as initConditional } from '../../blocks/conditional';
import { init as initLowStockQty } from '../../blocks/inventory-email';
import { init as initCheckbox } from '../../blocks/checkbox';
import { init as initShippingDimensions } from '../../blocks/shipping-dimensions';
import { init as initShippingFee } from '../../blocks/shipping-fee';
export const initBlocks = () => {
const coreBlocks = __experimentalGetCoreBlocks();
@ -52,4 +53,5 @@ export const initBlocks = () => {
initLowStockQty();
initCheckbox();
initShippingDimensions();
initShippingFee();
};

View File

@ -0,0 +1,2 @@
export * from './radio-field';
export * from './types';

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { createElement, Fragment } from '@wordpress/element';
import { RadioControl } from '@wordpress/components';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import { sanitizeHTML } from '../../utils/sanitize-html';
import { RadioFieldProps } from './types';
export function RadioField< T = string >( {
title,
description,
className,
...props
}: RadioFieldProps< T > ) {
return (
<RadioControl
{ ...props }
className={ classNames( className, 'woocommerce-radio-field' ) }
label={
<>
<span className="woocommerce-radio-field__title">
{ title }
</span>
{ description && (
<span
className="woocommerce-radio-field__description"
dangerouslySetInnerHTML={ sanitizeHTML(
description
) }
/>
) }
</>
}
/>
);
}

View File

@ -0,0 +1,26 @@
.woocommerce-radio-field {
.components-base-control__label {
text-transform: none;
font-weight: 400;
> span {
display: block;
line-height: 1.5;
}
margin-bottom: $gap-large;
}
&__title {
font-size: 16px;
font-weight: 600;
color: #1e1e1e;
}
&__description {
font-size: 13px;
color: #1e1e1e;
}
.components-base-control__field > .components-v-stack {
gap: $gap;
}
}

View File

@ -0,0 +1,9 @@
/**
* External dependencies
*/
import { RadioControl } from '@wordpress/components';
export type RadioFieldProps< T > = Omit< RadioControl.Props< T >, 'label' > & {
title: string;
description?: string;
};

View File

@ -1,46 +0,0 @@
/**
* External dependencies
*/
import { createElement, Fragment } from '@wordpress/element';
import type { BlockAttributes } from '@wordpress/blocks';
import { RadioControl } from '@wordpress/components';
import { useBlockProps } from '@wordpress/block-editor';
import { useEntityProp } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { sanitizeHTML } from '../../utils/sanitize-html';
export function Edit( { attributes }: { attributes: BlockAttributes } ) {
const blockProps = useBlockProps();
const { description, options, property, title } = attributes;
const [ value, setValue ] = useEntityProp< string >(
'postType',
'product',
property
);
return (
<div { ...blockProps }>
<RadioControl
label={
<>
<span className="wp-block-woocommerce-product-radio__title">
{ title }
</span>
<span
className="wp-block-woocommerce-product-radio__description"
dangerouslySetInnerHTML={ sanitizeHTML(
description
) }
/>
</>
}
selected={ value }
options={ options }
onChange={ ( selected ) => setValue( selected || '' ) }
/>
</div>
);
}

View File

@ -1,26 +0,0 @@
.wp-block-woocommerce-product-radio {
.components-base-control__label {
text-transform: none;
font-weight: 400;
> span {
display: block;
line-height: 1.5;
}
margin-bottom: $gap-large;
}
&__title {
font-size: 16px;
font-weight: 600;
color: #1e1e1e;
}
&__description {
font-size: 13px;
color: #1e1e1e;
}
.components-base-control__field > .components-v-stack {
gap: $gap;
}
}

View File

@ -1,17 +0,0 @@
/**
* Internal dependencies
*/
import initBlock from '../../utils/init-block';
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

@ -12,7 +12,8 @@
"type": "string"
},
"description": {
"type": "string"
"type": "string",
"__experimentalRole": "content"
},
"icon": {
"type": "object"

View File

@ -1,6 +1,7 @@
@import 'blocks/schedule-sale/editor.scss';
@import 'blocks/track-inventory/editor.scss';
@import 'blocks/shipping-dimensions/editor.scss';
@import 'blocks/shipping-fee/editor.scss';
@import 'components/editor/style.scss';
@import 'components/product-section-layout/style.scss';
@import 'components/edit-product-link-modal/style.scss';
@ -9,8 +10,8 @@
@import 'components/header/style.scss';
@import 'components/images/editor.scss';
@import 'components/block-editor/style.scss';
@import 'components/radio/editor.scss';
@import 'blocks/checkbox/editor.scss';
@import 'components/radio-field/style.scss';
@import 'components/section/editor.scss';
@import 'components/tab/editor.scss';
@import 'components/tabs/style.scss';

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add product shipping fee block to blocks template definition

View File

@ -687,12 +687,24 @@ class WC_Post_Types {
array(
'woocommerce/product-section',
array(
'title' => __( 'Shipping', 'woocommerce' ),
'icon' => array(
'title' => __( 'Fees & dimensions', 'woocommerce' ),
'description' => sprintf(
/* translators: %1$s: How to get started? link opening tag. %2$s: How to get started? link closing tag.*/
__( 'Set up shipping costs and enter dimensions used for accurate rate calculations. %1$sHow to get started?%2$s.', 'woocommerce' ),
'<a href="https://woocommerce.com/posts/how-to-calculate-shipping-costs-for-your-woocommerce-store/" target="_blank" rel="noreferrer">',
'</a>'
),
'icon' => array(
'src' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 7.75C3.5 6.7835 4.2835 6 5.25 6H14.75H15.5V6.75V9H17.25H17.5607L17.7803 9.21967L20.7803 12.2197L21 12.4393V12.75V14.75C21 15.7165 20.2165 16.5 19.25 16.5H19.2377C19.2458 16.5822 19.25 16.6656 19.25 16.75C19.25 18.1307 18.1307 19.25 16.75 19.25C15.3693 19.25 14.25 18.1307 14.25 16.75C14.25 16.6656 14.2542 16.5822 14.2623 16.5H14H10.2377C10.2458 16.5822 10.25 16.6656 10.25 16.75C10.25 18.1307 9.13071 19.25 7.75 19.25C6.36929 19.25 5.25 18.1307 5.25 16.75C5.25 16.6656 5.25418 16.5822 5.26234 16.5H4.25H3.5V15.75V7.75ZM14 15V9.75V9V7.5H5.25C5.11193 7.5 5 7.61193 5 7.75V15H5.96464C6.41837 14.5372 7.05065 14.25 7.75 14.25C8.44935 14.25 9.08163 14.5372 9.53536 15H14ZM18.5354 15H19.25C19.3881 15 19.5 14.8881 19.5 14.75V13.0607L16.9393 10.5H15.5V14.5845C15.8677 14.3717 16.2946 14.25 16.75 14.25C17.4493 14.25 18.0816 14.5372 18.5354 15ZM6.7815 16.5C6.76094 16.5799 6.75 16.6637 6.75 16.75C6.75 17.3023 7.19772 17.75 7.75 17.75C8.30228 17.75 8.75 17.3023 8.75 16.75C8.75 16.6637 8.73906 16.5799 8.7185 16.5C8.60749 16.0687 8.21596 15.75 7.75 15.75C7.28404 15.75 6.89251 16.0687 6.7815 16.5ZM15.7815 16.5C15.7609 16.5799 15.75 16.6637 15.75 16.75C15.75 17.3023 16.1977 17.75 16.75 17.75C17.3023 17.75 17.75 17.3023 17.75 16.75C17.75 16.6637 17.7391 16.5799 17.7185 16.5C17.7144 16.4841 17.7099 16.4683 17.705 16.4526C17.5784 16.0456 17.1987 15.75 16.75 15.75C16.284 15.75 15.8925 16.0687 15.7815 16.5Z" fill="#1E1E1E"/></svg>',
),
),
array(
array(
'woocommerce/product-shipping-fee-fields',
array(
'title' => __( 'Shipping fee', 'woocommerce' ),
),
),
array(
'woocommerce/product-shipping-dimensions-fields',
),