[Product Block Editor]: re implement product description field (#41862)

This commit is contained in:
Damián Suárez 2023-12-07 20:51:39 -03:00 committed by GitHub
commit cf33d1a31c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 304 additions and 82 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
[Product Block Editor]: replace description button by editable block

View File

@ -51,9 +51,10 @@
"@wordpress/data": "wp-6.0",
"@wordpress/date": "wp-6.0",
"@wordpress/deprecated": "wp-6.0",
"@wordpress/editor": "wp-6.0",
"@wordpress/edit-post": "wp-6.0",
"@wordpress/editor": "wp-6.0",
"@wordpress/element": "wp-6.0",
"@wordpress/hooks": "wp-6.0",
"@wordpress/html-entities": "wp-6.0",
"@wordpress/i18n": "wp-6.0",
"@wordpress/icons": "wp-6.0",
@ -88,8 +89,8 @@
"@types/wordpress__core-data": "2.4.5",
"@types/wordpress__data": "6.0.2",
"@types/wordpress__date": "3.3.2",
"@types/wordpress__editor": "13.0.0",
"@types/wordpress__edit-post": "7.5.4",
"@types/wordpress__editor": "13.0.0",
"@types/wordpress__keycodes": "2.3.1",
"@types/wordpress__media-utils": "3.0.0",
"@types/wordpress__plugins": "3.0.0",

View File

@ -20,6 +20,6 @@
"reusable": false,
"inserter": false,
"lock": false,
"__experimentalToolbar": false
"__experimentalToolbar": true
}
}

View File

@ -0,0 +1,40 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { ToolbarButton } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { recordEvent } from '@woocommerce/tracks';
import { dispatch } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { parse } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { store } from '../../../../store/product-editor-ui';
export default function FullEditorToolbarButton( {
label = __( 'Edit Product description', 'woocommerce' ),
text = __( 'Full editor', 'woocommerce' ),
} ) {
const { openModalEditor, setModalEditorBlocks } = dispatch( store );
const [ description ] = useEntityProp< string >(
'postType',
'product',
'description'
);
return (
<ToolbarButton
label={ label }
onClick={ () => {
setModalEditorBlocks( parse( description ) );
recordEvent( 'product_add_description_click' );
openModalEditor();
} }
>
{ text }
</ToolbarButton>
);
}

View File

@ -0,0 +1,57 @@
/**
* External dependencies
*/
import { createElement, Fragment } from '@wordpress/element';
import { createHigherOrderComponent } from '@wordpress/compose';
import { BlockControls } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import type {
DescriptionBlockEditComponent,
DescriptionBlockEditProps,
} from '../types';
import FullEditorToolbarButton from './full-editor-toolbar-button';
const wooBlockwithFullEditorToolbarButton =
createHigherOrderComponent< DescriptionBlockEditComponent >(
( BlockEdit: DescriptionBlockEditComponent ) => {
return ( props: DescriptionBlockEditProps ) => {
// Only extend summary field block instances
if ( props?.name !== 'woocommerce/product-summary-field' ) {
return <BlockEdit { ...props } />;
}
// Only add the `Full editor` button when the block is selected
if ( ! props?.isSelected ) {
return <BlockEdit { ...props } />;
}
/*
* Extend the toolbar only to the sumary field block instance
* that has the `woocommerce/product-description-field__content` template block ID.
*/
if (
props?.attributes?._templateBlockId !==
'product-description__content'
) {
return <BlockEdit { ...props } />;
}
const blockControlProps = { group: 'other' };
return (
<>
<BlockControls { ...blockControlProps }>
<FullEditorToolbarButton />
</BlockControls>
<BlockEdit { ...props } />
</>
);
};
},
'wooBlockwithFullEditorToolbarButton'
);
export default wooBlockwithFullEditorToolbarButton;

View File

