Commit e3a91de3455 for woocommerce
commit e3a91de34556442a059991c52e3dee3c2ecac185
Author: Luigi Teschio <gigitux@gmail.com>
Date: Tue Oct 20 18:13:17 2026 +0200
Associate the Shop page with the Product Catalog template (#68506)
* Associate the Shop page with the Product Catalog template
* Add changelog for Shop page template association
* WIP
* clean up PR
* improve logic
* improve logic
* fix warning
* fix logic
* deprecate version
* remove strict
* improve logic
* fix e2e test
* Update plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/index.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix crash
* Update plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/block-types.ts
Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
* use store via string
* fix warning widget area
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
diff --git a/plugins/woocommerce/changelog/associate-shop-page-product-catalog b/plugins/woocommerce/changelog/associate-shop-page-product-catalog
new file mode 100644
index 00000000000..2f62ae1d53e
--- /dev/null
+++ b/plugins/woocommerce/changelog/associate-shop-page-product-catalog
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Associate the Shop page with the Product Catalog block template while keeping the template selector hidden.
diff --git a/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/block-types.ts b/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/block-types.ts
index f332fcc5c47..c18a6642b32 100644
--- a/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/block-types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/block-types.ts
@@ -1,3 +1,13 @@
+/**
+ * Template blocks needed to preview the Product Catalog on the Shop page.
+ */
+export const SHOP_PAGE_TEMPLATE_BLOCK_TYPES = [
+ 'woocommerce/breadcrumbs',
+ 'woocommerce/catalog-sorting',
+ 'woocommerce/product-results-count',
+ 'woocommerce/legacy-template',
+];
+
/**
* WooCommerce block types hidden from post editors.
*/
diff --git a/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/index.ts b/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/index.ts
index 9c9c3e9f3af..a2dded51efb 100644
--- a/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/index.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/index.ts
@@ -3,12 +3,17 @@
*/
import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks';
import domReady from '@wordpress/dom-ready';
+import { getSetting, STORE_PAGES } from '@woocommerce/settings';
+import { select, subscribe } from '@wordpress/data';
+// Use the store name to avoid adding wp-editor as a dependency on widget screens.
+import { CORE_EDITOR_STORE } from '@woocommerce/utils';
/**
* Internal dependencies
*/
import {
POST_EDITOR_BLOCK_TYPES_TO_UNREGISTER,
+ SHOP_PAGE_TEMPLATE_BLOCK_TYPES,
WIDGET_EDITOR_ALLOWED_BLOCK_TYPES,
} from './block-types';
@@ -36,13 +41,26 @@ const getBlockEditorContext = (): BlockEditorContext => {
return 'other';
};
-const getBlockTypesToUnregister = (): string[] => {
+const getBlockTypesToUnregister = ( postId?: number ): string[] => {
const registeredBlockTypes = getBlockTypes().map( ( { name } ) => name );
const blockEditorContext = getBlockEditorContext();
if ( blockEditorContext === 'post' ) {
- return POST_EDITOR_BLOCK_TYPES_TO_UNREGISTER.filter( ( blockType ) =>
- registeredBlockTypes.includes( blockType )
+ const shopPageId =
+ 'shop' in STORE_PAGES ? STORE_PAGES.shop?.id : undefined;
+
+ const isShopPageEditor =
+ getSetting( 'currentThemeIsFSETheme', false ) &&
+ !! shopPageId &&
+ Number( postId ) === shopPageId;
+
+ return POST_EDITOR_BLOCK_TYPES_TO_UNREGISTER.filter(
+ ( blockType ) =>
+ registeredBlockTypes.includes( blockType ) &&
+ ! (
+ isShopPageEditor &&
+ SHOP_PAGE_TEMPLATE_BLOCK_TYPES.includes( blockType )
+ )
);
}
@@ -57,8 +75,26 @@ const getBlockTypesToUnregister = (): string[] => {
return [];
};
-domReady( () => {
- getBlockTypesToUnregister().forEach( ( blockType ) => {
+const unregisterBlocks = ( postId?: number ) => {
+ getBlockTypesToUnregister( postId ).forEach( ( blockType ) => {
unregisterBlockType( blockType );
} );
+};
+
+domReady( () => {
+ if ( getBlockEditorContext() !== 'post' ) {
+ unregisterBlocks();
+ return;
+ }
+
+ // DOM readiness does not guarantee the editor has loaded its current post.
+ const unsubscribe = subscribe( () => {
+ if ( ! select( CORE_EDITOR_STORE ).getCurrentPostId() ) {
+ return;
+ }
+
+ const postId = select( CORE_EDITOR_STORE )?.getCurrentPostId() ?? null;
+ unsubscribe();
+ unregisterBlocks( postId );
+ }, CORE_EDITOR_STORE );
} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/test/index.ts b/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/test/index.ts
index 3c62652f0b6..5412a20efb2 100644
--- a/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/test/index.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/filters/unregister-block-types/test/index.ts
@@ -2,6 +2,22 @@
* External dependencies
*/
import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks';
+import { getSetting } from '@woocommerce/settings';
+import { select, subscribe } from '@wordpress/data';
+
+jest.mock( '@woocommerce/utils', () =>
+ jest.requireActual( '@woocommerce/utils/wordpress-stores' )
+);
+
+jest.mock( '@wordpress/data', () => ( {
+ select: jest.fn(),
+ subscribe: jest.fn(),
+} ) );
+
+jest.mock( '@woocommerce/settings', () => ( {
+ getSetting: jest.fn(),
+ STORE_PAGES: { shop: { id: 42 } },
+} ) );
jest.mock( '@wordpress/blocks', () => ( {
getBlockTypes: jest.fn(),
@@ -13,7 +29,10 @@ jest.mock( '@wordpress/dom-ready', () => ( {
default: jest.fn( ( callback ) => callback() ),
} ) );
-const loadFilter = (
+const getCurrentPostId = jest.fn();
+const unsubscribe = jest.fn();
+
+const loadFilter = async (
adminPage: string | undefined,
blockTypes: string[],
pageNow?: string
@@ -28,20 +47,73 @@ const loadFilter = (
blockTypes.map( ( name ) => ( { name } ) )
);
- jest.isolateModules( () => {
- require( '../index' );
+ await jest.isolateModulesAsync( async () => {
+ await import( '../index' );
} );
+
+ if ( getCurrentPostId() ) {
+ jest.mocked( subscribe ).mock.calls[ 0 ]?.[ 0 ]();
+ }
};
describe( 'unregister block types', () => {
beforeEach( () => {
jest.clearAllMocks();
+ ( getSetting as jest.Mock ).mockReturnValue( false );
+ getCurrentPostId.mockReturnValue( 1 );
+ ( select as jest.Mock ).mockReturnValue( { getCurrentPostId } );
+ ( subscribe as jest.Mock ).mockReturnValue( unsubscribe );
+ } );
+
+ it.each( [
+ [ 'the Shop page in a block theme', 42, true, 1 ],
+ [ 'another page in a block theme', 43, true, 4 ],
+ [ 'the Shop page in a classic theme', 42, false, 4 ],
+ ] )(
+ 'keeps catalog blocks registered only for the Shop page template preview (%s)',
+ async ( _context, postId, isBlockTheme, expectedCount ) => {
+ ( getSetting as jest.Mock ).mockReturnValue( isBlockTheme );
+ getCurrentPostId.mockReturnValue( postId );
+ await loadFilter( 'post-php', [
+ 'woocommerce/breadcrumbs',
+ 'woocommerce/catalog-sorting',
+ 'woocommerce/product-results-count',
+ 'woocommerce/product-reviews',
+ ] );
+
+ expect( unregisterBlockType ).toHaveBeenCalledTimes(
+ expectedCount
+ );
+ expect( unregisterBlockType ).toHaveBeenCalledWith(
+ 'woocommerce/product-reviews'
+ );
+ }
+ );
+
+ it( 'waits for the Shop page to load before unregistering blocks', async () => {
+ ( getSetting as jest.Mock ).mockReturnValue( true );
+ getCurrentPostId.mockReturnValue( undefined );
+ await loadFilter( 'post-php', [
+ 'woocommerce/breadcrumbs',
+ 'woocommerce/product-reviews',
+ ] );
+ expect( unregisterBlockType ).not.toHaveBeenCalled();
+
+ getCurrentPostId.mockReturnValue( 42 );
+ const [ onEditorChange ] = jest.mocked( subscribe ).mock.calls[ 0 ];
+ onEditorChange();
+
+ expect( unregisterBlockType ).toHaveBeenCalledTimes( 1 );
+ expect( unregisterBlockType ).toHaveBeenCalledWith(
+ 'woocommerce/product-reviews'
+ );
+ expect( unsubscribe ).toHaveBeenCalledTimes( 1 );
} );
it.each( [ 'post-php', 'post-new-php' ] )(
'unregisters only post-editor block types in the deny list in %s',
- ( adminPage ) => {
- loadFilter( adminPage, [
+ async ( adminPage ) => {
+ await loadFilter( adminPage, [
'woocommerce/breadcrumbs',
'woocommerce/catalog-sorting',
'woocommerce/product-results-count',
@@ -77,8 +149,8 @@ describe( 'unregister block types', () => {
[ 'the Customizer', undefined, 'customize' ],
] )(
'unregisters WooCommerce blocks outside the widget-editor allow list in %s',
- ( _context, adminPage, pageNow ) => {
- loadFilter(
+ async ( _context, adminPage, pageNow ) => {
+ await loadFilter(
adminPage,
[
'woocommerce/product-search',
@@ -119,8 +191,8 @@ describe( 'unregister block types', () => {
it.each( [ 'site-editor-php', undefined ] )(
'does not unregister blocks in unrestricted editor contexts (%s)',
- ( adminPage ) => {
- loadFilter( adminPage, [
+ async ( adminPage ) => {
+ await loadFilter( adminPage, [
'woocommerce/breadcrumbs',
'woocommerce/catalog-sorting',
'woocommerce/checkout',
diff --git a/plugins/woocommerce/client/blocks/assets/js/index.js b/plugins/woocommerce/client/blocks/assets/js/index.js
index fdf74a7f893..9aeed7decf8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/index.js
+++ b/plugins/woocommerce/client/blocks/assets/js/index.js
@@ -7,5 +7,6 @@ import './filters/block-list-block';
import './filters/get-block-attributes';
import './filters/hide-incompatible-template-parts';
import './filters/unregister-block-types';
+import './plugins/shop-page-rendering-mode';
import './base/components/notice-banner/style.scss';
import './blocks/product-elements-blocks/variations';
diff --git a/plugins/woocommerce/client/blocks/assets/js/plugins/shop-page-rendering-mode/index.ts b/plugins/woocommerce/client/blocks/assets/js/plugins/shop-page-rendering-mode/index.ts
new file mode 100644
index 00000000000..572ae00eb22
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/plugins/shop-page-rendering-mode/index.ts
@@ -0,0 +1,49 @@
+/**
+ * External dependencies
+ */
+import { STORE_PAGES } from '@woocommerce/settings';
+// Use the store name to avoid adding wp-editor as a dependency on widget screens.
+import { CORE_EDITOR_STORE } from '@woocommerce/utils/wordpress-stores';
+import { store as coreStore } from '@wordpress/core-data';
+import { select as selectStore, useDispatch, useSelect } from '@wordpress/data';
+import domReady from '@wordpress/dom-ready';
+import { useEffect } from '@wordpress/element';
+import { registerPlugin } from '@wordpress/plugins';
+
+const ShopPageRenderingMode = () => {
+ const { setRenderingMode } = useDispatch( CORE_EDITOR_STORE );
+ const shouldPreviewTemplate = useSelect( ( select ) => {
+ const editor = select( CORE_EDITOR_STORE );
+ const shopPageId =
+ 'shop' in STORE_PAGES ? STORE_PAGES.shop?.id : undefined;
+
+ const isBlockTheme =
+ select( coreStore )?.getCurrentTheme()?.is_block_theme;
+
+ return (
+ isBlockTheme &&
+ !! shopPageId &&
+ !! editor &&
+ editor.getCurrentPostType() === 'page' &&
+ Number( editor.getCurrentPostId() ) === shopPageId &&
+ !! editor.getEditorSettings().supportsTemplateMode
+ );
+ }, [] );
+
+ useEffect( () => {
+ // Set the initial preview without overriding subsequent user toggles.
+ if ( shouldPreviewTemplate ) {
+ setRenderingMode( 'template-locked' );
+ }
+ }, [ shouldPreviewTemplate, setRenderingMode ] );
+
+ return null;
+};
+
+domReady( () => {
+ if ( selectStore( CORE_EDITOR_STORE ) ) {
+ registerPlugin( 'woocommerce-shop-page-rendering-mode', {
+ render: ShopPageRenderingMode,
+ } );
+ }
+} );
diff --git a/plugins/woocommerce/client/blocks/package.json b/plugins/woocommerce/client/blocks/package.json
index c9bc1f6c33b..9535b0a4f1d 100644
--- a/plugins/woocommerce/client/blocks/package.json
+++ b/plugins/woocommerce/client/blocks/package.json
@@ -21,6 +21,7 @@
"*.scss",
"./assets/js/blocks/product-elements-blocks/**",
"./assets/js/filters/**",
+ "./assets/js/plugins/**",
"./assets/js/middleware/**",
"./packages/public-api/types/**",
"./assets/js/blocks/checkout/inner-blocks/**/index.tsx",
diff --git a/plugins/woocommerce/src/Blocks/Templates/ProductCatalogTemplate.php b/plugins/woocommerce/src/Blocks/Templates/ProductCatalogTemplate.php
index 616cf2bb878..82344913ad1 100644
--- a/plugins/woocommerce/src/Blocks/Templates/ProductCatalogTemplate.php
+++ b/plugins/woocommerce/src/Blocks/Templates/ProductCatalogTemplate.php
@@ -4,7 +4,6 @@ namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\BlockTemplatesRegistry;
use Automattic\WooCommerce\Blocks\Package;
-use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
/**
* ProductCatalogTemplate class.
@@ -25,9 +24,59 @@ class ProductCatalogTemplate extends AbstractTemplate {
*/
public function init() {
add_action( 'template_redirect', array( $this, 'render_block_template' ) );
- add_filter( 'current_theme_supports-block-templates', array( $this, 'remove_block_template_support_for_shop_page' ) );
+ add_filter( 'get_block_templates', array( $this, 'handle_get_block_templates_for_post_content' ), 20, 3 );
+ add_filter( 'get_post_metadata', array( $this, 'handle_get_post_metadata' ), 10, 4 );
}
+ /**
+ * Use the Product Catalog template for the Shop page without changing its saved template.
+ *
+ * @internal
+ *
+ * @param mixed $value Short-circuited metadata value, or null.
+ * @param int $post_id Post ID.
+ * @param string $meta_key Metadata key.
+ * @param bool $single Whether to return a single value.
+ * @return mixed
+ */
+ public function handle_get_post_metadata( $value, $post_id, $meta_key, $single ) {
+ if (
+ null === $value &&
+ '_wp_page_template' === $meta_key &&
+ wp_is_block_theme() &&
+ wc_get_page_id( 'shop' ) === $post_id
+ ) {
+ return $single ? self::SLUG : array( self::SLUG );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Ensure WordPress can read the first Product Catalog template at index 0.
+ * See https://core.trac.wordpress.org/ticket/62407
+ *
+ * @internal
+ *
+ * @param \WP_Block_Template[] $templates Available templates.
+ * @param array $query Template query arguments.
+ * @param string $template_type Template type.
+ * @return \WP_Block_Template[]
+ */
+ public static function handle_get_block_templates_for_post_content( $templates, $query, $template_type ) {
+ if (
+ 'wp_template' === $template_type &&
+ is_array( $templates ) &&
+ is_array( $query['slug__in'] ?? null ) &&
+ in_array( self::SLUG, $query['slug__in'], true )
+ ) {
+ return array_values( $templates );
+ }
+
+ return $templates;
+ }
+
+
/**
* Returns the title of the template.
*
@@ -56,28 +105,16 @@ class ProductCatalogTemplate extends AbstractTemplate {
}
/**
- * Remove the template panel from the Sidebar of the Shop page because
- * the Site Editor handles it.
+ * Hide Shop page template controls for classic themes.
+ *
+ * @deprecated 11.3.0
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/6278
*
* @param bool $is_support Whether the active theme supports block templates.
- *
- * @return bool
+ * @return void
*/
public function remove_block_template_support_for_shop_page( $is_support ) {
- global $pagenow, $post;
-
- if (
- is_admin() &&
- 'post.php' === $pagenow &&
- function_exists( 'wc_get_page_id' ) &&
- is_a( $post, 'WP_Post' ) &&
- wc_get_page_id( 'shop' ) === $post->ID
- ) {
- return false;
- }
-
- return $is_support;
+ wc_deprecated_function( __METHOD__, '11.3.0' );
}
}
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/templates/shop-page.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/templates/shop-page.block_theme.spec.ts
index f2c50fbbcd3..bb0f00ef4b5 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/templates/shop-page.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/templates/shop-page.block_theme.spec.ts
@@ -27,18 +27,6 @@ const expectShopTemplateToBeLoaded = async ( page: Page ) => {
};
test.describe( 'Shop page', () => {
- test( 'template selector is not visible in the Page editor', async ( {
- admin,
- page,
- requestUtils,
- } ) => {
- const shopPageId = await getShopPageId( requestUtils );
-
- await admin.editPost( String( shopPageId ) );
-
- await expect( page.getByText( 'Template' ) ).toBeHidden();
- } );
-
test( 'loads the product catalog template after the shop page permalink is updated via REST API', async ( {
page,
requestUtils,