Commit e190dfa3bc for woocommerce
commit e190dfa3bc2851493ce4dec7cc3eedf06ce8b96f
Author: Jan Lysý <lysyjan@users.noreply.github.com>
Date: Wed Feb 4 16:30:58 2026 +0100
Add post_id context support for personalization tags API (#63103)
* Add post_id context support for personalization tags API
- Add post_id parameter to personalization tags REST endpoint
- Fire woocommerce_email_editor_personalization_tags_for_post action - Include post_id in selector query for context-aware filtering
- Add invalidatePersonalizationTagsCache action for cache control
* Add changelog entries for personalization tags API changes
* Remove outdated @since tag from email personalization tags action
* Document woocommerce_email_editor_personalization_tags_for_post action in README
* Add integration tests for Email_Api_Controller
diff --git a/packages/js/email-editor/changelog/add-post-id-context-personalization-tags b/packages/js/email-editor/changelog/add-post-id-context-personalization-tags
new file mode 100644
index 0000000000..0e0a8beadc
--- /dev/null
+++ b/packages/js/email-editor/changelog/add-post-id-context-personalization-tags
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add post_id context support and cache invalidation for personalization tags
diff --git a/packages/js/email-editor/src/store/actions.ts b/packages/js/email-editor/src/store/actions.ts
index e6fb68691d..a83c530d1c 100644
--- a/packages/js/email-editor/src/store/actions.ts
+++ b/packages/js/email-editor/src/store/actions.ts
@@ -8,7 +8,7 @@ import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
-import { storeName } from './constants';
+import { storeName, PERSONALIZATION_TAG_ENTITY } from './constants';
import {
SendingPreviewStatus,
State,
@@ -33,18 +33,47 @@ export function updateSendPreviewEmail( toEmail: string ) {
} as const;
}
-export function setEmailPost( postId: number | string, postType: string ) {
- if ( ! postId || ! postType ) {
- throw new Error(
- 'setEmailPost requires valid postId and postType parameters'
- );
- }
+export const setEmailPost =
+ ( postId: number | string, postType: string ) =>
+ async ( { dispatch } ) => {
+ if ( ! postId || ! postType ) {
+ throw new Error(
+ 'setEmailPost requires valid postId and postType parameters'
+ );
+ }
+
+ dispatch( {
+ type: 'SET_EMAIL_POST',
+ state: { postId, postType } as Partial< State >,
+ } );
+ };
- return {
- type: 'SET_EMAIL_POST',
- state: { postId, postType } as Partial< State >,
- } as const;
-}
+/**
+ * Invalidates the personalization tags cache to force a refetch.
+ * Call this when the tags need to be refreshed (e.g., after changing automation triggers).
+ */
+export const invalidatePersonalizationTagsCache =
+ () =>
+ async ( { registry } ) => {
+ // Get the current post ID to build the exact query params
+ const postId = registry.select( storeName ).getEmailPostId();
+ const queryParams: Record< string, unknown > = {
+ context: 'view',
+ per_page: -1,
+ };
+ if ( postId ) {
+ queryParams.post_id = postId;
+ }
+
+ // Invalidate the resolution for this specific query
+ registry
+ .dispatch( coreDataStore )
+ .invalidateResolution( 'getEntityRecords', [
+ PERSONALIZATION_TAG_ENTITY.kind,
+ PERSONALIZATION_TAG_ENTITY.name,
+ queryParams,
+ ] );
+ };
export function setEmailPostType( postType: string ) {
if ( ! postType ) {
diff --git a/packages/js/email-editor/src/store/selectors.ts b/packages/js/email-editor/src/store/selectors.ts
index ffcafbada3..41cacc3e24 100644
--- a/packages/js/email-editor/src/store/selectors.ts
+++ b/packages/js/email-editor/src/store/selectors.ts
@@ -367,13 +367,21 @@ export function getPreviewState( state: State ): State[ 'preview' ] {
export const getPersonalizationTagsList = createRegistrySelector(
( select ) => () => {
+ const postId = select( storeName ).getEmailPostId();
+ const queryParams: Record< string, unknown > = {
+ context: 'view',
+ per_page: -1,
+ };
+
+ // Include post_id for context-aware tag filtering (e.g., automation emails)
+ if ( postId ) {
+ queryParams.post_id = postId;
+ }
+
const tags = ( select( coreDataStore ).getEntityRecords(
PERSONALIZATION_TAG_ENTITY.kind,
PERSONALIZATION_TAG_ENTITY.name,
- {
- context: 'view',
- per_page: -1,
- }
+ queryParams
) || [] ) as PersonalizationTag[];
const postType = select( storeName ).getEmailPostType();
diff --git a/packages/php/email-editor/README.md b/packages/php/email-editor/README.md
index c9640a6bce..6e3cee4643 100644
--- a/packages/php/email-editor/README.md
+++ b/packages/php/email-editor/README.md
@@ -80,13 +80,14 @@ We may add, update and delete any of them.
### Actions
-| Name | Argument | Description |
-|--------------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `woocommerce_email_editor_initialized` | `null` | Called when the Email Editor is initialized |
-| `woocommerce_email_blocks_renderer_initialized` | `BlocksRegistry` | Called when the block content renderer is initialized. You may use this to add a new BlockRenderer |
-| `woocommerce_email_editor_register_templates` | | Called when the basic blank email template is registered. You can add more templates via register_block_template |
-| `woocommerce_email_editor_send_preview_email_before_wp_mail` | `string` $to, `string` $subject, `string` $body | Called before sending the preview email via wp_mail. Use this to modify email headers (e.g., from address, from name) or perform pre-send actions. |
-| `woocommerce_email_editor_send_preview_email_after_wp_mail` | `string` $to, `string` $subject, `string` $body, `bool` $result | Called after sending the preview email via wp_mail. Use this to clean up filters, log results, or perform post-send actions. The `$result` indicates success. |
+| Name | Argument | Description |
+|--------------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `woocommerce_email_editor_initialized` | `null` | Called when the Email Editor is initialized |
+| `woocommerce_email_blocks_renderer_initialized` | `BlocksRegistry` | Called when the block content renderer is initialized. You may use this to add a new BlockRenderer |
+| `woocommerce_email_editor_register_templates` | | Called when the basic blank email template is registered. You can add more templates via register_block_template |
+| `woocommerce_email_editor_send_preview_email_before_wp_mail` | `string` $to, `string` $subject, `string` $body | Called before sending the preview email via wp_mail. Use this to modify email headers (e.g., from address, from name) or perform pre-send actions. |
+| `woocommerce_email_editor_send_preview_email_after_wp_mail` | `string` $to, `string` $subject, `string` $body, `bool` $result | Called after sending the preview email via wp_mail. Use this to clean up filters, log results, or perform post-send actions. The `$result` indicates success. |
+| `woocommerce_email_editor_personalization_tags_for_post` | `int` $post_id | Fires before retrieving personalization tags, allowing extensions to register or modify tags based on the post context. Only fires when post_id is a valid integer. |
### Filters
diff --git a/packages/php/email-editor/changelog/add-post-id-context-personalization-tags b/packages/php/email-editor/changelog/add-post-id-context-personalization-tags
new file mode 100644
index 0000000000..971b17e306
--- /dev/null
+++ b/packages/php/email-editor/changelog/add-post-id-context-personalization-tags
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add post_id parameter and action hook for context-aware personalization tags
diff --git a/packages/php/email-editor/src/Engine/class-email-api-controller.php b/packages/php/email-editor/src/Engine/class-email-api-controller.php
index 0248d1406a..10f1adc8e3 100644
--- a/packages/php/email-editor/src/Engine/class-email-api-controller.php
+++ b/packages/php/email-editor/src/Engine/class-email-api-controller.php
@@ -125,9 +125,26 @@ class Email_Api_Controller {
* This endpoint follows WordPress REST API conventions by returning
* the array directly instead of wrapping it in a response object.
*
+ * @param WP_REST_Request $request The REST request object.
* @return WP_REST_Response
+ * @phpstan-param WP_REST_Request<array<string, mixed>> $request
*/
- public function get_personalization_tags_collection(): WP_REST_Response {
+ public function get_personalization_tags_collection( WP_REST_Request $request ): WP_REST_Response {
+ $post_id = $request->get_param( 'post_id' );
+
+ // Allow extensions to extend or modify tags based on post context.
+ // This fires before getting tags so extensions can register additional tags.
+ $post_id = is_numeric( $post_id ) ? (int) $post_id : 0;
+ if ( $post_id > 0 ) {
+ /**
+ * Fires before retrieving personalization tags, allowing extensions
+ * to register or modify tags based on the post context.
+ *
+ * @param int $post_id The post ID for context-aware tag handling.
+ */
+ do_action( 'woocommerce_email_editor_personalization_tags_for_post', $post_id );
+ }
+
$tags = $this->personalization_tags_registry->get_all();
return new WP_REST_Response(
array_values(
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 4544e67189..6363a0e448 100644
--- a/packages/php/email-editor/src/Engine/class-email-editor.php
+++ b/packages/php/email-editor/src/Engine/class-email-editor.php
@@ -288,6 +288,14 @@ class Email_Editor {
'permission_callback' => function () {
return current_user_can( 'edit_posts' );
},
+ 'args' => array(
+ 'post_id' => array(
+ 'description' => __( 'The post ID for context-aware tag filtering.', 'woocommerce' ),
+ 'type' => 'integer',
+ 'required' => false,
+ 'sanitize_callback' => 'absint',
+ ),
+ ),
)
);
}
diff --git a/packages/php/email-editor/tests/integration/Engine/Email_Api_Controller_Test.php b/packages/php/email-editor/tests/integration/Engine/Email_Api_Controller_Test.php
new file mode 100644
index 0000000000..8f3d52d945
--- /dev/null
+++ b/packages/php/email-editor/tests/integration/Engine/Email_Api_Controller_Test.php
@@ -0,0 +1,231 @@
+<?php
+/**
+ * This file is part of the WooCommerce Email Editor package.
+ *
+ * @package Automattic\WooCommerce\EmailEditor
+ */
+
+declare(strict_types = 1);
+
+use Automattic\WooCommerce\EmailEditor\Engine\Email_Api_Controller;
+use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tag;
+use Automattic\WooCommerce\EmailEditor\Engine\PersonalizationTags\Personalization_Tags_Registry;
+
+/**
+ * Integration tests for Email_Api_Controller.
+ */
+class Email_Api_Controller_Test extends Email_Editor_Integration_Test_Case {
+ /**
+ * The API controller instance.
+ *
+ * @var Email_Api_Controller
+ */
+ private Email_Api_Controller $controller;
+
+ /**
+ * The personalization tags registry.
+ *
+ * @var Personalization_Tags_Registry
+ */
+ private Personalization_Tags_Registry $registry;
+
+ /**
+ * Set up before each test.
+ */
+ public function setUp(): void {
+ parent::setUp();
+ $this->registry = $this->di_container->get( Personalization_Tags_Registry::class );
+ $this->controller = $this->di_container->get( Email_Api_Controller::class );
+
+ // Register a test personalization tag.
+ $this->registry->register(
+ new Personalization_Tag(
+ 'Test Tag',
+ 'test_tag',
+ 'Test Category',
+ function () {
+ return 'Test Value';
+ }
+ )
+ );
+ }
+
+ /**
+ * Tear down after each test.
+ */
+ public function tearDown(): void {
+ parent::tearDown();
+ remove_all_actions( 'woocommerce_email_editor_personalization_tags_for_post' );
+ }
+
+ /**
+ * Create a REST request for personalization tags endpoint.
+ *
+ * @return WP_REST_Request<array<string, mixed>>
+ */
+ private function create_request(): WP_REST_Request {
+ /**
+ * WP REST Request object.
+ *
+ * @var WP_REST_Request<array<string, mixed>> $request
+ */
+ $request = new WP_REST_Request( 'GET', '/woocommerce-email-editor/v1/personalization_tags' );
+ return $request;
+ }
+
+ /**
+ * Test that the action is fired when a valid post_id is provided.
+ */
+ public function testActionFiredWithValidPostId(): void {
+ $action_fired = false;
+ $received_value = null;
+
+ add_action(
+ 'woocommerce_email_editor_personalization_tags_for_post',
+ function ( $post_id ) use ( &$action_fired, &$received_value ) {
+ $action_fired = true;
+ $received_value = $post_id;
+ }
+ );
+
+ $request = $this->create_request();
+ $request->set_param( 'post_id', 123 );
+
+ $this->controller->get_personalization_tags_collection( $request );
+
+ $this->assertTrue( $action_fired, 'Action should be fired when valid post_id is provided' );
+ $this->assertSame( 123, $received_value, 'Action should receive the correct post_id' );
+ }
+
+ /**
+ * Test that the action is fired when post_id is provided as a string.
+ */
+ public function testActionFiredWithNumericStringPostId(): void {
+ $action_fired = false;
+ $received_value = null;
+
+ add_action(
+ 'woocommerce_email_editor_personalization_tags_for_post',
+ function ( $post_id ) use ( &$action_fired, &$received_value ) {
+ $action_fired = true;
+ $received_value = $post_id;
+ }
+ );
+
+ $request = $this->create_request();
+ $request->set_param( 'post_id', '456' );
+
+ $this->controller->get_personalization_tags_collection( $request );
+
+ $this->assertTrue( $action_fired, 'Action should be fired when numeric string post_id is provided' );
+ $this->assertSame( 456, $received_value, 'Action should receive the post_id converted to integer' );
+ }
+
+ /**
+ * Test that the action is NOT fired when post_id is not provided.
+ */
+ public function testActionNotFiredWithoutPostId(): void {
+ $action_fired = false;
+
+ add_action(
+ 'woocommerce_email_editor_personalization_tags_for_post',
+ function () use ( &$action_fired ) {
+ $action_fired = true;
+ }
+ );
+
+ $request = $this->create_request();
+
+ $this->controller->get_personalization_tags_collection( $request );
+
+ $this->assertFalse( $action_fired, 'Action should not be fired when post_id is not provided' );
+ }
+
+ /**
+ * Test that the action is NOT fired when post_id is 0.
+ */
+ public function testActionNotFiredWithZeroPostId(): void {
+ $action_fired = false;
+
+ add_action(
+ 'woocommerce_email_editor_personalization_tags_for_post',
+ function () use ( &$action_fired ) {
+ $action_fired = true;
+ }
+ );
+
+ $request = $this->create_request();
+ $request->set_param( 'post_id', 0 );
+
+ $this->controller->get_personalization_tags_collection( $request );
+
+ $this->assertFalse( $action_fired, 'Action should not be fired when post_id is 0' );
+ }
+
+ /**
+ * Test that the action is NOT fired when post_id is a negative number.
+ */
+ public function testActionNotFiredWithNegativePostId(): void {
+ $action_fired = false;
+
+ add_action(
+ 'woocommerce_email_editor_personalization_tags_for_post',
+ function () use ( &$action_fired ) {
+ $action_fired = true;
+ }
+ );
+
+ $request = $this->create_request();
+ $request->set_param( 'post_id', -1 );
+
+ $this->controller->get_personalization_tags_collection( $request );
+
+ $this->assertFalse( $action_fired, 'Action should not be fired when post_id is negative' );
+ }
+
+ /**
+ * Test that the action is NOT fired when post_id is a non-numeric string (like template IDs).
+ */
+ public function testActionNotFiredWithNonNumericPostId(): void {
+ $action_fired = false;
+
+ add_action(
+ 'woocommerce_email_editor_personalization_tags_for_post',
+ function () use ( &$action_fired ) {
+ $action_fired = true;
+ }
+ );
+
+ $request = $this->create_request();
+ // Template IDs can be strings like "twentytwentyfive//wooemailtemplate".
+ $request->set_param( 'post_id', 'twentytwentyfive//wooemailtemplate' );
+
+ $this->controller->get_personalization_tags_collection( $request );
+
+ $this->assertFalse( $action_fired, 'Action should not be fired when post_id is a non-numeric string' );
+ }
+
+ /**
+ * Test that the response returns personalization tags.
+ */
+ public function testReturnsPersonalizationTags(): void {
+ $request = $this->create_request();
+
+ $response = $this->controller->get_personalization_tags_collection( $request );
+ $data = $response->get_data();
+
+ $this->assertIsArray( $data );
+ $this->assertNotEmpty( $data );
+
+ // Find the test tag we registered.
+ $found = false;
+ foreach ( $data as $tag ) {
+ if ( is_array( $tag ) && isset( $tag['token'] ) && '[test_tag]' === $tag['token'] ) {
+ $found = true;
+ break;
+ }
+ }
+
+ $this->assertTrue( $found, 'Test tag should be in the response' );
+ }
+}