@ -1,27 +1,27 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createElement, useEffect } from '@wordpress/element';
import {
BlockAttributes,
BlockInstance,
parse,
serialize,
} from '@wordpress/blocks';
import { useSelect, useDispatch } from '@wordpress/data';
import { Button } from '@wordpress/components';
import { BlockInstance, serialize } from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import classNames from 'classnames';
import { useWooBlockProps } from '@woocommerce/block-templates';
import { recordEvent } from '@woocommerce/tracks';
import { useEntityProp } from '@wordpress/core-data';
import { __ } from '@wordpress/i18n';
import {
// @ts-expect-error no exported member.
useInnerBlocksProps,
BlockControls,
} from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import { ContentPreview } from '../../../components/content-preview';
import { ProductEditorBlockEditProps } from '../../../types';
import ModalEditorWelcomeGuide from '../../../components/modal-editor-welcome-guide';
import { store } from '../../../store/product-editor-ui';
import type { DescriptionBlockEditComponent } from './types';
import FullEditorToolbarButton from './components/full-editor-toolbar-button';
/**
* Internal dependencies
@ -49,8 +49,7 @@ function clearDescriptionIfEmpty( blocks: BlockInstance[] ) {
export function DescriptionBlockEdit( {
attributes,
}: ProductEditorBlockEditProps< BlockAttributes > ) {
const blockProps = useWooBlockProps( attributes );
}: DescriptionBlockEditComponent ) {
const [ description, setDescription ] = useEntityProp< string >(
'postType',
'product',
@ -69,8 +68,6 @@ export function DescriptionBlockEdit( {
[]
);
const { openModalEditor, setModalEditorBlocks } = useDispatch( store );
// Update the description when the blocks change.
useEffect( () => {
if ( ! hasChanged ) {
@ -85,23 +82,34 @@ export function DescriptionBlockEdit( {
setDescription( html );
}, [ modalEditorBlocks, setDescription, hasChanged ] );
const blockProps = useWooBlockProps( attributes, {
className: classNames( { 'has-blocks': !! description.length } ),
tabIndex: 0,
} );
const innerBlockProps = useInnerBlocksProps(
{},
{
templateLock: 'contentOnly',
allowedBlocks: [ 'woocommerce/product-summary-field' ],
}
);
return (
<div { ...blockProps }>
<Button
variant="secondary"
onClick={ () => {
if ( description ) {
setModalEditorBlocks( parse( description ) );
}
{ !! description.length && (
<BlockControls>
<FullEditorToolbarButton
text={ __( 'Edit in full editor', 'woocommerce' ) }
/>
</BlockControls>
) }
openModalEditor();
recordEvent( 'product_add_description_click' );
} }
>
{ description.length
? __( 'Edit description', 'woocommerce' )
: __( 'Add description', 'woocommerce' ) }
</Button>
{ ! description.length && <div { ...innerBlockProps } /> }
{ !! description.length && (
<div className="wp-block-woocommerce-product-description-field__cover" />
) }
{ !! description.length && (
<ContentPreview content={ description } />

View File

@ -0,0 +1,38 @@
.wp-block-woocommerce-product-description-field {
&:focus {
box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9));
outline: 3px solid transparent;
}
&.has-blocks {
min-height: 320px;
.wp-block-woocommerce-product-description-field__cover,
.woocommerce-content-preview {
min-height: 320px;
&:after {
display: none;
}
.woocommerce-content-preview__iframe {
min-height: 320px;
}
}
}
.woocommerce-content-preview {
position: absolute;
top: 0;
left: 0;
margin: 0;
z-index: 100;
}
}
.wp-block-woocommerce-product-description-field__cover {
position: absolute;
width: 100%;
top: 0;
left: 0;
z-index: 200;
}

View File

@ -1,9 +1,16 @@
/**
* External dependencies
*/
import { postContent } from '@wordpress/icons';
import { addFilter } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import blockConfiguration from './block.json';
import { DescriptionBlockEdit } from './edit';
import { registerProductEditorBlockType } from '../../../utils';
import wooDescriptionBlockWithFullEditorButton from './components/with-full-editor-toolbar-button';
const { name, ...metadata } = blockConfiguration;
@ -12,6 +19,7 @@ export { metadata, name };
export const settings = {
example: {},
edit: DescriptionBlockEdit,
icon: postContent,
};
export const init = () =>
@ -20,3 +28,9 @@ export const init = () =>
metadata: metadata as never,
settings: settings as never,
} );
addFilter(
'editor.BlockEdit',
'woocommerce/summary-block-with-full-editor-button',
wooDescriptionBlockWithFullEditorButton
);

