@capillarytech/creatives-library 9.0.56-betaa.0 → 9.0.56

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 (116) hide show
  1. package/constants/unified.js +9 -4
  2. package/package.json +1 -1
  3. package/services/api.js +25 -4
  4. package/utils/common.js +0 -12
  5. package/utils/templateVarUtils.js +16 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/index.js +73 -60
  8. package/v2Components/CapActionButton/index.scss +44 -28
  9. package/v2Components/CapActionButton/messages.js +7 -3
  10. package/v2Components/CapActionButton/tests/index.test.js +17 -1
  11. package/v2Components/CapWhatsappCTA/messages.js +4 -0
  12. package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
  13. package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
  15. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  16. package/v2Components/CommonTestAndPreview/constants.js +8 -0
  17. package/v2Components/CommonTestAndPreview/index.js +55 -3
  18. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  19. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  20. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  21. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
  22. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  23. package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
  24. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  25. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  26. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  27. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  28. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +46 -320
  29. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
  30. package/v2Components/FormBuilder/Functional/constants.js +8 -43
  31. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
  32. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
  33. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
  34. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
  35. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
  36. package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
  37. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
  38. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
  39. package/v2Components/FormBuilder/index.js +12 -34
  40. package/v2Components/FormBuilder/tests/entryGate.test.js +7 -94
  41. package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
  42. package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
  43. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
  44. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
  45. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
  46. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
  47. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  48. package/v2Containers/CommunicationFlow/constants.js +65 -4
  49. package/v2Containers/CommunicationFlow/index.js +16 -23
  50. package/v2Containers/CommunicationFlow/messages.js +11 -3
  51. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
  52. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  53. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
  54. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
  55. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
  56. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
  57. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
  58. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
  59. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
  60. package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
  61. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  62. package/v2Containers/CreativesContainer/index.js +29 -3
  63. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
  64. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
  65. package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
  66. package/v2Containers/MobilePush/Create/index.js +18 -1
  67. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
  68. package/v2Containers/MobilePush/Edit/index.js +18 -1
  69. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
  70. package/v2Containers/MobilePush/commonMethods.js +49 -1
  71. package/v2Containers/MobilePushNew/index.js +29 -4
  72. package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
  73. package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
  74. package/v2Containers/MobilePushNew/utils.js +34 -1
  75. package/v2Containers/MobilepushWrapper/index.js +3 -1
  76. package/v2Containers/Rcs/index.js +34 -0
  77. package/v2Containers/Rcs/index.scss +15 -0
  78. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
  79. package/v2Containers/Rcs/tests/index.test.js +67 -0
  80. package/v2Containers/Sms/Create/index.js +3 -1
  81. package/v2Containers/Sms/Edit/index.js +25 -0
  82. package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
  83. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
  84. package/v2Containers/Viber/index.js +24 -1
  85. package/v2Containers/Viber/tests/index.test.js +103 -0
  86. package/v2Containers/WebPush/Create/index.js +19 -0
  87. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  88. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  89. package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
  90. package/v2Containers/Whatsapp/index.js +25 -4
  91. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
  92. package/v2Containers/Whatsapp/tests/index.test.js +115 -0
  93. package/v2Containers/Zalo/index.js +28 -1
  94. package/v2Containers/Zalo/tests/index.test.js +99 -0
  95. package/v2Containers/mockdata.js +25 -0
  96. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  97. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -82
  98. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  99. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  100. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -19
  101. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  102. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  103. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  104. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  105. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  106. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  107. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -89
  108. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -440
  109. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  110. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -282
  111. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -141
  112. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  113. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -151
  114. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -342
  115. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  116. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -33,6 +33,7 @@ const WebPushPreview = ({
33
33
  brandIconSrc,
34
34
  buttons,
35
35
  onClickShowInAllDevices,
36
+ showPreviewInAllDevices,
36
37
  }) => {
37
38
  const [selectedOS, setSelectedOS] = useState(DEFAULT_OS);
38
39
  const [selectedBrowser, setSelectedBrowser] = useState(DEFAULT_BROWSER);
@@ -93,18 +94,20 @@ const WebPushPreview = ({
93
94
  buttons={buttons}
94
95
  />
95
96
  <PreviewDisclaimer />
96
- <CapRow className="preview-all-devices-button">
97
- <CapColumn span={24}>
98
- <CapButton
99
- type="link"
100
- onClick={handleOpenPreviewSlideBox}
101
- className="preview-all-devices-trigger"
102
- data-test-id="webpush-preview-toggle"
103
- >
104
- <FormattedMessage {...messages.previewInAllDevices} />
105
- </CapButton>
106
- </CapColumn>
107
- </CapRow>
97
+ {showPreviewInAllDevices && (
98
+ <CapRow className="preview-all-devices-button">
99
+ <CapColumn span={24}>
100
+ <CapButton
101
+ type="link"
102
+ onClick={handleOpenPreviewSlideBox}
103
+ className="preview-all-devices-trigger"
104
+ data-test-id="webpush-preview-toggle"
105
+ >
106
+ <FormattedMessage {...messages.previewInAllDevices} />
107
+ </CapButton>
108
+ </CapColumn>
109
+ </CapRow>
110
+ )}
108
111
  </CapColumn>
109
112
  <CapSlideBox
110
113
  header={<FormattedMessage {...messages.webPushNotificationPreview} />}
@@ -141,6 +144,7 @@ WebPushPreview.propTypes = {
141
144
  })
142
145
  ),
143
146
  onClickShowInAllDevices: PropTypes.func,
147
+ showPreviewInAllDevices: PropTypes.bool,
144
148
  };
