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'] );
}
/**