Commit 17f5ecb498 for woocommerce

commit 17f5ecb49829fc3d85ec34497d676d40567a3959
Author: Jan Lysý <lysyjan@users.noreply.github.com>
Date:   Mon Dec 15 20:01:43 2025 +0100

    Add new email pattern categories and load them dynamically from block patterns (#62441)

    * Localized template category labels in email editor

    * Updated template category logic to dynamically use first category from content pattern

    * Added new email pattern categories: Basic, Welcome, and Abandoned Cart

    * Load template categories dynamically from block patterns

    * Renamed "Basic" email pattern category to "Newsletter"

    * Add category tabs navigation and method to retrieve email pattern categories

    * Updated test assertions to reflect renaming of "Basic" category to "Newsletter" in email pattern categories

    * Add cleanup for setTimeout in template selection modal

    * Address PR review feedback

    - Replace local PatternCategory type with UserPatternCategory imported from @wordpress/core-data/build-types/selectors
    - Update JS changelog to document TemplateCategory type widening
    - Update PHP changelog to document 'basic' to 'newsletter' category rename

    * Remove redundant email pattern categories and update associated test assertions

diff --git a/packages/js/email-editor/changelog/wooprd-1118-add-category-tabs-navigation b/packages/js/email-editor/changelog/wooprd-1118-add-category-tabs-navigation
new file mode 100644
index 0000000000..3f985d8fd0
--- /dev/null
+++ b/packages/js/email-editor/changelog/wooprd-1118-add-category-tabs-navigation
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add category tabs navigation to email template selection modal. The TemplateCategory type is now a string to support dynamic categories loaded from block patterns.
diff --git a/packages/js/email-editor/src/components/template-select/select-modal.tsx b/packages/js/email-editor/src/components/template-select/select-modal.tsx
index 94cb407df1..00d8d58408 100644
--- a/packages/js/email-editor/src/components/template-select/select-modal.tsx
+++ b/packages/js/email-editor/src/components/template-select/select-modal.tsx
@@ -1,9 +1,11 @@
 /**
  * External dependencies
  */
-import { useState, useEffect, memo } from '@wordpress/element';
+import { useState, useEffect, useMemo, memo } from '@wordpress/element';
 import { store as editorStore } from '@wordpress/editor';
-import { dispatch } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import type { UserPatternCategory } from '@wordpress/core-data/build-types/selectors';
+import { dispatch, useSelect } from '@wordpress/data';
 import { Modal, Button, Flex, FlexItem } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';

@@ -21,32 +23,64 @@ import { TemplateList } from './template-list';
 import { TemplateCategoriesListSidebar } from './template-categories-list-sidebar';
 import { recordEvent, recordEventOnce } from '../../events';

-const TemplateCategories: Array< { name: TemplateCategory; label: string } > = [
-	{
-		name: 'recent',
-		label: 'Recent',
-	},
-	{
-		name: 'basic',
-		label: 'Basic',
-	},
-];
+function getCategoriesFromTemplates(
+	templates: TemplatePreview[],
+	patternCategories: UserPatternCategory[]
+): Array< { name: TemplateCategory; label: string } > {
+	const categoryLabels = new Map< string, string >(
+		patternCategories.map( ( cat ) => [ cat.name, cat.label ] )
+	);
+	// Add localized label for 'recent' category (used by email posts)
+	categoryLabels.set( 'recent', __( 'Recent', 'woocommerce' ) );
+
+	const uniqueCategories = new Set< string >();
+	for ( const template of templates ) {
+		if ( template.category ) {
+			uniqueCategories.add( template.category );
+		}
+	}
+
+	return [ ...uniqueCategories ].map( ( category ) => ( {
+		name: category as TemplateCategory,
+		label: categoryLabels.get( category ) ?? category,
+	} ) );
+}

 function SelectTemplateBody( {
-	hasEmailPosts,
 	templates,
 	handleTemplateSelection,
 	templateSelectMode,
 } ) {
-	const [ selectedCategory, setSelectedCategory ] = useState(
-		TemplateCategories[ 1 ].name // Show the “Basic” category by default
+	const patternCategories = useSelect(
+		( select ) =>
+			select(
+				coreStore
+			).getBlockPatternCategories() as UserPatternCategory[],
+		[]
 	);

 	const hideRecentCategory = templateSelectMode === 'swap';

-	const displayCategories = TemplateCategories.filter(
-		( { name } ) => name !== 'recent' || ! hideRecentCategory
-	);
+	const displayCategories = useMemo( () => {
+		const allCategories = getCategoriesFromTemplates(
+			templates,
+			patternCategories ?? []
+		);
+
+		if ( hideRecentCategory ) {
+			return allCategories.filter( ( cat ) => cat.name !== 'recent' );
+		}
+
+		// Put 'recent' category first
+		return allCategories.sort( ( a, b ) => {
+			if ( a.name === 'recent' ) return -1;
+			if ( b.name === 'recent' ) return 1;
+			return 0;
+		} );
+	}, [ templates, patternCategories, hideRecentCategory ] );
+
+	const [ selectedCategory, setSelectedCategory ] =
+		useState< TemplateCategory | null >( null );

 	const handleCategorySelection = ( category: TemplateCategory ) => {
 		recordEvent( 'template_select_modal_category_change', { category } );
@@ -54,12 +88,19 @@ function SelectTemplateBody( {
 	};

 	useEffect( () => {
-		setTimeout( () => {
-			if ( hasEmailPosts && ! hideRecentCategory ) {
-				setSelectedCategory( TemplateCategories[ 0 ].name );
-			}
+		if ( selectedCategory !== null || displayCategories.length === 0 ) {
+			return undefined;
+		}
+
+		const timeoutId = setTimeout( () => {
+			const defaultCategory =
+				displayCategories.find( ( cat ) => cat.name !== 'recent' )
+					?.name ?? displayCategories[ 0 ]?.name;
+			setSelectedCategory( defaultCategory );
 		}, 1000 ); // using setTimeout to ensure the template styles are available before block preview
-	}, [ hasEmailPosts, hideRecentCategory ] );
+
+		return () => clearTimeout( timeoutId );
+	}, [ displayCategories, selectedCategory ] );

 	return (
 		<div className="block-editor-block-patterns-explorer">
@@ -89,8 +130,7 @@ export function SelectTemplateModal( {
 	const templateSelectMode = previewContent ? 'swap' : 'new';
 	recordEventOnce( 'template_select_modal_opened', { templateSelectMode } );

-	const [ templates, emailPosts, hasEmailPosts ] =
-		usePreviewTemplates( previewContent );
+	const [ templates, emailPosts ] = usePreviewTemplates( previewContent );

 	const hasTemplates = templates?.length > 0;

@@ -147,7 +187,6 @@ export function SelectTemplateModal( {
 			isFullScreen
 		>
 			<MemorizedSelectTemplateBody
-				hasEmailPosts={ hasEmailPosts }
 				templates={ [ ...templates, ...emailPosts ] }
 				handleTemplateSelection={ handleTemplateSelection }
 				templateSelectMode={ templateSelectMode }
diff --git a/packages/js/email-editor/src/hooks/use-preview-templates.ts b/packages/js/email-editor/src/hooks/use-preview-templates.ts
index 80a3ba7af6..01d2982ce3 100644
--- a/packages/js/email-editor/src/hooks/use-preview-templates.ts
+++ b/packages/js/email-editor/src/hooks/use-preview-templates.ts
@@ -156,7 +156,7 @@ export function usePreviewTemplates(
 						previewContentParsed: parsedTemplate,
 						emailParsed: contentPattern.blocks,
 						template,
-						category: 'basic', // TODO: This will be updated once template category is implemented
+						category: contentPattern.categories?.[ 0 ],
 						type: template.type,
 						displayName: contentPattern.title
 							? `${ template.title.rendered } - ${ contentPattern.title }`
diff --git a/packages/js/email-editor/src/store/types.ts b/packages/js/email-editor/src/store/types.ts
index 02a1526685..d22760f5b7 100644
--- a/packages/js/email-editor/src/store/types.ts
+++ b/packages/js/email-editor/src/store/types.ts
@@ -167,7 +167,7 @@ export type TemplatePreview = {
 	type: string;
 };

-export type TemplateCategory = 'recent' | 'basic';
+export type TemplateCategory = string;

 export type Feature =
 	| 'fullscreenMode'
diff --git a/packages/php/email-editor/changelog/wooprd-1118-add-category-tabs-navigation b/packages/php/email-editor/changelog/wooprd-1118-add-category-tabs-navigation
new file mode 100644
index 0000000000..cc5b35d1e9
--- /dev/null
+++ b/packages/php/email-editor/changelog/wooprd-1118-add-category-tabs-navigation
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add category tabs navigation to email template selection modal.
diff --git a/packages/php/email-editor/tests/integration/Engine/Patterns/Patterns_Test.php b/packages/php/email-editor/tests/integration/Engine/Patterns/Patterns_Test.php
index daca53771d..166b38b97d 100644
--- a/packages/php/email-editor/tests/integration/Engine/Patterns/Patterns_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Patterns/Patterns_Test.php
@@ -32,12 +32,12 @@ class Patterns_Test extends \Email_Editor_Integration_Test_Case {
 	 */
 	public function testItRegistersPatternCategories(): void {
 		$this->patterns->initialize();
-		$categories = \WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered();
-		/** @var array{name: string, label: string, description: string} $category */ // phpcs:ignore
-		$category = array_pop( $categories );
-		$this->assertEquals( 'email-contents', $category['name'] );
-		$this->assertEquals( 'Email Contents', $category['label'] );
-		$this->assertEquals( 'A collection of email content layouts.', $category['description'] );
+		$categories         = \WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered();
+		$categories_by_name = array_column( $categories, null, 'name' );
+
+		$this->assertArrayHasKey( 'email-contents', $categories_by_name );
+		$this->assertEquals( 'Email Contents', $categories_by_name['email-contents']['label'] );
+		$this->assertEquals( 'A collection of email content layouts.', $categories_by_name['email-contents']['description'] );
 	}

 	/**