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,