Introduce a product type selection within the new experience (#41823)

* Create a relation between the product type and the product block template

* Add 'patterns' to name the kind of products that can be created for a specific template

* Resolve template using its id as a template query param

* Rename ProductEditPattern to ProductTemplate

* Rename get_patterns hook to woocommerce_product_editor_get_product_templates

* Return the list of templates to the client

* Set layout template events as array

* Register the layout template based on the product template or the post type in case of product variations

* Registering non supported product types

* Create and register the woocommerce/product-details-section-description block

* Add the product type to the section description

* Create product type selector

* Fix menu item style

* Highlight selected menu item

* Set the selected product template

* Set product template title to lowercase in the content description

* Rename blocks by blockTemplates under the AbstractBlockTemplate class

* Rename to woocommerce_product_editor_product_templates filter

* Remove product_template_ prefix from the supported_product_types map

* Rename get_formatted to to_JSON and convert the props to client side like

* Refactor get_product_templates

* Fix icon resolution

* Add a confirmation modal for unsupported product templates

* Add changelog files

* Remove product types using for testing

* Fix redirection when changing to a non supported product template

* Set the change button state to busy when it is saving the product

* Fix php linter errors

* Fix rebase conflict

* Move ProductTemplate to Automattic\WooCommerce\Admin\Features\ProductBlockEditor namespace

* Add the to_json definition to the BlockTemplateInterface

* Create default product template by custom product type if it does not have a template associated yet

* Fix some comments and product template creation validation

* Add support to load the product template icon from an external resource

* Fix php linter

* Fix the changelog description
This commit is contained in:
Maikel David Pérez Gómez 2023-12-21 20:45:31 -03:00 committed by GitHub
parent eac1a460f0
commit a592a473d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 996 additions and 72 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Introduce a product type selection within the new experience

View File

@ -25,6 +25,7 @@ export { init as initToggle } from './generic/toggle';
export { init as attributesInit } from './product-fields/attributes';
export { init as initVariations } from './product-fields/variations';
export { init as initRequirePassword } from './product-fields/password';
export { init as initProductDetailsSectionDescription } from './product-fields/product-details-section-description';
export { init as initProductList } from './product-fields/product-list';
export { init as initVariationItems } from './product-fields/variation-items';
export { init as initVariationOptions } from './product-fields/variation-options';

View File

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

View File

@ -0,0 +1,376 @@
/**
* External dependencies
*/
import classNames from 'classnames';
import {
Button,
Dropdown,
MenuGroup,
MenuItem,
Modal,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import {
createElement,
createInterpolateElement,
useState,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import * as icons from '@wordpress/icons';
import { useWooBlockProps } from '@woocommerce/block-templates';
import { Product } from '@woocommerce/data';
import { getNewPath } from '@woocommerce/navigation';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useEntityId } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { ProductEditorSettings } from '../../../components';
import { ProductTemplate } from '../../../components/editor';
import { BlockFill } from '../../../components/block-slot-fill';
import { useValidations } from '../../../contexts/validation-context';
import {
WPError,
getProductErrorMessage,
} from '../../../utils/get-product-error-message';
import { ProductEditorBlockEditProps } from '../../../types';
import { ProductDetailsSectionDescriptionBlockAttributes } from './types';
export function ProductDetailsSectionDescriptionBlockEdit( {
attributes,
clientId,
}: ProductEditorBlockEditProps< ProductDetailsSectionDescriptionBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes );
const { productTemplates, productTemplate: selectedProductTemplate } =
useSelect( ( select ) => {
const { getEditorSettings } = select( 'core/editor' );
return getEditorSettings() as ProductEditorSettings;
} );
// eslint-disable-next-line @wordpress/no-unused-vars-before-return
const [ supportedProductTemplates, unsupportedProductTemplates ] =
productTemplates.reduce< [ ProductTemplate[], ProductTemplate[] ] >(
( [ supported, unsupported ], productTemplate ) => {
if ( productTemplate.layoutTemplateId ) {
supported.push( productTemplate );
} else {
unsupported.push( productTemplate );
}
return [ supported, unsupported ];
},
[ [], [] ]
);
const productId = useEntityId( 'postType', 'product' );
const { validate } = useValidations< Product >();
// @ts-expect-error There are no types for this.
const { editEntityRecord, saveEditedEntityRecord, saveEntityRecord } =
useDispatch( 'core' );
const { createSuccessNotice, createErrorNotice } =
useDispatch( 'core/notices' );
const rootClientId = useSelect(
( select ) => {
const { getBlockRootClientId } = select( 'core/block-editor' );
return getBlockRootClientId( clientId );
},
[ clientId ]
);
const [ unsupportedProductTemplate, setUnsupportedProductTemplate ] =
useState< ProductTemplate >();
const { isSaving } = useSelect(
( select ) => {
// @ts-expect-error There are no types for this.
const { isSavingEntityRecord } = select( 'core' );
return {
isSaving: isSavingEntityRecord< boolean >(
'postType',
'product',
productId
),
};
},
[ productId ]
);
if ( ! rootClientId ) return;
function menuItemClickHandler(
productTemplate: ProductTemplate,
onClose: () => void
) {
return async function handleMenuItemClick() {
try {
if ( ! productTemplate.layoutTemplateId ) {
setUnsupportedProductTemplate( productTemplate );
onClose();
return;
}
await validate( productTemplate.productData );
await editEntityRecord(
'postType',
'product',
productId,
productTemplate.productData
);
await saveEditedEntityRecord< Product >(
'postType',
'product',
productId,
{
throwOnError: true,
}
);
createSuccessNotice(
__( 'Product type changed.', 'woocommerce' )
);
} catch ( error ) {
const message = getProductErrorMessage( error as WPError );
createErrorNotice( message );
}
onClose();
};
}
function resolveIcon( iconId?: string | null, alt?: string ) {
if ( ! iconId ) return undefined;
const { Icon } = icons;
let icon: JSX.Element;
if ( /^https?:\/\//.test( iconId ) ) {
icon = <img src={ iconId } alt={ alt } />;
} else {
if ( ! ( iconId in icons ) ) return undefined;
icon = icons[ iconId as never ];
}
return <Icon icon={ icon } size={ 24 } />;
}
function getMenuItem( onClose: () => void ) {
return function renderMenuItem( productTemplate: ProductTemplate ) {
const isSelected =
selectedProductTemplate?.id === productTemplate.id;
return (
<MenuItem
key={ productTemplate.id }
info={ productTemplate.description ?? undefined }
isSelected={ isSelected }
icon={
isSelected
? resolveIcon( 'check' )
: resolveIcon(
productTemplate.icon,
productTemplate.title
)
}
iconPosition="left"
role="menuitemradio"
onClick={ menuItemClickHandler( productTemplate, onClose ) }
className={ classNames( {
'components-menu-item__button--selected': isSelected,
} ) }
>
{ productTemplate.title }
</MenuItem>
);
};
}
async function handleModelChangeClick() {
try {
if ( isSaving ) return;
await validate( unsupportedProductTemplate?.productData );
const product = ( await saveEditedEntityRecord< Product >(
'postType',
'product',
productId,
{
throwOnError: true,
}
) ) ?? { id: productId };
// Avoiding to save some changes that are not supported by the current product template.
// So in this case those changes are saved directly to the server.
await saveEntityRecord(
'postType',
'product',
{
...product,
...unsupportedProductTemplate?.productData,
},
// @ts-expect-error Expected 3 arguments, but got 4.
{
throwOnError: true,
}
);
createSuccessNotice( __( 'Product type changed.', 'woocommerce' ) );
// Let the server manage the redirection when the product is not supported
// by the product editor.
window.location.href = getNewPath( {}, `/product/${ productId }` );
} catch ( error ) {
const message = getProductErrorMessage( error as WPError );
createErrorNotice( message );
}
}
return (
<BlockFill
name="section-description"
clientId={ clientId }
slotContainerBlockName="woocommerce/product-section"
>
<div { ...blockProps }>
<p>
{ createInterpolateElement(
/* translators: <ProductTemplate />: the product template. */
__( 'This is a <ProductTemplate />.', 'woocommerce' ),
{
ProductTemplate: (
<span>
{ selectedProductTemplate?.title?.toLowerCase() }
</span>
),
}
) }
</p>
<Dropdown
focusOnMount={ false }
// @ts-expect-error Property does exists
popoverProps={ {
placement: 'bottom-start',
} }
renderToggle={ ( { isOpen, onToggle } ) => (
<Button
aria-expanded={ isOpen }
variant="link"
onClick={ onToggle }
>
<span>
{ __( 'Change product type', 'woocommerce' ) }
</span>
</Button>
) }
renderContent={ ( { onClose } ) => (
<div className="wp-block-woocommerce-product-details-section-description__dropdown components-dropdown-menu__menu">
<MenuGroup>
{ supportedProductTemplates.map(
getMenuItem( onClose )
) }
</MenuGroup>
{ unsupportedProductTemplates.length > 0 && (
<MenuGroup>
<Dropdown
focusOnMount={ false }
// @ts-expect-error Property does exists
popoverProps={ {
placement: 'right-start',
} }
renderToggle={ ( {
isOpen,
onToggle,
} ) => (
<MenuItem
aria-expanded={ isOpen }
icon={ resolveIcon(
'chevronRight'
) }
iconPosition="right"
onClick={ onToggle }
>
<span>
{ __(
'More',
'woocommerce'
) }
</span>
</MenuItem>
) }
renderContent={ () => (
<div className="wp-block-woocommerce-product-details-section-description__dropdown components-dropdown-menu__menu">
<MenuGroup>
{ unsupportedProductTemplates.map(
getMenuItem( onClose )
) }
</MenuGroup>
</div>
) }
/>
</MenuGroup>
) }
</div>
) }
/>
{ Boolean( unsupportedProductTemplate ) && (
<Modal
title={ __( 'Change product type?', 'woocommerce' ) }
className="wp-block-woocommerce-product-details-section-description__modal"
onRequestClose={ () => {
setUnsupportedProductTemplate( undefined );
} }
>
<p>
<b>
{ __(
'This product type isnt supported by the updated product editing experience yet.',
'woocommerce'
) }
</b>
</p>
<p>
{ __(
'Youll be taken to the classic editing screen that isnt optimized for commerce but offers advanced functionality and supports all extensions.',
'woocommerce'
) }
</p>
<div className="wp-block-woocommerce-product-details-section-description__modal-actions">
<Button
variant="secondary"
aria-disabled={ isSaving }
onClick={ () => {
if ( isSaving ) return;
setUnsupportedProductTemplate( undefined );
} }
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
variant="primary"
isBusy={ isSaving }
aria-disabled={ isSaving }
onClick={ handleModelChangeClick }
>
{ __( 'Change', 'woocommerce' ) }
</Button>
</div>
</Modal>
) }
</div>
</BlockFill>
);
}

View File

@ -0,0 +1,52 @@
.wp-block-woocommerce-product-details-section-description {
margin-top: $grid-unit;
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
gap: $grid-unit-05;
p {
margin: 0;
}
&__dropdown .components-button.components-menu-item__button {
border-radius: 2px;
padding: $grid-unit $grid-unit + 2;
padding-inline-start: $grid-unit-30 + $grid-unit-05;
&:hover {
background-color: $gray-100;
color: inherit;
}
&.has-icon {
gap: $grid-unit-05;
padding-inline-start: 0;
svg {
flex-shrink: 0;
align-self: flex-start;
}
}
&--selected {
svg,
.components-menu-item__item {
color: var(--wp-admin-theme-color);
}
}
}
&__modal {
max-width: 650px;
&-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: $grid-unit;
padding-top: $grid-unit-40;
}
}
}

