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' );
+	}
+}