Commit b681dadd65f for woocommerce

commit b681dadd65fe8363dd449a75476a90f14f01a972
Author: Jan Lysý <lysyjan@users.noreply.github.com>
Date:   Tue Mar 17 16:36:11 2026 +0100

    Email Editor: Add filter for send button disabled state (#63722)

    * Add woocommerce_email_editor_send_button_disabled filter to SendButton

    Allow consumers to override the send button's disabled state via filter.
    The filter passes the computed disabled boolean and the individual flags
    (hasEmptyContent, isEmailSent, isDirty) so consumers can make informed
    decisions. Default behavior is unchanged.

    This enables CIAB to keep the "Review and send" button always enabled
    and auto-save unsaved changes when clicked, instead of requiring users
    to manually save a draft first.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Fix prettier formatting in send button test

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Add send button disabled filter to README filters table

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Add runtime validation for send button disabled filter

    Validate that the filtered value is a boolean at runtime, falling back
    to the default disabled state if a third-party filter returns an
    unexpected type. Addresses CodeRabbit review feedback.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Assert filter contract in send button disabled test

    Add assertion that applyFilters is called with the full flags object
    (hasEmptyContent, isEmailSent, isDirty) to lock the public hook
    contract and prevent silent regressions.

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    * Use mockImplementationOnce to prevent cross-test leakage

    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/packages/js/email-editor/README.md b/packages/js/email-editor/README.md
index a593bc8084d..c5e63bd7a84 100644
--- a/packages/js/email-editor/README.md
+++ b/packages/js/email-editor/README.md
@@ -318,6 +318,7 @@ We may add, update and delete any of them.
 | `woocommerce_email_editor_wrap_editor_component`                   | `JSX.Element` Editor                                  | `JSX.Element` Editor                       | The main editor component. Custom component can wrap the editor and provide additional functionality                           |
 | `woocommerce_email_editor_send_button_label`                       | `string` 'Send'                                       | `string` 'Send' (default)                  | Email editor send button label. The `Send` text can be updated using this filter                                               |
 | `woocommerce_email_editor_send_action_callback`                    | `function` sendAction                                 | `function` sendAction                      | Action to perform when the Send button is clicked                                                                              |
+| `woocommerce_email_editor_send_button_disabled`                    | `boolean` isDisabled, `object` flags                  | `boolean` isDisabled                       | Override the send button disabled state. The flags object contains `hasEmptyContent`, `isEmailSent`, and `isDirty` booleans    |
 | `woocommerce_email_editor_content_validation_rules`                | `array` rules                                         | `EmailContentValidationRule[]` rules       | Email editor content validation rules. The validation is done on `send button` click and revalidated on `save draft`           |
 | `woocommerce_email_editor_check_sending_method_configuration_link` | `string` link                                         | `string` link                              | Edit or remove the sending configuration link message                                                                          |
 | `woocommerce_email_editor_setting_sidebar_extension_component`     | `JSX.Element` RichTextWithButton                      | `JSX.Element` Sidebar extension component  | Add components to the Email settings sidebar                                                                                   |
diff --git a/packages/js/email-editor/changelog/add-send-button-disabled-filter b/packages/js/email-editor/changelog/add-send-button-disabled-filter
new file mode 100644
index 00000000000..73e7efa3ec8
--- /dev/null
+++ b/packages/js/email-editor/changelog/add-send-button-disabled-filter
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add `woocommerce_email_editor_send_button_disabled` filter to allow overriding the send button disabled state
diff --git a/packages/js/email-editor/src/components/header/send-button.tsx b/packages/js/email-editor/src/components/header/send-button.tsx
index 2330484e7cc..4fd691f7f7e 100644
--- a/packages/js/email-editor/src/components/header/send-button.tsx
+++ b/packages/js/email-editor/src/components/header/send-button.tsx
@@ -34,7 +34,16 @@ export function SendButton() {
 		}
 	}

-	const isDisabled = hasEmptyContent || isEmailSent || isDirty;
+	const defaultIsDisabled = hasEmptyContent || isEmailSent || isDirty;
+	const filteredIsDisabled = applyFilters(
+		'woocommerce_email_editor_send_button_disabled',
+		defaultIsDisabled,
+		{ hasEmptyContent, isEmailSent, isDirty }
+	);
+	const isDisabled =
+		typeof filteredIsDisabled === 'boolean'
+			? filteredIsDisabled
+			: defaultIsDisabled;

 	const label = applyFilters(
 		'woocommerce_email_editor_send_button_label',
diff --git a/packages/js/email-editor/src/components/header/test/send-button.spec.tsx b/packages/js/email-editor/src/components/header/test/send-button.spec.tsx
index ce3ce53ac7c..e37aa30489a 100644
--- a/packages/js/email-editor/src/components/header/test/send-button.spec.tsx
+++ b/packages/js/email-editor/src/components/header/test/send-button.spec.tsx
@@ -7,6 +7,7 @@ import '../../test/__mocks__/setup-shared-mocks';
 import { render, fireEvent } from '@testing-library/react';
 import '@testing-library/jest-dom';
 import { useSelect } from '@wordpress/data';
+import { applyFilters } from '@wordpress/hooks';
 import {
 	// @ts-expect-error -- It is not exported yet.
 	useEntitiesSavedStatesIsDirty,
@@ -31,6 +32,7 @@ const useSelectMock = useSelect as jest.Mock;
 const useEntitiesSavedStatesIsDirtyMock =
 	useEntitiesSavedStatesIsDirty as jest.Mock;
 const recordEventMock = recordEvent as jest.Mock;
+const applyFiltersMock = applyFilters as jest.Mock;

 const mockStoreValues = {
 	hasEmptyContent: false,
@@ -70,6 +72,56 @@ describe( 'SendButton', () => {
 		expect( getByRole( 'button' ) ).toBeDisabled();
 	} );

+	it( 'should allow the disabled state to be overridden via filter', () => {
+		useEntitiesSavedStatesIsDirtyMock.mockReturnValue( { isDirty: true } );
+
+		// Override applyFilters to simulate a filter that removes isDirty from disabled
+		applyFiltersMock.mockImplementationOnce(
+			( hook: string, value: unknown, ...args: unknown[] ) => {
+				if (
+					hook === 'woocommerce_email_editor_send_button_disabled'
+				) {
+					const flags = args[ 0 ] as {
+						hasEmptyContent: boolean;
+						isEmailSent: boolean;
+					};
+					return flags.hasEmptyContent || flags.isEmailSent;
+				}
+				return value;
+			}
+		);
+
+		const { getByRole } = render( <SendButton /> );
+		expect( getByRole( 'button' ) ).not.toBeDisabled();
+		expect( applyFiltersMock ).toHaveBeenCalledWith(
+			'woocommerce_email_editor_send_button_disabled',
+			true,
+			{
+				hasEmptyContent: false,
+				isEmailSent: false,
+				isDirty: true,
+			}
+		);
+	} );
+
+	it( 'should fall back to default when filter returns non-boolean', () => {
+		useEntitiesSavedStatesIsDirtyMock.mockReturnValue( { isDirty: true } );
+
+		applyFiltersMock.mockImplementationOnce(
+			( hook: string, value: unknown ) => {
+				if (
+					hook === 'woocommerce_email_editor_send_button_disabled'
+				) {
+					return 'not-a-boolean';
+				}
+				return value;
+			}
+		);
+
+		const { getByRole } = render( <SendButton /> );
+		expect( getByRole( 'button' ) ).toBeDisabled();
+	} );
+
 	it( 'should be disabled if hasEmptyContent is true', () => {
 		mockStoreValues.hasEmptyContent = true;