145
149
 
146
150
  WebPushPreview.defaultProps = {
@@ -150,6 +154,7 @@ WebPushPreview.defaultProps = {
150
154
  imageSrc: '',
151
155
  brandIconSrc: '',
152
156
  buttons: [],
157
+ showPreviewInAllDevices: true,
153
158
  };
154
159
 
155
160
  export default WebPushPreview;
@@ -109,6 +109,11 @@ describe('WebPushPreview', () => {
109
109
  expect(button.hasClass('preview-all-devices-trigger')).toBe(true);
110
110
  });
111
111
 
112
+ it('should hide "Preview in all devices" button when showPreviewInAllDevices is false', () => {
113
+ const wrapper = mountWithIntl(<WebPushPreview {...defaultProps} showPreviewInAllDevices={false} />);
114
+ expect(wrapper.find('.preview-all-devices-button').exists()).toBe(false);
115
+ });
116
+
112
117
  it('should render CapSlideBox component', () => {
113
118
  const wrapper = mountWithIntl(<WebPushPreview {...defaultProps} />);
114
119
  const allSlideBoxes = wrapper.find(CapSlideBox);
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Tests for the onContentValidityChange reporting wired into WebPushCreate.
3
+ *
4
+ * WebPushCreate is a functional component (not a class like Sms/Create and Sms/Edit), so the
5
+ * dedup pattern is implemented as a useEffect + useRef instead of componentDidUpdate + an
6
+ * instance field, but the contract with the parent (CreativesContainer) is identical:
7
+ * onContentValidityChange({ isContentEmpty }) is called only when isContentEmpty actually
8
+ * changes since the last report, with title AND message both required for WebPush.
9
+ *
10
+ * Note: all relative mock paths are resolved from THIS test file's location
11
+ * (app/v2Containers/WebPush/Create/tests/), not from the component — mirrors
12
+ * testAndPreviewIntegration.test.js in this same directory.
13
+ */
14
+
15
+ import React from 'react';
16
+ import { render, screen, fireEvent } from '@testing-library/react';
17
+ import '@testing-library/jest-dom';
18
+ import { IntlProvider } from 'react-intl';
19
+
20
+ // ── Infrastructure / HOC mocks ──────────────────────────────────────────────
21
+ jest.mock('../../../../hoc/withCreatives', () => ({
22
+ __esModule: true,
23
+ default: ({ WrappedComponent }) => WrappedComponent,
24
+ }));
25
+
26
+ jest.mock('@capillarytech/vulcan-react-sdk/utils', () => ({
27
+ injectReducer: () => (Component) => Component,
28
+ injectSaga: () => (Component) => Component,
29
+ }));
30
+ jest.mock('@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes', () => ({
31
+ DAEMON: 'DAEMON',
32
+ }));
33
+
34
+ // ── Redux slices (paths relative to this test file) ─────────────────────────
35
+ jest.mock('../../../Templates/selectors', () => ({
36
+ makeSelectTemplates: () => () => {},
37
+ }));
38
+ jest.mock('../../../Templates/actions', () => ({}));
39
+ jest.mock('../../selectors', () => ({
40
+ makeSelectWebPush: () => () => ({}),
41
+ makeSelectCreateError: () => () => null,
42
+ makeSelectCreateTemplateInProgress: () => () => false,
43
+ makeSelectEditTemplateInProgress: () => () => false,
44
+ makeSelectEditError: () => () => null,
45
+ }));
46
+ jest.mock('../../actions', () => ({}));
47
+ jest.mock('../../reducer', () => () => ({}));
48
+ jest.mock('../../sagas', () => ({}));
49
+ jest.mock('../../../Templates/sagas', () => ({ v2TemplateSaga: {} }));
50
+ jest.mock('../../../Cap/selectors', () => ({
51
+ makeSelectMetaEntities: () => () => [],
52
+ setInjectedTags: () => () => [],
53
+ }));
54
+ jest.mock('../../../CreativesContainer/constants', () => ({
55
+ WEBPUSH: 'WEBPUSH',
56
+ }));
57
+ jest.mock('../../../CreativesContainer/messages', () => ({
58
+ testAndPreview: { id: 'testAndPreview', defaultMessage: 'Test & Preview' },
59
+ }));
60
+ jest.mock('../../../../utils/common', () => ({
61
+ isAiContentBotDisabled: () => false,
62
+ }));
63
+
64
+ // ── Form section components ──────────────────────────────────────────────────
65
+ // TemplateNameSection stays a stub — it is only relevant in isFullMode, which these tests
66
+ // keep off (mirrors baseProps below) to focus purely on title/message emptiness.
67
+ jest.mock('../components/TemplateNameSection', () => ({
68
+ __esModule: true,
69
+ default: () => <div data-testid="template-name-section" />,
70
+ }));
71
+ // NotificationTitleSection and MessageSection are given lightweight controlled-input stand-ins
72
+ // so tests can drive real onChange handlers (handleNotificationTitleChange / handleMessageChange)
73
+ // inside WebPushCreate, exercising the actual state that feeds the isContentEmpty computation.
74
+ jest.mock('../components/NotificationTitleSection', () => ({
75
+ __esModule: true,
76
+ default: ({ value, onChange }) => (
77
+ <input data-testid="notification-title-input" value={value} onChange={onChange} />
78
+ ),
79
+ }));
80
+ jest.mock('../components/MessageSection', () => ({
81
+ __esModule: true,
82
+ default: ({ value, onChange }) => (
83
+ <textarea data-testid="message-input" value={value} onChange={onChange} />
84
+ ),
85
+ }));
86
+ jest.mock('../components/MediaSection', () => ({
87
+ __esModule: true,
88
+ default: () => <div data-testid="media-section" />,
89
+ }));
90
+ jest.mock('../components/BrandIconSection', () => ({
91
+ __esModule: true,
92
+ default: () => <div data-testid="brand-icon-section" />,
93
+ }));
94
+ jest.mock('../components/ButtonsLinksSection', () => ({
95
+ __esModule: true,
96
+ default: () => <div data-testid="buttons-links-section" />,
97
+ }));
98
+ jest.mock('../components/FormActions', () => ({
99
+ __esModule: true,
100
+ default: () => <div data-testid="form-actions" />,
101
+ }));
102
+ jest.mock('../preview/WebPushPreview', () => ({
103
+ __esModule: true,
104
+ default: () => <div data-testid="webpush-preview" />,
105
+ }));
106
+ jest.mock('../../../TagList', () => ({
107
+ __esModule: true,
108
+ default: () => <div data-testid="tag-list" />,
109
+ }));
110
+
111
+ // ── Hooks ────────────────────────────────────────────────────────────────────
112
+ jest.mock('../hooks/useCharacterCount', () => ({
113
+ useCharacterCount: () => ({ updateCharacterCount: jest.fn() }),
114
+ }));
115
+ jest.mock('../hooks/useButtonManagement', () => ({
116
+ useButtonManagement: () => ({ buttons: [], setButtons: jest.fn(), isAddingButton: false }),
117
+ }));
118
+ jest.mock('../hooks/useImageUpload', () => ({
119
+ useImageUpload: () => ({
120
+ imageSrc: '',
121
+ imageUrl: '',
122
+ isValidating: false,
123
+ isUploading: false,
124
+ setImageSrc: jest.fn(),
125
+ setImageUrl: jest.fn(),
126
+ uploadMethod: 'UPLOAD_IMAGE',
127
+ }),
128
+ }));
129
+ jest.mock('../hooks/useTagManagement', () => ({
130
+ useTagManagement: () => ({ tags: [], extractTags: jest.fn(), handleOnTagsContextChange: jest.fn(), validationConfig: {} }),
131
+ }));
132
+
133
+ jest.mock('../../../../v2Components/TestAndPreviewSlidebox', () => ({
134
+ __esModule: true,
135
+ default: () => <div data-testid="test-and-preview-slidebox" />,
136
+ }));
137
+
138
+ // ── CapUI library ─────────────────────────────────────────────────────────────
139
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => ({
140
+ __esModule: true,
141
+ default: ({ children, className }) => <div className={className}>{children}</div>,
142
+ }));
143
+ jest.mock('@capillarytech/cap-ui-library/CapColumn', () => ({
144
+ __esModule: true,
145
+ default: ({ children, className }) => <div className={className}>{children}</div>,
146
+ }));
147
+ jest.mock('@capillarytech/cap-ui-library/CapButton', () => ({
148
+ __esModule: true,
149
+ default: ({ children, onClick, className, type }) => (
150
+ <button className={className} onClick={onClick} data-type={type}>
151
+ {children}
152
+ </button>
153
+ ),
154
+ }));
155
+
156
+ // ── Import component AFTER mocks ─────────────────────────────────────────────
157
+ // eslint-disable-next-line import/first
158
+ import WebPushCreateDefault from '../index';
159
+
160
+ const baseProps = () => ({
161
+ isFullMode: false,
162
+ handleClose: jest.fn(),
163
+ webPushActions: {
164
+ createTemplate: jest.fn(),
165
+ editTemplate: jest.fn(),
166
+ uploadImage: jest.fn(),
167
+ clearCreateError: jest.fn(),
168
+ clearEditError: jest.fn(),
169
+ clearCreateResponse: jest.fn(),
170
+ clearEditResponse: jest.fn(),
171
+ setSelectedAccount: jest.fn(),
172
+ clearState: jest.fn(),
173
+ },
174
+ createTemplateInProgress: false,
175
+ createTemplateError: null,
176
+ editTemplateInProgress: false,
177
+ editTemplateError: null,
178
+ accountData: null,
179
+ webPush: {},
180
+ onCreateComplete: jest.fn(),
181
+ getFormData: jest.fn(),
182
+ isGetFormData: false,
183
+ templateData: null,
184
+ creativesMode: 'create',
185
+ params: {},
186
+ globalActions: { getMetaEntities: jest.fn() },
187
+ location: { pathname: '/webpush', query: {}, search: '', state: {} },
188
+ metaEntities: [],
189
+ injectedTags: [],
190
+ getDefaultTags: jest.fn(),
191
+ supportedTags: [],
192
+ forwardedTags: [],
193
+ selectedOfferDetails: [],
194
+ eventContextTags: [],
195
+ waitEventContextTags: {},
196
+ templateActions: {},
197
+ Templates: { templates: [] },
198
+ restrictPersonalization: false,
199
+ });
200
+
201
+ const TestWrapper = ({ children }) => (
202
+ <IntlProvider locale="en" messages={{ testAndPreview: 'Test & Preview' }}>
203
+ {children}
204
+ </IntlProvider>
205
+ );
206
+
207
+ describe('WebPushCreate — onContentValidityChange reporting', () => {
208
+ it('reports isContentEmpty: true on mount when title and message are both empty', () => {
209
+ const onContentValidityChange = jest.fn();
210
+ render(
211
+ <TestWrapper>
212
+ <WebPushCreateDefault {...baseProps()} onContentValidityChange={onContentValidityChange} />
213
+ </TestWrapper>,
214
+ );
215
+
216
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
217
+ });
218
+
219
+ it('reports isContentEmpty: false once both title and message have content', () => {
220
+ const onContentValidityChange = jest.fn();
221
+ render(
222
+ <TestWrapper>
223
+ <WebPushCreateDefault {...baseProps()} onContentValidityChange={onContentValidityChange} />
224
+ </TestWrapper>,
225
+ );
226
+ onContentValidityChange.mockClear();
227
+
228
+ fireEvent.change(screen.getByTestId('notification-title-input'), { target: { value: 'Hello' } });
229
+ fireEvent.change(screen.getByTestId('message-input'), { target: { value: 'World' } });
230
+
231
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: false });
232
+ });
233
+
234
+ it('does not re-report (still empty) if only the title is filled in while message stays empty', () => {
235
+ const onContentValidityChange = jest.fn();
236
+ render(
237
+ <TestWrapper>
238
+ <WebPushCreateDefault {...baseProps()} onContentValidityChange={onContentValidityChange} />
239
+ </TestWrapper>,
240
+ );
241
+ // Mount already reported isContentEmpty: true (both fields blank).
242
+ onContentValidityChange.mockClear();
243
+
244
+ fireEvent.change(screen.getByTestId('notification-title-input'), { target: { value: 'Hello' } });
245
+
246
+ // isContentEmpty is still true (message is required too and remains blank), so the dedup
247
+ // guard must suppress a duplicate report.
248
+ expect(onContentValidityChange).not.toHaveBeenCalled();
249
+ });
250
+
251
+ it('re-reports isContentEmpty: true when a previously-filled message is cleared out', () => {
252
+ const onContentValidityChange = jest.fn();
253
+ render(
254
+ <TestWrapper>
255
+ <WebPushCreateDefault {...baseProps()} onContentValidityChange={onContentValidityChange} />
256
+ </TestWrapper>,
257
+ );
258
+ fireEvent.change(screen.getByTestId('notification-title-input'), { target: { value: 'Hello' } });
259
+ fireEvent.change(screen.getByTestId('message-input'), { target: { value: 'World' } });
260
+ onContentValidityChange.mockClear();
261
+
262
+ fireEvent.change(screen.getByTestId('message-input'), { target: { value: '' } });
263
+
264
+ expect(onContentValidityChange).toHaveBeenCalledWith({ isContentEmpty: true });
265
+ });
266
+
267
+ it('does not throw and does not report when onContentValidityChange is not provided', () => {
268
+ expect(() => {
269
+ render(
270
+ <TestWrapper>
271
+ <WebPushCreateDefault {...baseProps()} />
272
+ </TestWrapper>,
273
+ );
274
+ fireEvent.change(screen.getByTestId('notification-title-input'), { target: { value: 'Hello' } });
275
+ }).not.toThrow();
276
+ });
277
+
278
+ it('does not dispatch again on a re-render that does not change isContentEmpty (dedup guard)', () => {
279
+ const onContentValidityChange = jest.fn();
280
+ render(
281
+ <TestWrapper>
282
+ <WebPushCreateDefault {...baseProps()} onContentValidityChange={onContentValidityChange} />
283
+ </TestWrapper>,
284
+ );
285
+ fireEvent.change(screen.getByTestId('notification-title-input'), { target: { value: 'Hello' } });
286
+ fireEvent.change(screen.getByTestId('message-input'), { target: { value: 'World' } });
287
+ onContentValidityChange.mockClear();
288
+
289
+ // Further edits that keep both fields non-empty must not re-trigger the callback.
290
+ fireEvent.change(screen.getByTestId('message-input'), { target: { value: 'World!' } });
291
+
292
+ expect(onContentValidityChange).not.toHaveBeenCalled();
293
+ });
294
+ });
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react/jsx-boolean-value */
2
2
  /* eslint-disable camelcase */