View File

@ -0,0 +1,27 @@
/**
* Internal dependencies
*/
import { registerProductEditorBlockType } from '../../../utils';
/**
* Internal dependencies
*/
import blockConfiguration from './block.json';
import { ProductDetailsSectionDescriptionBlockEdit } from './edit';
const { name, ...metadata } = blockConfiguration;
export { metadata, name };
export const settings = {
example: {},
edit: ProductDetailsSectionDescriptionBlockEdit,
};
export function init() {
return registerProductEditorBlockType( {
name,
metadata: metadata as never,
settings: settings as never,
} );
}

View File

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

View File

@ -18,6 +18,7 @@
@import "product-fields/variations/editor.scss";
@import "product-fields/password/editor.scss";
@import "product-fields/product-list/editor.scss";
@import "product-fields/product-details-section-description/editor.scss";
@import "product-fields/variation-items/editor.scss";
@import "product-fields/variation-options/editor.scss";
@import "generic/taxonomy/editor.scss";

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { synchronizeBlocksWithTemplate, Template } from '@wordpress/blocks';
import { synchronizeBlocksWithTemplate } from '@wordpress/blocks';
import {
createElement,
useMemo,
@ -22,8 +22,6 @@ import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
BlockTools,
EditorSettings,
EditorBlockListSettings,
ObserveTyping,
} from '@wordpress/block-editor';
// It doesn't seem to notice the External dependency block whn @ts-ignore is added.
@ -32,37 +30,26 @@ import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore store should be included.
useEntityBlockEditor,
useEntityProp,
} from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { useConfirmUnsavedProductChanges } from '../../hooks/use-confirm-unsaved-product-changes';
import { ProductEditorContext } from '../../types';
import { PostTypeContext } from '../../contexts/post-type-context';
import { ModalEditor } from '../modal-editor';
import { store as productEditorUiStore } from '../../store/product-editor-ui';
type BlockEditorSettings = Partial<
EditorSettings & EditorBlockListSettings
> & {
templates?: Record< string, Template[] >;
};
type BlockEditorProps = {
context: Partial< ProductEditorContext >;
productType: string;
productId: number;
settings: BlockEditorSettings | undefined;
};
import { ModalEditor } from '../modal-editor';
import { ProductEditorSettings } from '../editor';
import { BlockEditorProps } from './types';
export function BlockEditor( {
context,
settings: _settings,
productType,
postType,
productId,
}: BlockEditorProps ) {
useConfirmUnsavedProductChanges( productType );
useConfirmUnsavedProductChanges( postType );
const canUserCreateMedia = useSelect( ( select: typeof WPSelect ) => {
const { canUser } = select( 'core' );
@ -82,7 +69,7 @@ export function BlockEditor( {
return () => window.removeEventListener( 'scroll', wpPinMenuEvent );
}, [] );
const settings: BlockEditorSettings = useMemo( () => {
const settings = useMemo< Partial< ProductEditorSettings > >( () => {
const mediaSettings = canUserCreateMedia
? {
mediaUpload( {
@ -110,27 +97,51 @@ export function BlockEditor( {
};
}, [ canUserCreateMedia, _settings ] );
const [ productType ] = useEntityProp( 'postType', postType, 'type' );
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
'postType',
productType,
postType,
{ id: productId }
);
const { updateEditorSettings } = useDispatch( 'core/editor' );
useLayoutEffect( () => {
const template = settings?.templates?.[ productType ];
const productTemplates = settings?.productTemplates ?? [];
const productTemplate = productTemplates.find(
( template ) => template.productData.type === productType
);
if ( ! template ) {
const layoutTemplates = settings?.layoutTemplates ?? [];
let layoutTemplateId = productTemplate?.layoutTemplateId;
// Product variations do not have a related product template but
// they do have a layout template
if ( postType === 'product_variation' ) {
layoutTemplateId = 'product-variation';
}
const layoutTemplate = layoutTemplates.find(
( template ) => template.id === layoutTemplateId
);
if ( ! layoutTemplate ) {
return;
}
const blockInstances = synchronizeBlocksWithTemplate( [], template );
const blockInstances = synchronizeBlocksWithTemplate(
[],
layoutTemplate.blockTemplates
);
onChange( blockInstances, {} );
updateEditorSettings( settings ?? {} );
}, [ productType, productId ] );
updateEditorSettings( {
...settings,
productTemplate,
} as Partial< ProductEditorSettings > );
}, [ settings, postType, productType ] );
// Check if the Modal editor is open from the store.
const isModalEditorOpen = useSelect( ( select ) => {

View File

@ -1 +1,2 @@
export * from './block-editor';
export * from './types';

View File

@ -0,0 +1,16 @@
/**
* External dependencies
*/
/**
* Internal dependencies
*/
import { ProductEditorContext } from '../../types';
import { ProductEditorSettings } from '../editor';
export type BlockEditorProps = {
context: Partial< ProductEditorContext >;
postType: string;
productId: number;
settings?: ProductEditorSettings;
};

View File

@ -11,13 +11,7 @@ import {
LayoutContextProvider,
useExtendLayout,
} from '@woocommerce/admin-layout';
import {
EditorSettings,
EditorBlockListSettings,
} from '@wordpress/block-editor';
import { Template } from '@wordpress/blocks';
import { Popover } from '@wordpress/components';
import { Product } from '@woocommerce/data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
@ -37,18 +31,7 @@ import { InterfaceSkeleton } from '@wordpress/interface';
import { Header } from '../header';
import { BlockEditor } from '../block-editor';
import { ValidationProvider } from '../../contexts/validation-context';
export type ProductEditorSettings = Partial<
EditorSettings & EditorBlockListSettings
> & {
templates: Record< string, Template[] >;
};
type EditorProps = {
product: Pick< Product, 'id' | 'type' >;
productType?: string;
settings: ProductEditorSettings | undefined;
};
import { EditorProps } from './types';
export function Editor( {
product,
@ -80,7 +63,7 @@ export function Editor( {
<>
<BlockEditor
settings={ settings }
productType={ productType }
postType={ productType }
productId={ product.id }
context={ {
selectedTab,

View File

@ -1,2 +1,3 @@
export * from './editor';
export * from './init-blocks';
export * from './types';

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { Product } from '@woocommerce/data';
import {
EditorSettings,
EditorBlockListSettings,
} from '@wordpress/block-editor';
import { Template } from '@wordpress/blocks';
export type LayoutTemplate = {
id: string;
title: string;
description: string;
area: string;
blockTemplates: Template[];
};
export type ProductTemplate = {
id: string;
title: string;
description: string | null;
icon: string | null;
order: number;
layoutTemplateId: string;
productData: Partial< Product >;
};
export type ProductEditorSettings = Partial<
EditorSettings & EditorBlockListSettings
> & {
layoutTemplates: LayoutTemplate[];
productTemplates: ProductTemplate[];
productTemplate?: ProductTemplate;
};
export type EditorProps = {
product: Pick< Product, 'id' | 'type' >;
productType?: string;
settings?: ProductEditorSettings;
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Introduce a product type selection within the new product editor experience

View File

@ -33,4 +33,11 @@ interface BlockTemplateInterface extends ContainerInterface {
* @return string
*/
public function generate_block_id( string $id_base ): string;
/**
* Get the template as JSON like array.
*
* @return array The JSON.
*/
public function to_json(): array;
}

View File

@ -31,6 +31,7 @@ class BlockRegistry {
'woocommerce/product-pricing-field',
'woocommerce/product-section',
'woocommerce/product-section-description',
'woocommerce/product-details-section-description',
'woocommerce/product-tab',
'woocommerce/product-toggle-field',
'woocommerce/product-taxonomy-field',

View File

@ -8,6 +8,7 @@ namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates\SimpleProductTemplate;
use Automattic\WooCommerce\Internal\Admin\Features\ProductBlockEditor\ProductTemplates\ProductVariationTemplate;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplate;
use Automattic\WooCommerce\Admin\PageController;
use Automattic\WooCommerce\Internal\Admin\BlockTemplateRegistry\BlockTemplateRegistry;
use Automattic\WooCommerce\Internal\Admin\BlockTemplates\Block;
@ -24,11 +25,11 @@ class Init {
const EDITOR_CONTEXT_NAME = 'woocommerce/edit-product';
/**
* Supported post types.
* Supported product types.
*
* @var array
*/
private $supported_post_types = array( 'simple' );
private $supported_product_types = array( 'simple' );
/**
* Redirection controller.
@ -42,18 +43,18 @@ class Init {
*/
public function __construct() {
if ( Features::is_enabled( 'product-variation-management' ) ) {
array_push( $this->supported_post_types, 'variable' );
array_push( $this->supported_product_types, 'variable' );
}
if ( Features::is_enabled( 'product-external-affiliate' ) ) {
array_push( $this->supported_post_types, 'external' );
array_push( $this->supported_product_types, 'external' );
}
if ( Features::is_enabled( 'product-grouped' ) ) {
array_push( $this->supported_post_types, 'grouped' );
array_push( $this->supported_product_types, 'grouped' );
}
$this->redirection_controller = new RedirectionController( $this->supported_post_types );
$this->redirection_controller = new RedirectionController( $this->supported_product_types );
if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
if ( ! Features::is_enabled( 'new-product-management-experience' ) ) {
@ -210,22 +211,38 @@ class Init {
* Get the product editor settings.
*/
private function get_product_editor_settings() {
$layout_template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
$layout_template_logger = BlockTemplateLogger::get_instance();
$editor_settings = array();
$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
$block_template_logger = BlockTemplateLogger::get_instance();
foreach ( $layout_template_registry->get_all_registered() as $layout_template ) {
$editor_settings['layoutTemplates'][] = $layout_template->to_json();
$block_template_logger->log_template_events_to_file( 'simple-product' );
$block_template_logger->log_template_events_to_file( 'product-variation' );
$layout_template_logger->log_template_events_to_file( $layout_template->get_id() );
$editor_settings['layoutTemplateEvents'][] = $layout_template_logger->get_formatted_template_events( $layout_template->get_id() );
}
$editor_settings['templates'] = array(
'product' => $template_registry->get_registered( 'simple-product' )->get_formatted_template(),
'product_variation' => $template_registry->get_registered( 'product-variation' )->get_formatted_template(),
/**
* Allows for new product template registration.
*
* @since 8.5.0
*/
$product_templates = apply_filters( 'woocommerce_product_editor_product_templates', $this->get_default_product_templates() );
$product_templates = $this->create_default_product_template_by_custom_product_type( $product_templates );
usort(
$product_templates,
function ( $a, $b ) {
return $a->get_order() - $b->get_order();
}
);
$editor_settings['templateEvents'] = array(
'product' => $block_template_logger->get_formatted_template_events( 'simple-product' ),
'product_variation' => $block_template_logger->get_formatted_template_events( 'product-variation' ),
$editor_settings['productTemplates'] = array_map(
function ( $product_template ) {
return $product_template->to_json();
},
$product_templates
);
$block_editor_context = new WP_Block_Editor_Context( array( 'name' => self::EDITOR_CONTEXT_NAME ) );
@ -233,12 +250,128 @@ class Init {
return get_block_editor_settings( $editor_settings, $block_editor_context );
}
/**
* Get default product templates.
*
* @return array The default templates.
*/
private function get_default_product_templates() {
$templates = array();
$templates[] = new ProductTemplate(
array(
'id' => 'standard-product-template',
'title' => __( 'Standard product', 'woocommerce' ),
'description' => __( 'A single physical or virtual product, e.g. a t-shirt or an eBook.', 'woocommerce' ),
'order' => 10,
'icon' => 'shipping',
'layout_template_id' => 'simple-product',
'product_data' => array(
'type' => 'simple',
),
)
);
$templates[] = new ProductTemplate(
array(
'id' => 'grouped-product-template',
'title' => __( 'Grouped product', 'woocommerce' ),
'description' => __( 'A set of products that go well together, e.g. camera kit.', 'woocommerce' ),
'order' => 20,
'icon' => 'group',
'layout_template_id' => 'simple-product',
'product_data' => array(
'type' => 'grouped',
),
)
);
$templates[] = new ProductTemplate(
array(
'id' => 'affiliate-product-template',
'title' => __( 'Affiliate product', 'woocommerce' ),
'description' => __( 'A link to a product sold on a different website, e.g. brand collab.', 'woocommerce' ),
'order' => 30,
'icon' => 'link',
'layout_template_id' => 'simple-product',
'product_data' => array(
'type' => 'external',
),
)
);
$templates[] = new ProductTemplate(
array(
'id' => 'variable-product-template',
'title' => __( 'Variable product', 'woocommerce' ),
'description' => __( 'A product with variations like color or size.', 'woocommerce' ),
'order' => 40,
'icon' => null,
'layout_template_id' => 'simple-product',
'product_data' => array(
'type' => 'variable',
),
)
);
return $templates;
}
/**
* Create default product template by custom product type if it does not have a
* template associated yet.
*
* @param array $templates The registered product templates.
* @return array The new templates.
*/
private function create_default_product_template_by_custom_product_type( array $templates ) {
// Getting the product types registered via the classic editor.
$registered_product_types = wc_get_product_types();
$custom_product_types = array_filter(
$registered_product_types,
function ( $product_type ) {
return ! in_array( $product_type, $this->supported_product_types, true );
},
ARRAY_FILTER_USE_KEY
);
$templates_with_product_type = array_filter(
$templates,
function ( $template ) {
$product_data = $template->get_product_data();
return ! is_null( $product_data ) && array_key_exists( 'type', $product_data );
}
);
$custom_product_types_on_templates = array_map(
function ( $template ) {
$product_data = $template->get_product_data();
return $product_data['type'];
},
$templates_with_product_type
);
foreach ( $custom_product_types as $product_type => $title ) {
if ( in_array( $product_type, $custom_product_types_on_templates, true ) ) {
continue;
}
$templates[] = new ProductTemplate(
array(
'id' => $product_type . '-product-template',
'title' => $title,
'product_data' => array(
'type' => $product_type,
),
)
);
}
return $templates;
}
/**
* Register product editor templates.
*/
private function register_product_editor_templates() {
$template_registry = wc_get_container()->get( BlockTemplateRegistry::class );
$template_registry->register( new SimpleProductTemplate() );
$template_registry->register( new ProductVariationTemplate() );
}

View File

@ -0,0 +1,208 @@
<?php
/**
* WooCommerce Product Block Editor
*/
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor;
/**
* The Product Template that represents the relation between the Product and
* the LayoutTemplate (ProductFormTemplateInterface)
*
* @see ProductFormTemplateInterface
*/
class ProductTemplate {
/**
* The template id.
*
* @var string
*/
private $id;
/**
* The template title.
*
* @var string
*/
private $title;
/**
* The product data.
*
* @var array
*/
private $product_data;
/**
* The template order.
*
* @var Integer
*/
private $order = 999;
/**
* The layout template id.
*
* @var string
*/
private $layout_template_id = null;
/**
* The template description.
*
* @var string
*/
private $description = null;
/**
* The template icon.
*
* @var string
*/
private $icon = null;
/**
* ProductTemplate constructor
*
* @param array $data The data.
*/
public function __construct( array $data ) {
$this->id = $data['id'];
$this->title = $data['title'];
$this->product_data = $data['product_data'];
if ( isset( $data['order'] ) ) {
$this->order = $data['order'];
}
if ( isset( $data['layout_template_id'] ) ) {
$this->layout_template_id = $data['layout_template_id'];
}
if ( isset( $data['description'] ) ) {
$this->description = $data['description'];
}
if ( isset( $data['icon'] ) ) {
$this->icon = $data['icon'];
}
}
/**
* Get the template ID.
*
* @return string The ID.
*/
public function get_id() {
return $this->id;
}
/**
* Get the template title.
*
* @return string The title.
*/
public function get_title() {
return $this->title;
}
/**
* Get the layout template ID.
*
* @return string The layout template ID.
*/
public function get_layout_template_id() {
return $this->layout_template_id;
}
/**
* Set the layout template ID.
*
* @param string $layout_template_id The layout template ID.
*/
public function set_layout_template_id( string $layout_template_id ) {
$this->layout_template_id = $layout_template_id;
}
/**
* Get the product data.
*
* @return array The product data.
*/
public function get_product_data() {
return $this->product_data;
}
/**
* Get the template description.
*
* @return string The description.
*/
public function get_description() {
return $this->description;
}
/**
* Set the template description.
*
* @param string $description The template description.
*/
public function set_description( string $description ) {
$this->description = $description;
}
/**
* Get the template icon.
*
* @return string The icon.
*/
public function get_icon() {
return $this->icon;
}
/**
* Set the template icon.
*
* @see https://github.com/WordPress/gutenberg/tree/trunk/packages/icons.
*
* @param string $icon The icon name from the @wordpress/components or a url for an external image resource.
*/
public function set_icon( string $icon ) {
$this->icon = $icon;
}
/**
* Get the template order.
*
* @return int The order.
*/
public function get_order() {
return $this->order;
}
/**
* Set the template order.
*
* @param int $order The template order.
*/
public function set_order( int $order ) {
$this->order = $order;
}
/**
* Get the product template as JSON like.
*
* @return array The JSON.
*/
public function to_json() {
return array(
'id' => $this->get_id(),
'title' => $this->get_title(),
'description' => $this->get_description(),
'icon' => $this->get_icon(),
'order' => $this->get_order(),
'layoutTemplateId' => $this->get_layout_template_id(),
'productData' => $this->get_product_data(),
);
}
}

View File

@ -14,19 +14,19 @@ use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
class RedirectionController {
/**
* Supported post types.
* Supported product types.
*
* @var array
*/
private $supported_post_types;
private $supported_product_types;
/**
* Set up the hooks used for redirection.
*
* @param array $supported_post_types Array of supported post types.
* @param array $supported_product_types Array of supported product types.
*/
public function __construct( $supported_post_types ) {
$this->supported_post_types = $supported_post_types;
public function __construct( $supported_product_types ) {
$this->supported_product_types = $supported_product_types;
if ( \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'product_block_editor' ) ) {
add_action( 'current_screen', array( $this, 'maybe_redirect_to_new_editor' ), 30, 0 );
@ -65,7 +65,7 @@ class RedirectionController {
$product = $product_id ? wc_get_product( $product_id ) : null;
$digital_product = $product->is_downloadable() || $product->is_virtual();
if ( $product && in_array( $product->get_type(), $this->supported_post_types, true ) ) {
if ( $product && in_array( $product->get_type(), $this->supported_product_types, true ) ) {
if ( Features::is_enabled( 'product-virtual-downloadable' ) ) {
return true;
}

View File

@ -72,7 +72,7 @@ final class BlockTemplateRegistry {
*
* @param string $id ID of the template.
*/
public function get_registered( $id ): BlockTemplateInterface {
public function get_registered( $id ): ?BlockTemplateInterface {
return isset( $this->templates[ $id ] ) ? $this->templates[ $id ] : null;
}
}

View File

@ -131,4 +131,19 @@ abstract class AbstractBlockTemplate implements BlockTemplateInterface {
return $inner_blocks_formatted_template;
}
/**
* Get the template as JSON like array.
*
* @return array The JSON.
*/
public function to_json(): array {
return array(
'id' => $this->get_id(),
'title' => $this->get_title(),
'description' => $this->get_description(),
'area' => $this->get_area(),
'blockTemplates' => $this->get_formatted_template(),
);
}
}

View File

@ -9,7 +9,7 @@ use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates\ProductFormTemplateInterface;
/**
* Simple Product Template.
* Product Variation Template.
*/
class ProductVariationTemplate extends AbstractProductFormTemplate implements ProductFormTemplateInterface {
/**
@ -28,7 +28,7 @@ class ProductVariationTemplate extends AbstractProductFormTemplate implements Pr
const SINGLE_VARIATION_NOTICE_DISMISSED_OPTION = 'woocommerce_single_variation_notice_dismissed';
/**
* SimpleProductTemplate constructor.
* ProductVariationTemplate constructor.
*/
public function __construct() {
$this->add_group_blocks();

View File

@ -198,6 +198,13 @@ class SimpleProductTemplate extends AbstractProductFormTemplate implements Produ
),
)
);
$basic_details->add_block(
array(
'id' => 'product-details-section-description',
'blockName' => 'woocommerce/product-details-section-description',
'order' => 10,
)
);
$basic_details->add_block(
array(
'id' => 'product-name',