Commit 51878a39594 for woocommerce
commit 51878a39594a97512a0c0eca09ef2b0ab7042a9b
Author: Jan Lysý <lysyjan@users.noreply.github.com>
Date: Fri Oct 23 20:20:07 2026 +0200
[Email Editor] Restore email post actions on WordPress 7.2 (#70177)
* Restore email post actions in the email editor on WordPress 7.2
WordPress 7.2 builds post actions on the server, so the email editor's
"Permanently delete" and WooCommerce's "Reset" actions no longer showed.
Register them through the entity actions API, with a script module that
reads their JavaScript parts from the woocommerce_email_editor_entity_actions
filter. Email post types opt in to replacing core's "Trash…" with the
woocommerce_email_editor_replace_trash_action filter, which WooCommerce
does for its emails. The client-side registration stays for WordPress 7.1
and earlier.
* Add changelog entries for email post actions on WordPress 7.2
* Read entity action modules in both registry shapes in tests
diff --git a/packages/js/email-editor/README.md b/packages/js/email-editor/README.md
index cb98caa0c30..c30b1bc0c6f 100644
--- a/packages/js/email-editor/README.md
+++ b/packages/js/email-editor/README.md
@@ -358,6 +358,7 @@ We may add, update and delete any of them.
| `woocommerce_email_editor_sidebar_email_type_info_icon` | none | `JSX.Element` icon component | Return an icon from @wordpress/icons e.g. () => <Icon icon={ postContent } /> |
| `woocommerce_email_editor_sidebar_email_type_info_content` | none | `JSX.Element` info content | Return a React component containing information about the current template or content |
| `woocommerce_email_editor_trash_modal_should_permanently_delete` | `boolean` (false-default) | `boolean` | Controls the action of the trash modal. Returning `true` will ensure the modal permanently deletes the email (skipping trash). |
+| `woocommerce_email_editor_entity_actions` | `Record< string, object >` actions (`{}` default) | `Record< string, object >` actions | WordPress 7.2+: JS parts (`RenderModal`, `isEligible`, …) of entity actions registered in PHP, keyed by action id |
| `woocommerce_email_editor_iframe_stylesheet_should_remove` | `boolean` (false-default), `CSSStyleSheet` stylesheet | `boolean` | Controls whether the iframe stylesheet should be removed. Returning `true` will remove the iframe stylesheet. |
| `woocommerce_email_editor_close_action_callback` | `function` backAction | `function` backAction | Action to perform when the close (back) button is clicked |
| `woocommerce_email_editor_close_content` | `React.ComponentType` DefaultBackButtonContent | `React.ComponentType` Back button content | Custom component for the back button content in the editor header |
diff --git a/packages/js/email-editor/changelog/fix-wooplug-8069-email-post-actions-wp-72 b/packages/js/email-editor/changelog/fix-wooplug-8069-email-post-actions-wp-72
new file mode 100644
index 00000000000..8d95c10f0f2
--- /dev/null
+++ b/packages/js/email-editor/changelog/fix-wooplug-8069-email-post-actions-wp-72
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Show the email editor's delete action instead of core's "Trash…" on WordPress 7.2 for email post types that opt in.
diff --git a/packages/js/email-editor/src/components/block-editor/editor.tsx b/packages/js/email-editor/src/components/block-editor/editor.tsx
index 17e52fdcb9a..ca14f4954a1 100644
--- a/packages/js/email-editor/src/components/block-editor/editor.tsx
+++ b/packages/js/email-editor/src/components/block-editor/editor.tsx
@@ -120,7 +120,8 @@ export function InnerEditor( {
[]
);
- // From WordPress 7.2 emails get core's delete actions, which leave it to the editor
+ // From WordPress 7.2 emails whose post type doesn't opt in to the email editor's delete action
+ // (`woocommerce_email_editor_replace_trash_action`) get core's, which leave it to the editor
// screen to move away from the deleted post. Go back to the email list.
const onActionPerformed = useCallback(
( actionId: string ) => {
diff --git a/packages/js/email-editor/src/editor.tsx b/packages/js/email-editor/src/editor.tsx
index 251e4b3cbb2..73d0f491f94 100644
--- a/packages/js/email-editor/src/editor.tsx
+++ b/packages/js/email-editor/src/editor.tsx
@@ -29,6 +29,7 @@ import {
} from './events';
import { initContentValidationMiddleware } from './middleware/content-validation';
import { initHacks } from './hacks';
+import { initEntityActions } from './entity-actions';
import {
useContentValidation,
useFilterEditorContentStylesheets,
@@ -138,6 +139,7 @@ function onInit() {
initContentValidationMiddleware();
initBlocks();
initHacks();
+ initEntityActions();
initTextHooks();
initializeLayout();
}
diff --git a/packages/js/email-editor/src/entity-actions.ts b/packages/js/email-editor/src/entity-actions.ts
new file mode 100644
index 00000000000..2f8e398f877
--- /dev/null
+++ b/packages/js/email-editor/src/entity-actions.ts
@@ -0,0 +1,25 @@
+/**
+ * External dependencies
+ */
+import { addFilter } from '@wordpress/hooks';
+
+/**
+ * Internal dependencies
+ */
+import getTrashEmailPostAction from './components/header/trash-email-post';
+
+/**
+ * From WordPress 7.2 post actions are registered on the server. The email editor's
+ * script module (`@woocommerce/email-editor/entity-actions`) reads the JavaScript
+ * parts of its actions from this filter when the editor loads them.
+ */
+export function initEntityActions() {
+ addFilter(
+ 'woocommerce_email_editor_entity_actions',
+ 'woocommerce-email-editor/trash-email-post',
+ ( actions: Record< string, unknown > ) => ( {
+ ...actions,
+ 'trash-email-post': getTrashEmailPostAction(),
+ } )
+ );
+}
diff --git a/packages/js/email-editor/src/hacks/README.md b/packages/js/email-editor/src/hacks/README.md
index f17c986bddf..4648e0790cb 100644
--- a/packages/js/email-editor/src/hacks/README.md
+++ b/packages/js/email-editor/src/hacks/README.md
@@ -48,7 +48,7 @@ Each hack below represents functionality that should ideally be supported throug
**Upstream solution needed:** A filter or configuration option to customize built-in entity actions without needing to unregister and re-register them.
-**WordPress 7.2+:** Entity actions are registered on the server (`wp_entity_actions_api_init`) and `core.registerPostTypeSchema` no longer fires in the email editor, so this hack has no effect. Core's trash action is shown instead; the editor sends the user back to the email list after it (`onActionPerformed` in [editor.tsx](../components/block-editor/editor.tsx)).
+**WordPress 7.2+:** Entity actions are registered on the server (`wp_entity_actions_api_init`) and `core.registerPostTypeSchema` no longer fires in the email editor, so this hack has no effect. `Email_Editor::register_entity_actions()` in the PHP package replaces core's action instead for post types that opt in with the `woocommerce_email_editor_replace_trash_action` PHP filter (other post types keep core's "Trash…"), and the `@woocommerce/email-editor/entity-actions` script module reads its JavaScript parts from the `woocommerce_email_editor_entity_actions` filter ([entity-actions.ts](../entity-actions.ts)). This hack can be removed once WordPress 7.2 is the minimum.
---
diff --git a/packages/js/email-editor/src/test/entity-actions.spec.ts b/packages/js/email-editor/src/test/entity-actions.spec.ts
new file mode 100644
index 00000000000..3e177eb3025
--- /dev/null
+++ b/packages/js/email-editor/src/test/entity-actions.spec.ts
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { applyFilters, removeAllFilters } from '@wordpress/hooks';
+
+/**
+ * Internal dependencies
+ */
+import { initEntityActions } from '../entity-actions';
+import getTrashEmailPostAction from '../components/header/trash-email-post';
+
+jest.mock( '../components/header/trash-email-post', () => ( {
+ __esModule: true,
+ default: jest.fn( () => ( {
+ id: 'trash-email-post',
+ label: 'Permanently delete',
+ RenderModal: () => null,
+ } ) ),
+} ) );
+
+const getTrashEmailPostActionMock = getTrashEmailPostAction as jest.Mock;
+
+describe( 'initEntityActions', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ removeAllFilters( 'woocommerce_email_editor_entity_actions' );
+ } );
+
+ it( 'adds the trash email post action to the entity actions filter', () => {
+ initEntityActions();
+
+ const actions = applyFilters(
+ 'woocommerce_email_editor_entity_actions',
+ { 'other-action': { label: 'Other' } }
+ ) as Record< string, { label: string; RenderModal?: unknown } >;
+
+ expect( actions[ 'other-action' ] ).toEqual( { label: 'Other' } );
+ expect( actions[ 'trash-email-post' ].label ).toBe(
+ 'Permanently delete'
+ );
+ expect( actions[ 'trash-email-post' ].RenderModal ).toBeDefined();
+ } );
+
+ it( 'builds the action only when the filter is applied', () => {
+ initEntityActions();
+
+ expect( getTrashEmailPostActionMock ).not.toHaveBeenCalled();
+
+ applyFilters( 'woocommerce_email_editor_entity_actions', {} );
+
+ expect( getTrashEmailPostActionMock ).toHaveBeenCalledTimes( 1 );
+ } );
+} );
diff --git a/packages/php/email-editor/changelog/fix-wooplug-8069-email-post-actions-wp-72 b/packages/php/email-editor/changelog/fix-wooplug-8069-email-post-actions-wp-72
new file mode 100644
index 00000000000..a946205e370
--- /dev/null
+++ b/packages/php/email-editor/changelog/fix-wooplug-8069-email-post-actions-wp-72
@@ -0,0 +1,4 @@
+Significance: minor
+Type: fix
+
+Show the email editor's delete action instead of core's "Trash…" on WordPress 7.2 for email post types that opt in with the woocommerce_email_editor_replace_trash_action filter.
diff --git a/packages/php/email-editor/src/Engine/class-email-editor.php b/packages/php/email-editor/src/Engine/class-email-editor.php
index b455ac28e0e..6d1545e92b5 100644
--- a/packages/php/email-editor/src/Engine/class-email-editor.php
+++ b/packages/php/email-editor/src/Engine/class-email-editor.php
@@ -25,6 +25,13 @@ use WP_Theme_JSON;
class Email_Editor {
public const WOOCOMMERCE_EMAIL_META_THEME_TYPE = 'woocommerce_email_theme';
+ /**
+ * Script module providing the JavaScript parts of the email editor's entity actions (WordPress 7.2+).
+ * Integrations can pass it to wp_register_entity_actions() and add their parts with the
+ * `woocommerce_email_editor_entity_actions` JavaScript filter.
+ */
+ public const ENTITY_ACTIONS_SCRIPT_MODULE_ID = '@woocommerce/email-editor/entity-actions';
+
/**
* Property for the email API controller.
*
@@ -112,6 +119,7 @@ class Email_Editor {
$this->register_block_templates();
$this->register_email_post_sent_status();
$this->register_personalization_tags();
+ $this->register_entity_actions_script_module();
$is_editor_page = apply_filters( 'woocommerce_is_email_editor_page', false );
if ( $is_editor_page ) {
$this->extend_email_post_api();
@@ -166,6 +174,70 @@ class Email_Editor {
}
}
+ /**
+ * From WordPress 7.2 the editor reads post actions from a server-side registry,
+ * so register the script module of the email editor's actions and the actions themselves there.
+ *
+ * @return void
+ */
+ private function register_entity_actions_script_module(): void {
+ if ( ! function_exists( 'wp_register_entity_actions' ) ) {
+ return;
+ }
+ $version = filemtime( __DIR__ . '/entity-actions.js' );
+ wp_register_script_module(
+ self::ENTITY_ACTIONS_SCRIPT_MODULE_ID,
+ plugins_url( 'entity-actions.js', __FILE__ ),
+ array(),
+ false === $version ? false : (string) $version
+ );
+ add_action( 'wp_entity_actions_api_init', array( $this, 'register_entity_actions' ) );
+ }
+
+ /**
+ * Replace core's "Trash" action of the email post types that opt in with the email editor's own,
+ * which can permanently delete the email (see the `woocommerce_email_editor_trash_modal_should_permanently_delete` JS filter).
+ * The label is replaced by the script module.
+ *
+ * @return void
+ */
+ public function register_entity_actions(): void {
+ if ( ! function_exists( 'wp_register_entity_actions' ) || ! function_exists( 'wp_unregister_entity_actions' ) ) {
+ return;
+ }
+ foreach ( self::get_post_types() as $post_type ) {
+ /**
+ * Filters whether the email editor's trash action replaces core's "Trash…" for an email post type on WordPress 7.2+.
+ * Only opt in when the editor's JavaScript comes from a version of the email editor package that provides the action,
+ * otherwise the post type is left without a trash action.
+ *
+ * @param bool $replace Whether to replace core's action. Default false.
+ * @param string $post_type The email post type.
+ *
+ * @since 2.20.0
+ */
+ if ( true !== apply_filters( 'woocommerce_email_editor_replace_trash_action', false, $post_type['name'] ) ) {
+ continue;
+ }
+ wp_unregister_entity_actions( 'postType', $post_type['name'], array( 'move-to-trash' ) );
+ wp_register_entity_actions(
+ 'woocommerce',
+ 'postType',
+ $post_type['name'],
+ array(
+ array(
+ 'id' => 'trash-email-post',
+ 'label' => __( 'Move to trash', 'woocommerce' ),
+ 'supportsBulk' => true,
+ 'hideModalHeader' => true,
+ 'modalFocusOnMount' => 'firstContentElement',
+ ),
+ ),
+ self::ENTITY_ACTIONS_SCRIPT_MODULE_ID
+ );
+ }
+ }
+
/**
* Register all personalization tags registered via
* the woocommerce_email_editor_register_personalization_tags filter.
diff --git a/packages/php/email-editor/src/Engine/entity-actions.js b/packages/php/email-editor/src/Engine/entity-actions.js
new file mode 100644
index 00000000000..7746c1fda54
--- /dev/null
+++ b/packages/php/email-editor/src/Engine/entity-actions.js
@@ -0,0 +1,19 @@
+/**
+ * JavaScript parts of the email editor's entity actions, keyed by action id (WordPress 7.2+).
+ * The editor imports this module when it loads the actions, after the email editor scripts
+ * have added their parts with the `woocommerce_email_editor_entity_actions` filter.
+ *
+ * @package Automattic\WooCommerce\EmailEditor
+ */
+
+const hooks = window.wp && window.wp.hooks;
+
+const actions = hooks
+ ? hooks.applyFilters( 'woocommerce_email_editor_entity_actions', {} )
+ : {};
+
+export default actions &&
+typeof actions === 'object' &&
+! Array.isArray( actions )
+ ? actions
+ : {};
diff --git a/packages/php/email-editor/tests/integration/Engine/Email_Editor_Test.php b/packages/php/email-editor/tests/integration/Engine/Email_Editor_Test.php
index c161b2f3211..01ab89467e9 100644
--- a/packages/php/email-editor/tests/integration/Engine/Email_Editor_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Email_Editor_Test.php
@@ -136,6 +136,118 @@ class Email_Editor_Test extends \Email_Editor_Integration_Test_Case {
$this->assertSame( $link, apply_filters( 'preview_post_link', $link, null ) );
}
+ /**
+ * On WordPress 7.2+ an email post type that opts in gets the email editor's trash action instead of core's.
+ */
+ public function testItReplacesCoreMoveToTrashActionForEmailPostTypesThatOptIn(): void {
+ if ( ! function_exists( 'wp_get_registered_entity_actions' ) ) {
+ $this->markTestSkipped( 'The entity actions API requires WordPress 7.2.' );
+ }
+ $this->reset_entity_actions_registry();
+ add_filter(
+ 'woocommerce_email_editor_replace_trash_action',
+ function ( $replace, $post_type ) {
+ return 'custom_email_type' === $post_type ? true : $replace;
+ },
+ 10,
+ 2
+ );
+
+ $ids = array_column( wp_get_registered_entity_actions( 'postType', 'custom_email_type' ), 'id' );
+
+ $this->assertContains( 'trash-email-post', $ids );
+ $this->assertNotContains( 'move-to-trash', $ids );
+ $this->assertSame( array( 'trash-email-post' ), $this->get_script_module_action_ids( 'custom_email_type' ) );
+ $this->assertStringContainsString( Email_Editor::ENTITY_ACTIONS_SCRIPT_MODULE_ID, $this->get_import_map() );
+ }
+
+ /**
+ * Email post types keep core's trash action unless they opt in, since their editor's JavaScript may not provide the replacement.
+ */
+ public function testItKeepsCoreMoveToTrashActionForEmailPostTypesThatDoNotOptIn(): void {
+ if ( ! function_exists( 'wp_get_registered_entity_actions' ) ) {
+ $this->markTestSkipped( 'The entity actions API requires WordPress 7.2.' );
+ }
+ $this->reset_entity_actions_registry();
+
+ $ids = array_column( wp_get_registered_entity_actions( 'postType', 'custom_email_type' ), 'id' );
+
+ $this->assertContains( 'move-to-trash', $ids );
+ $this->assertNotContains( 'trash-email-post', $ids );
+ }
+
+ /**
+ * Post types the email editor does not handle keep core's trash action.
+ */
+ public function testItKeepsCoreMoveToTrashActionForOtherPostTypes(): void {
+ if ( ! function_exists( 'wp_get_registered_entity_actions' ) ) {
+ $this->markTestSkipped( 'The entity actions API requires WordPress 7.2.' );
+ }
+ $this->reset_entity_actions_registry();
+
+ $ids = array_column( wp_get_registered_entity_actions( 'postType', 'post' ), 'id' );
+
+ $this->assertContains( 'move-to-trash', $ids );
+ $this->assertNotContains( 'trash-email-post', $ids );
+ }
+
+ /**
+ * Returns the ids of the actions the email editor's script module provides for a post type.
+ * WordPress 7.2 Beta 1 keys the modules by id, later builds list them as `id` and `actions` entries.
+ *
+ * @param string $post_type The post type.
+ * @return array The action ids.
+ */
+ private function get_script_module_action_ids( string $post_type ): array {
+ if ( ! function_exists( 'wp_get_registered_entity_action_modules' ) ) {
+ return array();
+ }
+ foreach ( wp_get_registered_entity_action_modules( 'postType', $post_type ) as $key => $module ) {
+ if ( isset( $module['id'], $module['actions'] ) ) {
+ if ( Email_Editor::ENTITY_ACTIONS_SCRIPT_MODULE_ID === $module['id'] ) {
+ return $module['actions'];
+ }
+ } elseif ( Email_Editor::ENTITY_ACTIONS_SCRIPT_MODULE_ID === (string) $key ) {
+ return $module;
+ }
+ }
+ return array();
+ }
+
+ /**
+ * Returns the import map the editor page would print with the email editor's script module as a dependency.
+ */
+ private function get_import_map(): string {
+ wp_register_script_module(
+ 'email-editor-test',
+ 'https://example.com/test.js',
+ array(
+ array(
+ 'id' => Email_Editor::ENTITY_ACTIONS_SCRIPT_MODULE_ID,
+ 'import' => 'dynamic',
+ ),
+ )
+ );
+ wp_enqueue_script_module( 'email-editor-test' );
+ $import_map = get_echo( array( wp_script_modules(), 'print_import_map' ) );
+ wp_dequeue_script_module( 'email-editor-test' );
+ wp_deregister_script_module( 'email-editor-test' );
+ return $import_map;
+ }
+
+ /**
+ * Drops the entity actions registry, which fills once per request,
+ * so the next read fires `wp_entity_actions_api_init` again.
+ */
+ private function reset_entity_actions_registry(): void {
+ if ( ! class_exists( \WP_Entity_Actions_Registry::class ) ) {
+ return;
+ }
+ $instance = new \ReflectionProperty( \WP_Entity_Actions_Registry::class, 'instance' );
+ $instance->setAccessible( true );
+ $instance->setValue( null, null );
+ }
+
/**
* Create an email post of the registered custom type.
*
@@ -158,6 +270,7 @@ class Email_Editor_Test extends \Email_Editor_Integration_Test_Case {
*/
public function tearDown(): void {
unset( $_GET['post'] );
+ $this->reset_entity_actions_registry();
parent::tearDown();
remove_filter( 'woocommerce_email_editor_post_types', $this->post_register_callback );
}
diff --git a/plugins/woocommerce/changelog/fix-wooplug-8069-email-post-actions-wp-72 b/plugins/woocommerce/changelog/fix-wooplug-8069-email-post-actions-wp-72
new file mode 100644
index 00000000000..297f5ad2472
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-wooplug-8069-email-post-actions-wp-72
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restore the "Permanently delete" and "Reset" actions in the block email editor on WordPress 7.2.
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 4dff1538957..ecd49c0f598 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
@@ -124,9 +124,22 @@ if (
}
/**
- * Register the reset notification email content entity action for the woo_email post type.
+ * 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.
+ * From WordPress 7.2 the action is registered on the server (Integration::register_entity_actions())
+ * and the email editor's script module reads its JavaScript parts from this filter.
*/
+addFilter(
+ 'woocommerce_email_editor_entity_actions',
+ `${ NAME_SPACE }/reset-notification-email-content`,
+ ( actions: Record< string, unknown > ) => ( {
+ ...actions,
+ 'reset-notification-email-content':
+ getResetNotificationEmailContentAction(),
+ } )
+);
+
+// Up to WordPress 7.1 the action is registered on the client.
const registerResetNotificationEmailContentAction = ( postType: string ) => {
if ( postType !== 'woo_email' ) {
return;
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/Integration.php b/plugins/woocommerce/src/Internal/EmailEditor/Integration.php
index b61584792eb..73f601d1647 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/Integration.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/Integration.php
@@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Internal\EmailEditor;
use Automattic\WooCommerce\EmailEditor\Email_Editor_Container;
use Automattic\WooCommerce\EmailEditor\Engine\Dependency_Check;
+use Automattic\WooCommerce\EmailEditor\Engine\Email_Editor;
use Automattic\WooCommerce\EmailEditor\Engine\Renderer\Renderer;
use Automattic\WooCommerce\EmailEditor\Engine\Send_Preview_Email;
use Automattic\WooCommerce\Internal\Admin\EmailPreview\EmailPreview;
@@ -171,6 +172,8 @@ class Integration {
add_action( 'admin_init', array( $this, 'render_block_email_preview_page' ) );
add_action( 'rest_api_init', array( $this->email_api_controller, 'register_routes' ) );
add_action( 'transition_post_status', array( $this, 'save_email_mapping_on_publish' ), 10, 3 );
+ add_action( 'wp_entity_actions_api_init', array( $this, 'register_entity_actions' ) );
+ add_filter( 'woocommerce_email_editor_replace_trash_action', array( $this, 'replace_trash_action' ), 10, 2 );
// Priority 11 ensures the email editor's `init` bootstrap (default priority 10)
// has registered the `woo_email` post type before we register meta against it.
add_action( 'init', array( WCEmailTemplateDivergenceDetector::class, 'register_meta' ), 11 );
@@ -189,6 +192,46 @@ class Integration {
add_action( WCEmailTemplateSyncBackfill::BACKFILL_COMPLETE_ACTION, array( WCEmailTemplateSyncTracker::class, 'on_backfill_complete' ), 20 );
}
+ /**
+ * Register the "Reset" email content action on WordPress 7.2+, where post actions come from a server-side registry.
+ * Its JavaScript parts come from the email editor script module, see email-editor-integration/index.ts.
+ *
+ * @internal
+ */
+ public function register_entity_actions(): void {
+ if ( ! function_exists( 'wp_register_entity_actions' ) ) {
+ return;
+ }
+ wp_register_entity_actions(
+ 'woocommerce',
+ 'postType',
+ self::EMAIL_POST_TYPE,
+ array(
+ array(
+ 'id' => 'reset-notification-email-content',
+ 'label' => __( 'Reset', 'woocommerce' ),
+ 'supportsBulk' => false,
+ 'hideModalHeader' => true,
+ 'modalFocusOnMount' => 'firstContentElement',
+ ),
+ ),
+ Email_Editor::ENTITY_ACTIONS_SCRIPT_MODULE_ID
+ );
+ }
+
+ /**
+ * Use the email editor's delete action instead of core's "Trash…" for WooCommerce emails on WordPress 7.2+.
+ *
+ * @internal
+ *
+ * @param bool|mixed $replace Whether to replace core's action.
+ * @param string $post_type The email post type.
+ * @return bool|mixed
+ */
+ public function replace_trash_action( $replace, $post_type ) {
+ return self::EMAIL_POST_TYPE === $post_type ? true : $replace;
+ }
+
/**
* Add WooCommerce email post type to the list of supported post types.
*
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailPostsCleanup.php b/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailPostsCleanup.php
index 94635e352d4..b003a447183 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailPostsCleanup.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCEmailPostsCleanup.php
@@ -82,10 +82,11 @@ class WCEmailPostsCleanup {
}
// A trashed copy no longer affects rendering; treat it as "revert to
- // default". The email editor UI never trashes woo_email posts (its
- // delete action permanently deletes via the
+ // default". The email editor's delete action permanently deletes
+ // woo_email posts (via the
// `woocommerce_email_editor_trash_modal_should_permanently_delete`
- // filter), so a trashed post slipped past that guard out-of-band.
+ // filter), so a trashed post was trashed elsewhere, e.g. by core's
+ // "Trash…" action that earlier versions showed on WordPress 7.2.
if ( 'trash' === $post->post_status ) {
if ( wp_delete_post( $post->ID, true ) ) {
delete_option( $option_name );
diff --git a/plugins/woocommerce/tests/e2e/tests/email-editor/email-editor-delete.spec.ts b/plugins/woocommerce/tests/e2e/tests/email-editor/email-editor-delete.spec.ts
index d2eada13a44..f5865aaddc7 100644
--- a/plugins/woocommerce/tests/e2e/tests/email-editor/email-editor-delete.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/email-editor/email-editor-delete.spec.ts
@@ -31,7 +31,7 @@ test.describe(
test.afterAll( async ( { baseURL } ) => {
try {
if ( emailPostId ) {
- // Up to WP 7.1 the test already deleted the post permanently.
+ // The test already deleted the post when it passed.
await deleteEmailPost( baseURL, emailPostId ).catch(
( error ) => {
if ( error.response?.status !== 404 ) {
@@ -65,14 +65,20 @@ test.describe(
.getByRole( 'button', { name: 'Actions' } )
.first()
.click();
- // Up to WP 7.1 the email editor's own action permanently deletes the
- // email. From WP 7.2 core's "Trash…" action moves it to the trash.
+ // WooCommerce replaces core's "Trash…" with an action that deletes
+ // the email permanently, and adds "Reset" for the email content.
+ await expect(
+ page.getByRole( 'menuitem', { name: 'Reset' } )
+ ).toBeVisible();
+ await expect(
+ page.getByRole( 'menuitem', { name: /trash/i } )
+ ).toHaveCount( 0 );
await page
- .getByRole( 'menuitem', { name: /permanently delete|trash/i } )
+ .getByRole( 'menuitem', { name: 'Permanently delete' } )
.click();
await page
.getByRole( 'dialog' )
- .getByRole( 'button', { name: /delete permanently|trash/i } )
+ .getByRole( 'button', { name: 'Delete permanently' } )
.click();
await expect( page ).toHaveURL( /page=wc-settings&tab=email/ );
@@ -88,7 +94,7 @@ test.describe(
)
.then( ( response ) => response.data.status )
.catch( ( error ) => error.response?.status );
- expect( [ 'trash', 404 ] ).toContain( postState );
+ expect( postState ).toBe( 404 );
} );
}
);
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/IntegrationTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/IntegrationTest.php
index 70449cfbca6..157cb624e73 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/IntegrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/IntegrationTest.php
@@ -637,6 +637,63 @@ class IntegrationTest extends WC_Unit_Test_Case {
$this->assertFalse( $integration->replace_editor( false, $ordinary_post ), 'Posts of other types must keep the default editor after the guard trips' );
}
+ /**
+ * @testdox On WordPress 7.2+ WooCommerce emails get the reset content and permanent delete actions from the email editor script module.
+ */
+ public function test_registers_email_entity_actions_for_woo_email(): void {
+ if ( ! function_exists( 'wp_register_entity_actions' ) ) {
+ $this->markTestSkipped( 'The entity actions API requires WordPress 7.2.' );
+ }
+ $this->skip_if_unsupported_environment();
+ $this->bootstrap_email_editor();
+
+ $registry = new \ReflectionProperty( \WP_Entity_Actions_Registry::class, 'instance' );
+ $registry->setAccessible( true );
+ $registry->setValue( null, null );
+
+ try {
+ $email_ids = array_column( wp_get_registered_entity_actions( 'postType', Integration::EMAIL_POST_TYPE ), 'id' );
+ $module_ids = $this->get_script_module_action_ids( Integration::EMAIL_POST_TYPE );
+ $post_ids = array_column( wp_get_registered_entity_actions( 'postType', 'post' ), 'id' );
+ } finally {
+ $registry->setValue( null, null );
+ }
+
+ $this->assertContains( 'reset-notification-email-content', $email_ids, 'WooCommerce emails must have the reset action' );
+ $this->assertContains( 'trash-email-post', $email_ids, 'WooCommerce emails must have the email editor\'s delete action' );
+ $this->assertNotContains( 'move-to-trash', $email_ids, 'Core\'s trash action must be replaced for WooCommerce emails' );
+ $this->assertEqualsCanonicalizing(
+ array( 'trash-email-post', 'reset-notification-email-content' ),
+ $module_ids,
+ 'Both actions must take their JavaScript parts from the email editor script module'
+ );
+ $this->assertNotContains( 'reset-notification-email-content', $post_ids, 'Other post types must not get the reset action' );
+ }
+
+ /**
+ * Returns the ids of the actions the email editor's script module provides for a post type.
+ * WordPress 7.2 Beta 1 keys the modules by id, later builds list them as `id` and `actions` entries.
+ *
+ * @param string $post_type The post type.
+ * @return array The action ids.
+ */
+ private function get_script_module_action_ids( string $post_type ): array {
+ if ( ! function_exists( 'wp_get_registered_entity_action_modules' ) ) {
+ return array();
+ }
+ $module_id = \Automattic\WooCommerce\EmailEditor\Engine\Email_Editor::ENTITY_ACTIONS_SCRIPT_MODULE_ID;
+ foreach ( wp_get_registered_entity_action_modules( 'postType', $post_type ) as $key => $module ) {
+ if ( isset( $module['id'], $module['actions'] ) ) {
+ if ( $module_id === $module['id'] ) {
+ return $module['actions'];
+ }
+ } elseif ( $module_id === (string) $key ) {
+ return $module;
+ }
+ }
+ return array();
+ }
+
/**
* @testdox Initializing the integration does not build the WC_Emails mailer, so emails registered later on init are still listed.
*/