Product Editor: Load layout templates from the REST API (#43384)

* Move ProductTemplate type

* useProductTemplate

* Use useProductTemplate

* Use useLayoutTemplate

* Handle if window.productBlockEditorSettings doesn't exist

* Unit tests for useProductTemplate

* Fix fallback in useProductTemplate

* Remove layoutTemplates from ProductEditorSettings on client

* Fallback to simple-product layout template

* Unit test to verify the standard product template is used if product type is variable

* Use standard product template if product type is variable

* Unit test to verify product type is used to match if product template id matches a template with a different product type

* Make sure product type matches on product template, unless variable, in which case we match simple

* Remove layoutTemplates and layoutTemplateEvents from global

* Changelog

* Changelog

* Import types only
This commit is contained in:
Matt Sherman 2024-01-12 11:54:35 -05:00 committed by GitHub
parent ad735b2bf2
commit ee6642e0c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 231 additions and 54 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Load layout templates via the REST API.

View File

@ -29,14 +29,16 @@ 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 type {
ProductEditorBlockEditProps,
ProductTemplate,
} from '../../../types';
import { ProductDetailsSectionDescriptionBlockAttributes } from './types';
export function ProductDetailsSectionDescriptionBlockEdit( {

View File

@ -12,6 +12,7 @@ import { useDispatch, useSelect, select as WPSelect } from '@wordpress/data';
import { uploadMedia } from '@wordpress/media-utils';
import { PluginArea } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import { useLayoutTemplate } from '@woocommerce/block-templates';
import { Product } from '@woocommerce/data';
import {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -38,12 +39,29 @@ import {
*/
import useProductEntityProp from '../../hooks/use-product-entity-prop';
import { useConfirmUnsavedProductChanges } from '../../hooks/use-confirm-unsaved-product-changes';
import { useProductTemplate } from '../../hooks/use-product-template';
import { PostTypeContext } from '../../contexts/post-type-context';
import { store as productEditorUiStore } from '../../store/product-editor-ui';
import { ModalEditor } from '../modal-editor';
import { ProductEditorSettings } from '../editor';
import { BlockEditorProps } from './types';
import { ProductTemplate } from '../../types';
function getLayoutTemplateId(
productTemplate: ProductTemplate | undefined,
postType: string
) {
if ( productTemplate?.layoutTemplateId ) {
return productTemplate.layoutTemplateId;
}
if ( postType === 'product_variation' ) {
return 'product-variation';
}
// Fallback to simple product if no layout template is set.
return 'simple-product';
}
export function BlockEditor( {
context,
settings: _settings,
@ -107,6 +125,15 @@ export function BlockEditor( {
{ postType }
);
const { productTemplate } = useProductTemplate(
productTemplateId,
productType
);
const { layoutTemplate } = useLayoutTemplate(
getLayoutTemplateId( productTemplate, postType )
);
const [ blocks, onInput, onChange ] = useEntityBlockEditor(
'postType',
postType,
@ -116,34 +143,6 @@ export function BlockEditor( {
const { updateEditorSettings } = useDispatch( 'core/editor' );
useLayoutEffect( () => {
const productTemplates = settings?.productTemplates ?? [];
const productTemplate = productTemplates.find( ( template ) => {
if ( productTemplateId === template.id ) {
return true;
}
if ( ! productType ) {
return false;
}
// Fallback to the product type if the product does not have any product
// template associated to itself.
return template.productData.type === productType;
} );
const layoutTemplates = settings?.layoutTemplates ?? [];
let layoutTemplateId = productTemplate?.layoutTemplateId;
// A product variation is not a product type so we can not use it to
// fallback to a default layout template. We use a post type instead.
if ( ! layoutTemplateId && postType === 'product_variation' ) {
layoutTemplateId = 'product-variation';
}
const layoutTemplate = layoutTemplates.find(
( template ) => template.id === layoutTemplateId
);
if ( ! layoutTemplate ) {
return;
}
@ -159,7 +158,7 @@ export function BlockEditor( {
...settings,
productTemplate,
} as Partial< ProductEditorSettings > );
}, [ settings, postType, productTemplateId, productType ] );
}, [ settings, postType, productTemplate, productType, layoutTemplate ] );
// Check if the Modal editor is open from the store.
const isModalEditorOpen = useSelect( ( select ) => {

View File

@ -8,6 +8,11 @@ import {
} from '@wordpress/block-editor';
import { Template } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import { ProductTemplate } from '../../types';
export type LayoutTemplate = {
id: string;
title: string;
@ -16,20 +21,9 @@ export type LayoutTemplate = {
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;
};

View File

@ -5,3 +5,4 @@ export { useCurrencyInputProps as __experimentalUseCurrencyInputProps } from './
export { useVariationSwitcher as __experimentalUseVariationSwitcher } from './use-variation-switcher';
export { default as __experimentalUseProductEntityProp } from './use-product-entity-prop';
export { default as __experimentalUseProductMetadata } from './use-product-metadata';
export { useProductTemplate as __experimentalUseProductTemplate } from './use-product-template';

View File

@ -0,0 +1 @@
export * from './use-product-template';

View File

@ -0,0 +1,120 @@
/**
* External dependencies
*/
import { renderHook } from '@testing-library/react-hooks';
/**
* Internal dependencies
*/
import { useProductTemplate } from '../use-product-template';
const originalProductBlockEditorSettings =
globalThis.window.productBlockEditorSettings;
describe( 'useProductTemplate', () => {
beforeEach( () => {
globalThis.window.productBlockEditorSettings = {
productTemplates: [
{
id: 'template-1',
title: 'Template 1',
description: 'Template 1 description',
icon: 'icon',
order: 1,
layoutTemplateId: 'layout-template-1',
productData: {
type: 'simple',
},
},
{
id: 'template-2',
title: 'Template 2',
description: 'Template 2 description',
icon: 'icon',
layoutTemplateId: 'layout-template-2',
order: 2,
productData: {
type: 'grouped',
},
},
{
id: 'template-3',
title: 'Template 3',
description: 'Template 3 description',
icon: 'icon',
layoutTemplateId: 'layout-template-3',
order: 3,
productData: {
type: 'simple',
},
},
{
id: 'standard-product-template',
title: 'Standard Product Template',
description: 'Standard Product Template description',
icon: 'icon',
layoutTemplateId: 'layout-template-4',
order: 4,
productData: {
type: 'simple',
},
},
],
};
} );
afterEach( () => {
globalThis.window.productBlockEditorSettings =
originalProductBlockEditorSettings;
} );
it( 'should return the product template if it exists', () => {
const { result } = renderHook( () =>
useProductTemplate( 'template-3', 'simple' )
);
expect( result.current.productTemplate?.id ).toEqual( 'template-3' );
} );
it( 'should return the first product template with a matching type in the productData if no matching product template by id', () => {
const { result } = renderHook( () =>
useProductTemplate( 'invalid-template-id', 'grouped' )
);
expect( result.current.productTemplate?.id ).toEqual( 'template-2' );
} );
it( 'should return the first product template with a matching type in the productData if no product template id is set', () => {
const { result } = renderHook( () =>
useProductTemplate( undefined, 'grouped' )
);
expect( result.current.productTemplate?.id ).toEqual( 'template-2' );
} );
it( 'should return undefined if no matching product template by id or type', () => {
const { result } = renderHook( () =>
useProductTemplate( 'invalid-template-id', 'external' )
);
expect( result.current.productTemplate ).toBeUndefined();
} );
it( 'should use the standard product template if the product type is variable', () => {
const { result } = renderHook( () =>
useProductTemplate( 'template-1', 'variable' )
);
expect( result.current.productTemplate?.id ).toEqual(
'standard-product-template'
);
} );
it( 'should use the product type to match if the product template id matches a template with a different product type', () => {
const { result } = renderHook( () =>
useProductTemplate( 'template-2', 'simple' )
);
expect( result.current.productTemplate?.id ).toEqual( 'template-1' );
} );
} );

View File

@ -0,0 +1,53 @@
/**
* External dependencies
*/
import { ProductType } from '@woocommerce/data';
/**
* Internal dependencies
*/
import { ProductTemplate } from '../../types';
declare global {
interface Window {
productBlockEditorSettings: {
productTemplates: ProductTemplate[];
};
}
}
export const useProductTemplate = (
productTemplateId: string | undefined,
productType: ProductType | undefined
) => {
const productTemplates =
window.productBlockEditorSettings?.productTemplates ?? [];
const productTemplateIdToFind =
productType === 'variable'
? 'standard-product-template'
: productTemplateId;
const productTypeToFind =
productType === 'variable' ? 'simple' : productType;
let matchingProductTemplate = productTemplates.find(
( productTemplate ) =>
productTemplate.id === productTemplateIdToFind &&
productTemplate.productData.type === productTypeToFind
);
if ( ! matchingProductTemplate ) {
// Fallback to the first template with the same product type.
matchingProductTemplate = productTemplates.find(
( productTemplate ) =>
productTemplate.productData.type === productTypeToFind
);
}
// When we switch to getting the product template from the API,
// this will be needed.
const isResolving = false;
return { productTemplate: matchingProductTemplate, isResolving };
};

View File

@ -2,6 +2,17 @@
* External dependencies
*/
import { BlockAttributes, BlockEditProps } from '@wordpress/blocks';
import { Product } from '@woocommerce/data';
export type ProductTemplate = {
id: string;
title: string;
description: string | null;
icon: string | null;
order: number;
layoutTemplateId: string;
productData: Partial< Product >;
};
export interface ProductEditorContext {
postId: number;

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Remove putting template layouts on the productBlockEditorSettings JS global.

View File

@ -224,18 +224,6 @@ class Init {
* Get the product editor settings.
*/
private function get_product_editor_settings() {
$layout_template_registry = wc_get_container()->get( LayoutTemplateRegistry::class );
$layout_template_logger = BlockTemplateLogger::get_instance();
$editor_settings = array();
foreach ( $layout_template_registry->instantiate_layout_templates() as $layout_template ) {
$editor_settings['layoutTemplates'][] = $layout_template->to_json();
$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['productTemplates'] = array_map(
function ( $product_template ) {
return $product_template->to_json();