diff --git a/packages/js/product-editor/changelog/update-use-use-layout-template b/packages/js/product-editor/changelog/update-use-use-layout-template new file mode 100644 index 00000000000..bbb8e028838 --- /dev/null +++ b/packages/js/product-editor/changelog/update-use-use-layout-template @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Load layout templates via the REST API. diff --git a/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx b/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx index 58be9f06d5f..31de196b771 100644 --- a/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx +++ b/packages/js/product-editor/src/blocks/product-fields/product-details-section-description/edit.tsx @@ -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( { diff --git a/packages/js/product-editor/src/components/block-editor/block-editor.tsx b/packages/js/product-editor/src/components/block-editor/block-editor.tsx index 8c74cbf67fc..8008423cec4 100644 --- a/packages/js/product-editor/src/components/block-editor/block-editor.tsx +++ b/packages/js/product-editor/src/components/block-editor/block-editor.tsx @@ -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 ) => { diff --git a/packages/js/product-editor/src/components/editor/types.ts b/packages/js/product-editor/src/components/editor/types.ts index d26ee90bb42..5501515b58d 100644 --- a/packages/js/product-editor/src/components/editor/types.ts +++ b/packages/js/product-editor/src/components/editor/types.ts @@ -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; }; diff --git a/packages/js/product-editor/src/hooks/index.ts b/packages/js/product-editor/src/hooks/index.ts index 531e68a8f22..73ed679d57b 100644 --- a/packages/js/product-editor/src/hooks/index.ts +++ b/packages/js/product-editor/src/hooks/index.ts @@ -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'; diff --git a/packages/js/product-editor/src/hooks/use-product-template/index.ts b/packages/js/product-editor/src/hooks/use-product-template/index.ts new file mode 100644 index 00000000000..e2db5322cf4 --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-product-template/index.ts @@ -0,0 +1 @@ +export * from './use-product-template'; diff --git a/packages/js/product-editor/src/hooks/use-product-template/test/use-product-template.test.ts b/packages/js/product-editor/src/hooks/use-product-template/test/use-product-template.test.ts new file mode 100644 index 00000000000..c88d81b0e98 --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-product-template/test/use-product-template.test.ts @@ -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' ); + } ); +} ); diff --git a/packages/js/product-editor/src/hooks/use-product-template/use-product-template.ts b/packages/js/product-editor/src/hooks/use-product-template/use-product-template.ts new file mode 100644 index 00000000000..49bc19c1610 --- /dev/null +++ b/packages/js/product-editor/src/hooks/use-product-template/use-product-template.ts @@ -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 }; +}; diff --git a/packages/js/product-editor/src/types.ts b/packages/js/product-editor/src/types.ts index 8e2aedea395..3b71eaddbff 100644 --- a/packages/js/product-editor/src/types.ts +++ b/packages/js/product-editor/src/types.ts @@ -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; diff --git a/plugins/woocommerce/changelog/update-use-use-layout-template b/plugins/woocommerce/changelog/update-use-use-layout-template new file mode 100644 index 00000000000..76f7a1afb7b --- /dev/null +++ b/plugins/woocommerce/changelog/update-use-use-layout-template @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Remove putting template layouts on the productBlockEditorSettings JS global. diff --git a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php index 52c73af8f77..d15ad551d45 100644 --- a/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php +++ b/plugins/woocommerce/src/Admin/Features/ProductBlockEditor/Init.php @@ -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();