3
3
  /* eslint-disable no-undef */
4
- import React, { useState, useEffect, useCallback } from 'react';
4
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
5
5
  import { bindActionCreators } from 'redux';
6
6
  import { createStructuredSelector } from 'reselect';
7
7
  import { injectIntl, FormattedMessage } from 'react-intl';
@@ -119,7 +119,7 @@ import { ANDROID } from '../../v2Components/CommonTestAndPreview/constants';
119
119
  import CapImageUpload from '../../v2Components/CapImageUpload';
120
120
  import TagList from '../TagList';
121
121
  import { validateTags } from '../../utils/tagValidations';
122
- import { splitContentByOrderedVarTokens } from '../../utils/templateVarUtils';
122
+ import { splitContentByOrderedVarTokens, reconcileVarMapToSlotFormat } from '../../utils/templateVarUtils';
123
123
  import { capitalizeString } from '../../utils/Formatter';
124
124
  import CapWhatsappCTA from '../../v2Components/CapWhatsappCTA';
125
125
  import {
@@ -188,6 +188,7 @@ export const Whatsapp = (props) => {
188
188
  handleTestAndPreview: propsHandleTestAndPreview,
189
189
  handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
190
190
  isTestAndPreviewMode: propsIsTestAndPreviewMode,
191
+ onContentValidityChange,
191
192
  } = props || {};
192
193
  const { formatMessage } = intl;
193
194
  const { TextArea } = CapInput;
@@ -504,7 +505,8 @@ export const Whatsapp = (props) => {
504
505
  if (templateHeaderArray?.length !== 0) {
505
506
  let clonedVarMap = {};
506
507
  if (!isEmpty(varMap)) {
507
- clonedVarMap = cloneDeep(varMap);
508
+ // Reconcile CCS sequential varMapped indices to the UI’s `${token}_${index}` slot keys so values map to the correct segments.
509
+ clonedVarMap = reconcileVarMapToSlotFormat(varMap, templateHeaderArray, regex);
508
510
  } else {
509
511
  templateHeaderArray?.forEach((headerValue, i) => {
510
512
  if (headerValue?.match(regex)?.length > 0) {
@@ -562,7 +564,9 @@ export const Whatsapp = (props) => {
562
564
  if (tempMsgArray.length !== 0) {
563
565
  const { varMapped = {} } = editContent;
564
566
  if (!isEmpty(varMapped)) {
565
- varMap = cloneDeep(varMapped);
567
+ // CCS's varMapped can use plain sequential occurrence indices ("0","1",...)
568
+ // rather than this UI's own `${token}_${index}` slot keys
569
+ varMap = reconcileVarMapToSlotFormat(varMapped, tempMsgArray, validVarRegex);
566
570
  } else {
567
571
  //computing and setting varMap for first edit
568
572
  for (let i = 0; i < tempMsgArray.length; i += 1) {
@@ -701,6 +705,23 @@ export const Whatsapp = (props) => {
701
705
  }
702
706
  }, [carouselValidateTag, tags])
703
707
 
708
+ // Reports live "is content empty" validity to the parent slidebox so it can
709
+ // disable the Done/Preview-and-test buttons whenever the template message
710
+ // (the one field that is always required, regardless of template type) is
711
+ // cleared out. Mirrors the Sms Create/Edit componentDidUpdate pattern; the
712
+ // ref-based dedup avoids redundant parent setState calls / render loops.
713
+ const lastReportedTemplateMessageEmptyRef = useRef(undefined);
714
+ useEffect(() => {
715
+ if (typeof onContentValidityChange !== 'function') {
716
+ return;
717
+ }
718
+ const isContentEmpty = !templateMessage?.trim();
719
+ if (lastReportedTemplateMessageEmptyRef.current !== isContentEmpty) {
720
+ lastReportedTemplateMessageEmptyRef.current = isContentEmpty;
721
+ onContentValidityChange({ isContentEmpty });
722
+ }
723
+ }, [templateMessage, onContentValidityChange]);
724
+
704
725
  const handleOnTagsContextChange = (data) => {
705
726
  const { type } = location.query || {};
706
727
  const isEmbedded = type === EMBEDDED;