View File

@ -0,0 +1,20 @@
/**
* External dependencies
*/
import {
// @ts-expect-error no exported member.
ComponentType,
} from '@wordpress/element';
/**
* Internal dependencies
*/
import {
ProductEditorBlockAttributes,
ProductEditorBlockEditProps,
} from '../../../types';
export type DescriptionBlockEditProps =
ProductEditorBlockEditProps< ProductEditorBlockAttributes >;
export type DescriptionBlockEditComponent =
ComponentType< DescriptionBlockEditProps >;

View File

@ -1,4 +1,5 @@
@import "product-fields/attributes/editor.scss";
@import "product-fields/description/editor.scss";
@import "product-fields/downloads/editor.scss";
@import "product-fields/images/editor.scss";
@import "product-fields/inventory-email/editor.scss";

View File

@ -67,7 +67,10 @@ export function ContentPreview( { content }: ContentPreviewProps ) {
return (
<div className="woocommerce-content-preview">
<Iframe className="woocommerce-content-preview__iframe">
<Iframe
className="woocommerce-content-preview__iframe"
tabIndex={ -1 }
>
<>
<EditorStyles styles={ parentEditorSettings?.styles } />
<style>

View File

@ -43,7 +43,6 @@ type IframeEditorProps = {
};
export function IframeEditor( {
initialBlocks = [],
onChange = () => {},
onClose,
onInput = () => {},
@ -51,14 +50,23 @@ export function IframeEditor( {
showBackButton = false,
}: IframeEditorProps ) {
const [ resizeObserver ] = useResizeObserver();
const [ temporalBlocks, setTemporalBlocks ] =
useState< BlockInstance[] >( initialBlocks );
const [ temporalBlocks, setTemporalBlocks ] = useState< BlockInstance[] >(
[]
);
// Pick the blocks from the store.
const blocks: BlockInstance[] = useSelect( ( select ) => {
return select( productEditorUiStore ).getModalEditorBlocks();
}, [] );
/*
* Set the initial blocks from the store.
* @todo: probably we can get rid of the initialBlocks prop.
*/
useEffect( () => {
setTemporalBlocks( blocks );
}, [] ); // eslint-disable-line
const { setModalEditorBlocks: setBlocks, setModalEditorContentHasChanged } =
useDispatch( productEditorUiStore );

View File

@ -54,6 +54,7 @@ function MockTabs( { onChange = jest.fn() } ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
context={ mockContext }
name="test1"
/>
<Tab
{ ...blockProps }
@ -61,6 +62,7 @@ function MockTabs( { onChange = jest.fn() } ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
context={ mockContext }
name="test2"
/>
<Tab
{ ...blockProps }
@ -68,6 +70,7 @@ function MockTabs( { onChange = jest.fn() } ) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
context={ mockContext }
name="test3"
/>
</SlotFillProvider>
);

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { BlockEditProps } from '@wordpress/blocks';
import { BlockAttributes, BlockEditProps } from '@wordpress/blocks';
export interface ProductEditorContext {
postId: number;
@ -13,6 +13,11 @@ export interface ProductEditorContext {
export interface ProductEditorBlockEditProps< T extends Record< string, any > >
extends BlockEditProps< T > {
readonly context: ProductEditorContext;
readonly name: string;
}
export interface ProductEditorBlockAttributes extends BlockAttributes {
_templateBlockId?: string;
}
export interface Metadata< T > {

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
[Product Block Editor]: replace description button by editable block

View File

@ -254,7 +254,8 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
),
)
);
$description_section->add_block(
$description_field_block = $description_section->add_block(
array(
'id' => 'product-description',
'blockName' => 'woocommerce/product-description-field',
@ -262,6 +263,18 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
)
);
$description_field_block->add_block(
array(
'id' => 'product-description__content',
'blockName' => 'woocommerce/product-summary-field',
'order' => 10,
'attributes' => array(
'helpText' => null,
'label' => null,
),
)
);
// External/Affiliate section.
if ( Features::is_enabled( 'product-external-affiliate' ) ) {
$buy_button_section = $general_group->add_section(

View File

@ -88,55 +88,55 @@ test.describe( 'General tab', () => {
}
} );
test( 'can create a simple product', async ( { page } ) => {
await page.goto( NEW_EDITOR_ADD_PRODUCT_URL );
await clickOnTab( 'General', page );
await page
.getByPlaceholder( 'e.g. 12 oz Coffee Mug' )
.fill( productData.name );
await page
.locator( '.components-summary-control' )
.fill( productData.summary );
await page
.locator(
'[id^="wp-block-woocommerce-product-regular-price-field"]'
)
.first()
.fill( productData.productPrice );
await page
.locator(
'[id^="wp-block-woocommerce-product-sale-price-field"]'
)
.first()
.fill( productData.salePrice );
test( 'can create a simple product', async ( { page } ) => {
await page.goto( NEW_EDITOR_ADD_PRODUCT_URL );
await clickOnTab( 'General', page );
await page
.getByPlaceholder( 'e.g. 12 oz Coffee Mug' )
.fill( productData.name );
await page
.locator( '[data-template-block-id="basic-details"] .components-summary-control' )
.fill( productData.summary );
await page
.locator(
'[id^="wp-block-woocommerce-product-regular-price-field"]'
)
.first()
.fill( productData.productPrice );
await page
.locator(
'[id^="wp-block-woocommerce-product-sale-price-field"]'
)
.first()
.fill( productData.salePrice );
await page
.locator( '.woocommerce-product-header__actions' )
.getByRole( 'button', {
name: 'Add',
} )
.click();
await page
.locator( '.woocommerce-product-header__actions' )
.getByRole( 'button', {
name: 'Add',
} )
.click();
const element = await page.locator(
'div.components-snackbar__content'
);
const textContent = await element.innerText();
const element = await page.locator(
'div.components-snackbar__content'
);
const textContent = await element.innerText();
await expect( textContent ).toMatch( /Product added/ );
await expect( textContent ).toMatch( /Product added/ );
const title = await page.locator(
'.woocommerce-product-header__title'
);
const title = await page.locator(
'.woocommerce-product-header__title'
);
// Save product ID
const productIdRegex = /product%2F(\d+)/;
const url = await page.url();
const productIdMatch = productIdRegex.exec( url );
productId = productIdMatch ? productIdMatch[ 1 ] : null;
// Save product ID
const productIdRegex = /product%2F(\d+)/;
const url = await page.url();
const productIdMatch = productIdRegex.exec( url );
productId = productIdMatch ? productIdMatch[ 1 ] : null;
await expect( productId ).toBeDefined();
await expect( title ).toHaveText( productData.name );
} );
await expect( productId ).toBeDefined();
await expect( title ).toHaveText( productData.name );
} );
test( 'can not create a product with duplicated SKU', async ( {
page,
} ) => {
@ -146,7 +146,7 @@ test.describe( 'General tab', () => {
.locator( '//input[@placeholder="e.g. 12 oz Coffee Mug"]' )
.fill( productData.name );
await page
.locator( '.components-summary-control' )
.locator( '[data-template-block-id="basic-details"] .components-summary-control' )
.fill( productData.summary );
await page
.locator(

View File

@ -2498,6 +2498,9 @@ importers:
'@wordpress/element':
specifier: wp-6.0
version: 4.4.1
'@wordpress/hooks':
specifier: wp-6.0
version: 3.6.1
'@wordpress/html-entities':
specifier: wp-6.0
version: 3.6.1