[Product Block Editor]: re implement product description field (#41862)
This commit is contained in:
commit
cf33d1a31c
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
[Product Block Editor]: replace description button by editable block
|
|
@ -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",
|
||||
|
|
|
@ -20,6 +20,6 @@
|
|||
"reusable": false,
|
||||
"inserter": false,
|
||||
"lock": false,
|
||||
"__experimentalToolbar": false
|
||||
"__experimentalToolbar": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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 } />
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 >;
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 );
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 > {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
[Product Block Editor]: replace description button by editable block
|
|
@ -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(
|
||||
|
|
|
@ -88,14 +88,14 @@ test.describe( 'General tab', () => {
|
|||
}
|
||||
} );
|
||||
|
||||
test( 'can create a simple product', async ( { page } ) => {
|
||||
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' )
|
||||
.locator( '[data-template-block-id="basic-details"] .components-summary-control' )
|
||||
.fill( productData.summary );
|
||||
await page
|
||||
.locator(
|
||||
|
@ -136,7 +136,7 @@ test.describe( 'General tab', () => {
|
|||
|
||||
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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue