Commit 1458129ec41 for woocommerce

commit 1458129ec41ab4e1d47eedd44cdb36ae6a7c9a4a
Author: Seun Olorunsola <30554163+triple0t@users.noreply.github.com>
Date:   Mon Mar 9 13:07:45 2026 +0100

    Add reset notification email content (#63558)

    * Email Editor: Add reset notification email content action

      Registers a "Reset content to default" entity action for woo_email posts
      in the email editor. When triggered, it fetches the original plugin-
      distributed block template content via a new REST endpoint and restores
      the post content to that default state.

      - Add GET /woocommerce-email-editor/v1/emails/{id}/default-content endpoint
      - Export registerEntityAction and PostWithPermissions from email editor package
      - Register reset action for woo_email post type from email-editor-integration

    * More route registration to the EmailApiController and remove support for WordPress 6.7 (Woo has discontinued it).

    * Fix lint errors

    * Fix lint error

    * Enhance EmailApiController with new posts generator and default content schema

    - Introduced WCTransactionalEmailPostsGenerator for managing email templates.
    - Added get_default_content_schema method to define the schema for the default content endpoint response.
    - Updated initialization logic to ensure both post manager and posts generator are set before generating email content.

diff --git a/packages/js/email-editor/changelog/wooprd-1019-export-register-entity-action b/packages/js/email-editor/changelog/wooprd-1019-export-register-entity-action
new file mode 100644
index 00000000000..32340cd1c34
--- /dev/null
+++ b/packages/js/email-editor/changelog/wooprd-1019-export-register-entity-action
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Export registerEntityAction, unregisterEntityAction and PostWithPermissions from the email editor package public API.
diff --git a/packages/js/email-editor/src/index.ts b/packages/js/email-editor/src/index.ts
index b0f6d3a3c76..2d3dc4f579e 100644
--- a/packages/js/email-editor/src/index.ts
+++ b/packages/js/email-editor/src/index.ts
@@ -167,8 +167,18 @@ export type {
 	EmailEditorSettings,
 	EmailTheme,
 	EmailEditorUrls,
+	PostWithPermissions,
 } from './store/types';

+/**
+ * The registerEntityAction and unregisterEntityAction are used to register and unregister entity actions.
+ * These use Gutenberg's private APIs and are highly unstable.
+ * DO NOT USE OUTSIDE WooCommerce.
+ *
+ * If necessary, import the unlock module and access the private APIs for your use case.
+ */
+export { registerEntityAction, unregisterEntityAction } from './private-apis';
+
 /**
  * A modal component for sending test emails from the email editor.
  *
diff --git a/plugins/woocommerce/changelog/wooprd-1019-add-reset-notification-email-content b/plugins/woocommerce/changelog/wooprd-1019-add-reset-notification-email-content
new file mode 100644
index 00000000000..a90907c4b8d
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooprd-1019-add-reset-notification-email-content
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add reset notification email content action to the email editor, allowing users to reset email content to the original plugin-distributed state.
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/index.ts b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/index.ts
index 1d40a4c9cf8..87f5aedd04d 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/index.ts
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/index.ts
@@ -3,9 +3,12 @@
 /**
  * External dependencies
  */
-import { addFilter } from '@wordpress/hooks';
+import { addFilter, addAction } from '@wordpress/hooks';
 import { __ } from '@wordpress/i18n';
-import { initializeEditor } from '@woocommerce/email-editor';
+import {
+	initializeEditor,
+	registerEntityAction,
+} from '@woocommerce/email-editor';

 /**
  * Internal dependencies
@@ -14,6 +17,7 @@ import { NAME_SPACE } from './constants';
 import { modifyTemplateSidebar } from './templates';
 import { modifySidebar } from './sidebar_settings';
 import { registerEmailValidationRules } from './email-validation';
+import getResetNotificationEmailContentAction from './reset-notification-email-content';

 import './style.scss';

@@ -68,4 +72,26 @@ addFilter( 'woocommerce_email_editor_create_coupon_handler', NAME_SPACE, () => {
 modifySidebar();
 modifyTemplateSidebar();
 registerEmailValidationRules();
+
+/**
+ * Register the reset notification email content entity action for the woo_email post type.
+ * This action allows users to reset the email content to the original state as distributed by the plugin.
+ */
+const registerResetNotificationEmailContentAction = ( postType: string ) => {
+	if ( postType !== 'woo_email' ) {
+		return;
+	}
+	registerEntityAction(
+		'postType',
+		postType,
+		getResetNotificationEmailContentAction()
+	);
+};
+
+addAction(
+	'core.registerPostTypeSchema',
+	`${ NAME_SPACE }/reset-notification-email-content`,
+	registerResetNotificationEmailContentAction
+);
+
 initializeEditor( 'woocommerce-email-editor' );
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/reset-notification-email-content.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/reset-notification-email-content.tsx
new file mode 100644
index 00000000000..9c96ce326a7
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/email-editor-integration/reset-notification-email-content.tsx
@@ -0,0 +1,183 @@
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+import { store as coreStore } from '@wordpress/core-data';
+import { backup } from '@wordpress/icons';
+import { useState } from '@wordpress/element';
+import { useDispatch } from '@wordpress/data';
+import {
+	Button,
+	__experimentalText as Text,
+	__experimentalHStack as HStack,
+	__experimentalVStack as VStack,
+} from '@wordpress/components';
+import { decodeEntities } from '@wordpress/html-entities';
+import { parse, serialize } from '@wordpress/blocks';
+import apiFetch from '@wordpress/api-fetch';
+
+// eslint-disable-next-line @woocommerce/dependency-group
+import type { PostWithPermissions } from '@woocommerce/email-editor';
+
+function getItemTitle( item: {
+	title: string | { rendered: string } | { raw: string };
+} ) {
+	if ( typeof item.title === 'string' ) {
+		return decodeEntities( item.title );
+	}
+	if ( item.title && 'rendered' in item.title ) {
+		return decodeEntities( item.title.rendered );
+	}
+	if ( item.title && 'raw' in item.title ) {
+		return decodeEntities( item.title.raw );
+	}
+	return '';
+}
+
+const getResetNotificationEmailContentAction = () => {
+	/**
+	 * Reset notification email content action.
+	 * Resets a woo_email post content to the original state as distributed by the plugin.
+	 */
+	const resetNotificationEmailContent = {
+		id: 'reset-notification-email-content',
+		label: __( 'Reset content to default', 'woocommerce' ),
+		supportsBulk: false,
+		icon: backup,
+		isEligible( item: PostWithPermissions ) {
+			if (
+				item.type === 'wp_template' ||
+				item.type === 'wp_template_part' ||
+				item.type === 'wp_block'
+			) {
+				return false;
+			}
+			const { permissions } = item;
+			return permissions?.update;
+		},
+		hideModalHeader: true,
+		modalFocusOnMount: 'firstContentElement',
+		RenderModal: ( {
+			items,
+			closeModal,
+			onActionPerformed,
+		}: {
+			items: PostWithPermissions[];
+			closeModal?: () => void;
+			onActionPerformed?: ( items: PostWithPermissions[] ) => void;
+		} ) => {
+			const [ isBusy, setIsBusy ] = useState( false );
+			const { createSuccessNotice, createErrorNotice } =
+				useDispatch( noticesStore );
+			const { editEntityRecord, saveEditedEntityRecord } =
+				useDispatch( coreStore );
+
+			const item = items[ 0 ];
+			const modalTitle = sprintf(
+				// translators: %s: The email's title
+				__(
+					'Are you sure you want to reset "%s" content to the default?',
+					'woocommerce'
+				),
+				getItemTitle( item )
+			);
+
+			return (
+				<VStack spacing="5">
+					<Text>{ modalTitle }</Text>
+					<HStack justify="right">
+						<Button
+							variant="tertiary"
+							onClick={ () => {
+								closeModal?.();
+							} }
+							disabled={ isBusy }
+							__next40pxDefaultSize
+						>
+							{ __( 'Cancel', 'woocommerce' ) }
+						</Button>
+						<Button
+							variant="primary"
+							onClick={ async () => {
+								setIsBusy( true );
+
+								try {
+									const response = ( await apiFetch( {
+										path: `/woocommerce-email-editor/v1/emails/${ item.id }/default-content`,
+									} ) ) as { content: string };
+
+									const blocks = parse(
+										response.content || ''
+									);
+
+									await editEntityRecord(
+										'postType',
+										item.type,
+										item.id,
+										{
+											blocks,
+											content: serialize( blocks ),
+										}
+									);
+
+									await saveEditedEntityRecord(
+										'postType',
+										item.type,
+										item.id,
+										{}
+									);
+
+									const successMessage = sprintf(
+										/* translators: The email's title. */
+										__(
+											'"%s" content reset to default.',
+											'woocommerce'
+										),
+										getItemTitle( item )
+									);
+
+									createSuccessNotice( successMessage, {
+										type: 'snackbar',
+										id: 'reset-notification-email-content-action',
+									} );
+
+									onActionPerformed?.( items );
+								} catch ( error ) {
+									let errorMessage = __(
+										'An error occurred while resetting the email content.',
+										'woocommerce'
+									);
+
+									if (
+										error &&
+										typeof error === 'object' &&
+										'message' in error
+									) {
+										errorMessage = String( error.message );
+									}
+
+									createErrorNotice( errorMessage, {
+										type: 'snackbar',
+									} );
+								} finally {
+									setIsBusy( false );
+									closeModal?.();
+								}
+							} }
+							isBusy={ isBusy }
+							disabled={ isBusy }
+							__next40pxDefaultSize
+						>
+							{ __( 'Reset to default', 'woocommerce' ) }
+						</Button>
+					</HStack>
+				</VStack>
+			);
+		},
+	};
+
+	return resetNotificationEmailContent;
+};
+
+export default getResetNotificationEmailContentAction;
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php b/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
index 2f1201e82b3..3e0e71957fa 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/EmailApiController.php
@@ -6,8 +6,11 @@ namespace Automattic\WooCommerce\Internal\EmailEditor;

 use Automattic\WooCommerce\EmailEditor\Validator\Builder;
 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsManager;
+use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsGenerator;
 use WC_Email;
 use WP_Error;
+use WP_REST_Request;
+use WP_REST_Response;

 defined( 'ABSPATH' ) || exit;

@@ -25,13 +28,21 @@ class EmailApiController {
 	 */
 	private ?WCTransactionalEmailPostsManager $post_manager;

+	/**
+	 * The WooCommerce transactional email posts generator.
+	 *
+	 * @var WCTransactionalEmailPostsGenerator|null
+	 */
+	private ?WCTransactionalEmailPostsGenerator $posts_generator = null;
+
 	/**
 	 * Initialize the controller.
 	 *
 	 * @internal
 	 */
 	final public function init(): void {
-		$this->post_manager = WCTransactionalEmailPostsManager::get_instance();
+		$this->post_manager    = WCTransactionalEmailPostsManager::get_instance();
+		$this->posts_generator = new WCTransactionalEmailPostsGenerator();
 	}

 	/**
@@ -247,4 +258,84 @@ class EmailApiController {
 		}
 		return null;
 	}
+
+	/**
+	 * Register REST API routes for the email API controller.
+	 */
+	public function register_routes(): void {
+		register_rest_route(
+			'woocommerce-email-editor/v1',
+			'/emails/(?P<id>\d+)/default-content',
+			array(
+				'methods'             => \WP_REST_Server::READABLE,
+				'callback'            => array( $this, 'get_default_content_response' ),
+				'permission_callback' => function () {
+					return current_user_can( 'manage_woocommerce' );
+				},
+				'args'                => array(
+					'id' => array(
+						'description'       => __( 'The ID of the woo_email post.', 'woocommerce' ),
+						'type'              => 'integer',
+						'required'          => true,
+						'sanitize_callback' => 'absint',
+					),
+				),
+				'schema'              => array( $this, 'get_default_content_schema' ),
+			)
+		);
+	}
+
+	/**
+	 * Get the schema for the default content endpoint response.
+	 *
+	 * @return array
+	 */
+	public function get_default_content_schema(): array {
+		return array(
+			'$schema'    => 'http://json-schema.org/draft-04/schema#',
+			'title'      => 'woo_email_default_content',
+			'type'       => 'object',
+			'properties' => array(
+				'content' => array(
+					'description' => __( 'The default block content for the email.', 'woocommerce' ),
+					'type'        => 'string',
+					'readonly'    => true,
+				),
+			),
+		);
+	}
+
+	/**
+	 * Return the default (plugin-distributed) block content for a woo_email post.
+	 *
+	 * @param WP_REST_Request $request The REST request.
+	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
+	 * @return WP_REST_Response|WP_Error
+	 */
+	public function get_default_content_response( WP_REST_Request $request ) {
+		if ( ! ( $this->post_manager && $this->posts_generator ) ) {
+			return new WP_Error(
+				'woocommerce_email_editor_not_initialized',
+				__( 'Email editor is not initialized.', 'woocommerce' ),
+				array( 'status' => 500 )
+			);
+		}
+
+		$post_id    = (int) $request->get_param( 'id' );
+		$email_type = $this->post_manager->get_email_type_from_post_id( $post_id );
+		$email      = $this->get_email_by_type( $email_type ?? '' );
+
+		if ( ! $email ) {
+			return new WP_Error(
+				'woocommerce_email_not_found',
+				__( 'No email found for the given post ID.', 'woocommerce' ),
+				array( 'status' => 404 )
+			);
+		}
+
+		return new WP_REST_Response(
+			array( 'content' => $this->posts_generator->get_email_template( $email ) ),
+			200
+		);
+	}
 }
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/Integration.php b/plugins/woocommerce/src/Internal/EmailEditor/Integration.php
index bb46aeddd5a..dcc7f8f73d2 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/Integration.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/Integration.php
@@ -140,6 +140,7 @@ class Integration {
 		add_action( 'woocommerce_email_editor_send_preview_email_before_wp_mail', array( $this, 'send_preview_email_before_wp_mail' ), 10 );
 		add_action( 'woocommerce_email_editor_send_preview_email_after_wp_mail', array( $this, 'send_preview_email_after_wp_mail' ), 10 );
 		add_filter( 'woocommerce_email_editor_send_preview_email_subject', array( $this, 'update_email_subject_for_send_preview_email' ), 10, 2 );
+		add_action( 'rest_api_init', array( $this->email_api_controller, 'register_routes' ) );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
index f4193373801..b410a1967e0 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/EmailApiControllerTest.php
@@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Tests\Internal\EmailEditor;

 use Automattic\WooCommerce\Internal\EmailEditor\EmailApiController;
 use Automattic\WooCommerce\Internal\EmailEditor\Integration;
+use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsGenerator;
 use Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails\WCTransactionalEmailPostsManager;

 require_once 'EmailStub.php';
@@ -280,4 +281,61 @@ class EmailApiControllerTest extends \WC_Unit_Test_Case {
 		$this->assertEquals( $this->email_type, $result['email_type'] );
 		$this->assertEquals( 'Default Subject', $result['default_subject'] );
 	}
+
+	/**
+	 * @testdox Should return 404 when post ID has no associated email type.
+	 */
+	public function test_get_default_content_response_returns_404_for_unknown_post(): void {
+		$unassociated_post = $this->factory()->post->create_and_get(
+			array(
+				'post_title'  => 'Unknown Email',
+				'post_name'   => 'unknown_email',
+				'post_type'   => Integration::EMAIL_POST_TYPE,
+				'post_status' => 'draft',
+			)
+		);
+
+		$request = new \WP_REST_Request( 'GET', '/woocommerce-email-editor/v1/emails/' . $unassociated_post->ID . '/default-content' );
+		$request->set_param( 'id', $unassociated_post->ID );
+
+		$result = $this->email_api_controller->get_default_content_response( $request );
+
+		$this->assertInstanceOf( \WP_Error::class, $result );
+		$this->assertSame( 'woocommerce_email_not_found', $result->get_error_code() );
+		$this->assertSame( 404, $result->get_error_data()['status'] );
+	}
+
+	/**
+	 * @testdox Should return default content for a valid email post.
+	 */
+	public function test_get_default_content_response_returns_content_for_valid_post(): void {
+		$mock_email     = $this->createMock( \WC_Email::class );
+		$mock_email->id = $this->email_type;
+
+		$mock_generator = $this->createMock( WCTransactionalEmailPostsGenerator::class );
+		$mock_generator->method( 'get_email_template' )
+			->willReturn( '<!-- wp:paragraph --><p>Default content</p><!-- /wp:paragraph -->' );
+
+		$controller = $this->getMockBuilder( EmailApiController::class )
+			->onlyMethods( array( 'get_emails' ) )
+			->getMock();
+		$controller->method( 'get_emails' )
+			->willReturn( array( $mock_email ) );
+		$controller->init();
+
+		$reflection = new \ReflectionClass( EmailApiController::class );
+		$property   = $reflection->getProperty( 'posts_generator' );
+		$property->setAccessible( true );
+		$property->setValue( $controller, $mock_generator );
+
+		$request = new \WP_REST_Request( 'GET', '/woocommerce-email-editor/v1/emails/' . $this->email_post->ID . '/default-content' );
+		$request->set_param( 'id', $this->email_post->ID );
+
+		$result = $controller->get_default_content_response( $request );
+
+		$this->assertInstanceOf( \WP_REST_Response::class, $result );
+		$this->assertSame( 200, $result->get_status() );
+		$this->assertArrayHasKey( 'content', $result->get_data() );
+		$this->assertSame( '<!-- wp:paragraph --><p>Default content</p><!-- /wp:paragraph -->', $result->get_data()['content'] );
+	}
 }