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:
parent
ad735b2bf2
commit
ee6642e0c7
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Load layout templates via the REST API.
|
|
@ -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( {
|
||||
|
|
|
@ -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 ) => {
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './use-product-template';
|
|
@ -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' );
|
||||
} );
|
||||
} );
|
|
@ -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 };
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Remove putting template layouts on the productBlockEditorSettings JS global.
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue