@capillarytech/creatives-library 9.0.56-alpha.7 → 9.0.56-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/v2Components/CapActionButton/index.js +73 -60
  3. package/v2Components/CapActionButton/index.scss +44 -28
  4. package/v2Components/CapActionButton/messages.js +7 -3
  5. package/v2Components/CapActionButton/tests/index.test.js +17 -1
  6. package/v2Components/CapWhatsappCTA/messages.js +4 -0
  7. package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
  8. package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
  9. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +9 -1
  10. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +58 -0
  11. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +3 -0
  12. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +19 -4
  13. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +8 -4
  14. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
  15. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +26 -0
  16. package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
  17. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  18. package/v2Containers/CreativesContainer/index.js +27 -0
  19. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
  20. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +13 -0
  21. package/v2Containers/MobilePush/Create/index.js +18 -1
  22. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
  23. package/v2Containers/MobilePush/Edit/index.js +18 -1
  24. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
  25. package/v2Containers/MobilePush/commonMethods.js +49 -1
  26. package/v2Containers/MobilePushNew/index.js +29 -4
  27. package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
  28. package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
  29. package/v2Containers/MobilePushNew/utils.js +34 -1
  30. package/v2Containers/MobilepushWrapper/index.js +3 -1
  31. package/v2Containers/Rcs/index.js +34 -0
  32. package/v2Containers/Rcs/index.scss +15 -0
  33. package/v2Containers/Rcs/tests/index.test.js +67 -0
  34. package/v2Containers/Sms/Create/index.js +3 -1
  35. package/v2Containers/Sms/Edit/index.js +25 -0
  36. package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
  37. package/v2Containers/Viber/index.js +24 -1
  38. package/v2Containers/Viber/tests/index.test.js +103 -0
  39. package/v2Containers/WebPush/Create/index.js +24 -0
  40. package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
  41. package/v2Containers/Whatsapp/index.js +19 -1
  42. package/v2Containers/Whatsapp/tests/index.test.js +115 -0
  43. package/v2Containers/Zalo/index.js +28 -1
  44. package/v2Containers/Zalo/tests/index.test.js +99 -0
@@ -158,6 +158,7 @@ const renderHelper = (args) => {
158
158
  {...(args.omitGetDefaultTags ? {} : { getDefaultTags: true })}
159
159
  isDltEnabled={args.isDltEnabled || false}
160
160
  smsRegister={'DLT'}
161
+ onContentValidityChange={args.onContentValidityChange}
161
162
  />
162
163
  </Provider>,
163
164
  );
@@ -1923,6 +1924,72 @@ describe('Character Counting Functions', () => {
1923
1924
  });
1924
1925
  });
1925
1926
 
1927
+ describe('Rcs — onContentValidityChange reporting', () => {
1928
+ // Full-mode create with a fresh (non-hydrating) rcsData: templateDesc starts out empty,
1929
+ // same as the pre-existing "template title change" test's initial-state assumption.
1930
+ it('reports isContentEmpty: true on mount when the (default text_message) description is empty', () => {
1931
+ const onContentValidityChange = jest.fn();
1932
+ renderHelper({ onContentValidityChange });
1933
+
1934
+ expect(onContentValidityChange).toHaveBeenCalledWith(
1935
+ expect.objectContaining({ isContentEmpty: true }),
1936
+ );
1937
+ });
1938
+
1939
+ it('reports isContentEmpty: false once the description has content, then true again once cleared', () => {
1940
+ const onContentValidityChange = jest.fn();
1941
+ renderHelper({ onContentValidityChange });
1942
+ onContentValidityChange.mockClear();
1943
+
1944
+ const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
1945
+ act(() => {
1946
+ descTextArea.props().onChange({ target: { value: 'Hello there' } });
1947
+ });
1948
+ renderedComponent.update();
1949
+
1950
+ expect(onContentValidityChange).toHaveBeenCalledWith(
1951
+ expect.objectContaining({ isContentEmpty: false }),
1952
+ );
1953
+ onContentValidityChange.mockClear();
1954
+
1955
+ const descTextAreaAfter = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
1956
+ act(() => {
1957
+ descTextAreaAfter.props().onChange({ target: { value: ' ' } });
1958
+ });
1959
+ renderedComponent.update();
1960
+
1961
+ expect(onContentValidityChange).toHaveBeenCalledWith(
1962
+ expect.objectContaining({ isContentEmpty: true }),
1963
+ );
1964
+ });
1965
+
1966
+ it('does not throw and does not report when onContentValidityChange is not provided', () => {
1967
+ expect(() => {
1968
+ renderHelper({});
1969
+ const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
1970
+ act(() => {
1971
+ descTextArea.props().onChange({ target: { value: 'abc' } });
1972
+ });
1973
+ renderedComponent.update();
1974
+ }).not.toThrow();
1975
+ });
1976
+
1977
+ it('does not dispatch again on a re-render that does not change validity (dedup guard)', () => {
1978
+ const onContentValidityChange = jest.fn();
1979
+ renderHelper({ onContentValidityChange });
1980
+ onContentValidityChange.mockClear();
1981
+
1982
+ const descTextArea = renderedComponent.find('CapInputTextArea#rcs_template_message_textarea').at(0);
1983
+ act(() => {
1984
+ // Still empty (still whitespace-only) — validity does not actually change.
1985
+ descTextArea.props().onChange({ target: { value: ' ' } });
1986
+ });
1987
+ renderedComponent.update();
1988
+
1989
+ expect(onContentValidityChange).not.toHaveBeenCalled();
1990
+ });
1991
+ });
1992
+
1926
1993
  describe('CapActionButton (mocked within RCS tests)', () => {
1927
1994
  it('should render the mocked CapActionButton placeholder', () => {
1928
1995
  const wrapper = mountWithIntl(
@@ -185,7 +185,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
185
185
  }
186
186
 
187
187
  componentDidUpdate() {
188
- if (!this.props.embeddedSmsFallback || typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
188
+ // Reporting itself is opt-in via the callback prop — not tied to embeddedSmsFallback,
189
+ // which is a separate, unrelated flag (tag-popover styling for the SMS-fallback surface).
190
+ if (typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
189
191
  return;
190
192
  }
191
193
  const validity = getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount);
@@ -34,6 +34,7 @@ import v2EditSmsReducer from './reducer';
34
34
  import { v2SmsEditSagas } from './sagas';
35
35
  import * as globalActions from '../../Cap/actions';
36
36
  import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
37
+ import { getSmsEmbeddedFooterValidity } from '../smsFormDataHelpers';
37
38
 
38
39
  export class Edit extends React.Component { // eslint-disable-line react/prefer-stateless-function
39
40
  constructor(props) {
@@ -54,6 +55,11 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
54
55
  isTestAndPreviewMode: false,
55
56
  pendingGetFormData: false,
56
57
  };
58
+ // Tracks the last validity value reported to the parent so componentDidUpdate does not
59
+ // dispatch on every render. Intentionally undefined (not true) so the first render
60
+ // always reports the real validity rather than assuming the form starts invalid.
61
+ this._lastReportedSmsFooterTemplateNameEmpty = undefined;
62
+ this._lastReportedSmsFooterMessageEmpty = undefined;
57
63
  this.saveFormData = this.saveFormData.bind(this);
58
64
  this.onFormDataChange = this.onFormDataChange.bind(this);
59
65
  this.onTemplateNameChange = this.onTemplateNameChange.bind(this);
@@ -189,6 +195,24 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
189
195
  }
190
196
  }
191
197
 
198
+ componentDidUpdate() {
199
+ // Reports live form validity so the host (CreativesContainer) can disable Done/Preview
200
+ // and Test as soon as the message body is cleared — mirrors Sms/Create's own reporting.
201
+ if (typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
202
+ return;
203
+ }
204
+ const validity = getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount);
205
+ const isTemplateNameEmpty = !!validity.isTemplateNameEmpty;
206
+ const isMessageEmpty = !!validity.isMessageEmpty;
207
+ if (
208
+ this._lastReportedSmsFooterTemplateNameEmpty === isTemplateNameEmpty &&
209
+ this._lastReportedSmsFooterMessageEmpty === isMessageEmpty
210
+ ) return;
211
+ this._lastReportedSmsFooterTemplateNameEmpty = isTemplateNameEmpty;
212
+ this._lastReportedSmsFooterMessageEmpty = isMessageEmpty;
213
+ this.props.onEmbeddedSmsFooterValidity(validity);
214
+ }
215
+
192
216
  componentWillUnmount() {
193
217
  if (this.pendingGetFormDataTimeout) {
194
218
  clearTimeout(this.pendingGetFormDataTimeout);
@@ -1138,6 +1162,7 @@ Edit.propTypes = {
1138
1162
  handleTestAndPreview: PropTypes.func,
1139
1163
  handleCloseTestAndPreview: PropTypes.func,
1140
1164
  isTestAndPreviewMode: PropTypes.bool,
1165
+ onEmbeddedSmsFooterValidity: PropTypes.func,
1141
1166
  };
1142
1167
 
1143
1168
  const mapStateToProps = createStructuredSelector({
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { shallow } from 'enzyme';
3
+ import { Edit } from '../index';
4
+
5
+ // Shallow-render the plain class (bypassing the withCreatives/Redux/saga HOC) so this test
6
+ // only exercises the componentDidUpdate → onEmbeddedSmsFooterValidity wiring in isolation.
7
+
8
+ const baseProps = () => ({
9
+ actions: {
10
+ getTemplateDetails: jest.fn(),
11
+ setTemplateDetails: jest.fn(),
12
+ clearEditResponse: jest.fn(),
13
+ resetEditTemplate: jest.fn(),
14
+ editTemplate: jest.fn(),
15
+ },
16
+ globalActions: {
17
+ fetchSchemaForEntity: jest.fn(),
18
+ setInjectedTags: jest.fn(),
19
+ addMessageToQueue: jest.fn(),
20
+ },
21
+ Edit: {},
22
+ params: { id: 'tpl-1' },
23
+ location: { query: { type: 'embedded', module: 'library' } },
24
+ metaEntities: {},
25
+ intl: { formatMessage: (m) => m.defaultMessage || m.id },
26
+ isFullMode: false,
27
+ getFormSubscriptionData: jest.fn(),
28
+ });
29
+
30
+ describe('Sms/Edit — onEmbeddedSmsFooterValidity reporting', () => {
31
+ it('reports isMessageEmpty: true on mount when the message body is empty', () => {
32
+ const onEmbeddedSmsFooterValidity = jest.fn();
33
+ const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
34
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
35
+
36
+ expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
37
+ expect.objectContaining({ isMessageEmpty: true }),
38
+ );
39
+ });
40
+
41
+ it('reports isMessageEmpty: false once the message body has content', () => {
42
+ const onEmbeddedSmsFooterValidity = jest.fn();
43
+ const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
44
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
45
+ onEmbeddedSmsFooterValidity.mockClear();
46
+
47
+ wrapper.setState({ formData: { 0: { 'sms-editor': 'Hello there' }, base: { 'sms-editor': 'Hello there' } }, tabCount: 1 });
48
+
49
+ expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
50
+ expect.objectContaining({ isMessageEmpty: false }),
51
+ );
52
+ });
53
+
54
+ it('re-reports empty when an existing (edit-loaded) message is cleared out', () => {
55
+ const onEmbeddedSmsFooterValidity = jest.fn();
56
+ const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
57
+ wrapper.setState({ formData: { 0: { 'sms-editor': 'Existing content' }, base: { 'sms-editor': 'Existing content' } }, tabCount: 1 });
58
+ onEmbeddedSmsFooterValidity.mockClear();
59
+
60
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' }, base: { 'sms-editor': '' } }, tabCount: 1 });
61
+
62
+ expect(onEmbeddedSmsFooterValidity).toHaveBeenCalledWith(
63
+ expect.objectContaining({ isMessageEmpty: true }),
64
+ );
65
+ });
66
+
67
+ it('does not throw and does not report when onEmbeddedSmsFooterValidity is not provided', () => {
68
+ const wrapper = shallow(<Edit {...baseProps()} />);
69
+ expect(() => {
70
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' } }, tabCount: 1 });
71
+ }).not.toThrow();
72
+ });
73
+
74
+ it('does not dispatch again on a re-render that does not change validity (dedup guard)', () => {
75
+ const onEmbeddedSmsFooterValidity = jest.fn();
76
+ const wrapper = shallow(<Edit {...baseProps()} onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity} />);
77
+ wrapper.setState({ formData: { 0: { 'sms-editor': '' } }, tabCount: 1 });
78
+ onEmbeddedSmsFooterValidity.mockClear();
79
+
80
+ // Unrelated state change — validity tuple (isTemplateNameEmpty/isMessageEmpty) is unchanged.
81
+ wrapper.setState({ currentTab: 1 });
82
+
83
+ expect(onEmbeddedSmsFooterValidity).not.toHaveBeenCalled();
84
+ });
85
+ });
@@ -1,4 +1,6 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
1
+ import React, {
2
+ useState, useEffect, useCallback, useRef,
3
+ } from 'react';
2
4
  import { bindActionCreators } from 'redux';
3
5
  import { createStructuredSelector } from 'reselect';
4
6
  import { injectIntl, FormattedMessage } from 'react-intl';
@@ -139,6 +141,7 @@ export const Viber = (props) => {
139
141
  showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
140
142
  handleTestAndPreview: propsHandleTestAndPreview,
141
143
  handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
144
+ onContentValidityChange,
142
145
  } = props || {};
143
146
 
144
147
  const { formatMessage } = intl;
@@ -257,6 +260,26 @@ export const Viber = (props) => {
257
260
  }
258
261
  }, [viber.templateDetails || templateData]);
259
262
 
263
+ // Reports live message-content validity to a parent (e.g. CreativesContainer's
264
+ // slidebox) so it can disable its own Done/Preview-and-test buttons whenever the
265
+ // Viber message body is empty/whitespace-only. Mirrors the Sms Create/Edit
266
+ // componentDidUpdate pattern, using a ref (instead of an instance field) as the
267
+ // dedup guard so the callback only fires when the reported value actually changes -
268
+ // calling it unconditionally on every render risks an infinite update loop when the
269
+ // parent's setState triggers a re-render of this component.
270
+ const lastReportedContentEmptyRef = useRef(undefined);
271
+ useEffect(() => {
272
+ if (typeof onContentValidityChange !== 'function') {
273
+ return;
274
+ }
275
+ const isContentEmpty = (messageContent || '').trim() === '';
276
+ if (lastReportedContentEmptyRef.current === isContentEmpty) {
277
+ return;
278
+ }
279
+ lastReportedContentEmptyRef.current = isContentEmpty;
280
+ onContentValidityChange({ isContentEmpty });
281
+ }, [messageContent, onContentValidityChange]);
282
+
260
283
  // Text area Code start here
261
284
 
262
285
  // Tags Code start from here
@@ -469,4 +469,107 @@ describe('Test Viber container', () => {
469
469
 
470
470
  expect(await screen.findByText("URL can't be empty")).toBeInTheDocument();
471
471
  });
472
+
473
+ describe('onContentValidityChange reporting (Done/Preview button gating in the slidebox)', () => {
474
+ it('reports isContentEmpty: true on mount for an empty create flow, then false once a message is typed', async () => {
475
+ const onContentValidityChange = jest.fn();
476
+ renderComponent({
477
+ actions: mockActions,
478
+ globalActions: mockGlobalActions,
479
+ templateData: { mode: 'create' },
480
+ viber: {
481
+ uploadedAssetData: {},
482
+ createTemplateInProgress: false,
483
+ },
484
+ location: {
485
+ pathname: '/sms/edit',
486
+ query: { type: false, module: 'default' },
487
+ search: '',
488
+ },
489
+ isFullMode: true,
490
+ handleClose: jest.fn(),
491
+ onContentValidityChange,
492
+ });
493
+
494
+ await waitFor(() => {
495
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
496
+ });
497
+
498
+ const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
499
+ fireEvent.change(msgBox, { target: { value: 'hello there' } });
500
+
501
+ await waitFor(() => {
502
+ expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: false });
503
+ });
504
+ });
505
+
506
+ it('does not re-report the same validity value (dedup guard)', async () => {
507
+ const onContentValidityChange = jest.fn();
508
+ renderComponent({
509
+ actions: mockActions,
510
+ globalActions: mockGlobalActions,
511
+ templateData: { mode: 'create' },
512
+ viber: {
513
+ uploadedAssetData: {},
514
+ createTemplateInProgress: false,
515
+ },
516
+ location: {
517
+ pathname: '/sms/edit',
518
+ query: { type: false, module: 'default' },
519
+ search: '',
520
+ },
521
+ isFullMode: true,
522
+ handleClose: jest.fn(),
523
+ onContentValidityChange,
524
+ });
525
+
526
+ await waitFor(() => {
527
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
528
+ });
529
+ const callsAfterMount = onContentValidityChange.mock.calls.length;
530
+
531
+ // Whitespace-only change: still empty, should not trigger another call.
532
+ const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
533
+ fireEvent.change(msgBox, { target: { value: ' ' } });
534
+
535
+ expect(onContentValidityChange).toHaveBeenCalledTimes(callsAfterMount);
536
+ });
537
+
538
+ it('reports isContentEmpty: false for an edit flow pre-filled with message content, then true once cleared', async () => {
539
+ const onContentValidityChange = jest.fn();
540
+ renderComponent({
541
+ actions: mockActions,
542
+ globalActions: mockGlobalActions,
543
+ templateData: { mode: 'create' },
544
+ viber: {
545
+ uploadedAssetData: {},
546
+ createTemplateInProgress: false,
547
+ templateDetails: templateDetailsText,
548
+ },
549
+ location: {
550
+ pathname: '/sms/edit',
551
+ query: { type: false, module: 'default' },
552
+ search: '',
553
+ },
554
+ isFullMode: true,
555
+ params: { id: 'test' },
556
+ handleClose: jest.fn(),
557
+ metaEntities,
558
+ getDefaultTags,
559
+ injectedTags,
560
+ onContentValidityChange,
561
+ });
562
+
563
+ await waitFor(() => {
564
+ expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: false });
565
+ });
566
+
567
+ const msgBox = screen.getAllByPlaceholderText(/Enter message/i)[0];
568
+ fireEvent.change(msgBox, { target: { value: '' } });
569
+
570
+ await waitFor(() => {
571
+ expect(onContentValidityChange).toHaveBeenLastCalledWith({ isContentEmpty: true });
572
+ });
573
+ });
574
+ });
472
575
  });
@@ -164,6 +164,7 @@ const WebPushCreate = ({
164
164
  templateActions: templateActionsProps,
165
165
  Templates,
166
166
  restrictPersonalization = false,
167
+ onContentValidityChange,
167
168
  }) => {
168
169
  const { formatMessage } = intl;
169
170
  const aiContentBotDisabled = isAiContentBotDisabled();
@@ -194,6 +195,10 @@ const WebPushCreate = ({
194
195
  const messageCountRef = useRef(null);
195
196
  const saveInitiatedRef = useRef(false);
196
197
  const messageTextAreaRef = useRef(null);
198
+ // Tracks the last isContentEmpty value reported to the parent (CreativesContainer) so the
199
+ // effect below does not dispatch on every render. Intentionally undefined (not a boolean)
200
+ // so the first render always reports the real validity rather than assuming a starting value.
201
+ const lastReportedContentEmptyRef = useRef(undefined);
197
202
 
198
203
  // Custom hooks
199
204
  const { updateCharacterCount } = useCharacterCount(formatMessage, messages);
@@ -566,6 +571,24 @@ const WebPushCreate = ({
566
571
  });
567
572
  }, [isFullMode, templateName]);
568
573
 
574
+ // Reports live content validity to the parent (CreativesContainer) so it can disable the
575
+ // Done/Preview-and-test buttons as soon as the notification title or message is cleared.
576
+ // Both title and message are required fields for WebPush (see isSaveDisabled below), so the
577
+ // content is considered empty if either is blank/whitespace-only. Mirrors the Sms Create/Edit
578
+ // componentDidUpdate pattern — the ref-based dedup guard prevents an infinite render loop
579
+ // (parent setState -> re-render -> effect re-fires -> parent setState -> ...).
580
+ useEffect(() => {
581
+ if (typeof onContentValidityChange !== 'function') {
582
+ return;
583
+ }
584
+ const isContentEmpty = !notificationTitle.trim() || !message.trim();
585
+ if (lastReportedContentEmptyRef.current === isContentEmpty) {
586
+ return;
587
+ }
588
+ lastReportedContentEmptyRef.current = isContentEmpty;
589
+ onContentValidityChange({ isContentEmpty });
590
+ }, [notificationTitle, message, onContentValidityChange]);
591
+
569
592
  // Pure validator that returns boolean without setting error state
570
593
  const validateFormSilent = () => {
571
594
  const templateNameInvalid = isFullMode && validateTemplateName(templateName);
@@ -1175,6 +1198,7 @@ WebPushCreate.propTypes = {
1175
1198
  waitEventContextTags: PropTypes.object,
1176
1199
  templateActions: PropTypes.object,
1177
1200
  restrictPersonalization: PropTypes.bool,
1201
+ onContentValidityChange: PropTypes.func,
1178
1202
  };
1179
1203
 
1180
1204
  WebPushCreate.defaultProps = {