@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
@@ -181,6 +181,8 @@ export function SlideBoxContent(props) {
181
181
  onPersonalizationTokensChange,
182
182
  isTestAndPreviewMode,
183
183
  onHtmlEditorValidationStateChange,
184
+ onEmbeddedSmsFooterValidity,
185
+ onContentValidityChange,
184
186
  } = props;
185
187
  const localTemplatesConfig = props.localTemplatesConfig || pick(props, constants.LOCAL_TEMPLATE_CONFIG_KEYS);
186
188
  const useLocalTemplates = !!get(localTemplatesConfig, 'useLocalTemplates');
@@ -456,6 +458,7 @@ export function SlideBoxContent(props) {
456
458
  eventContextTags,
457
459
  waitEventContextTags,
458
460
  handleClose,
461
+ onContentValidityChange,
459
462
  };
460
463
 
461
464
  return (
@@ -602,6 +605,7 @@ export function SlideBoxContent(props) {
602
605
  handleCloseTestAndPreview={handleCloseTestAndPreview}
603
606
  isTestAndPreviewMode={isTestAndPreviewMode}
604
607
  onValidationFail={onValidationFail}
608
+ onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity}
605
609
  />
606
610
  )}
607
611
  {isEditFTP && (
@@ -644,6 +648,7 @@ export function SlideBoxContent(props) {
644
648
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
645
649
  handleTestAndPreview={handleTestAndPreview}
646
650
  handleCloseTestAndPreview={handleCloseTestAndPreview}
651
+ onContentValidityChange={onContentValidityChange}
647
652
  />
648
653
  }
649
654
  {
@@ -667,6 +672,7 @@ export function SlideBoxContent(props) {
667
672
  templateData={templateData}
668
673
  isGetFormData={isGetFormData}
669
674
  getFormSubscriptionData={getFormData}
675
+ onEmbeddedSmsFooterValidity={onEmbeddedSmsFooterValidity}
670
676
  getLiquidTags={getLiquidTags}
671
677
  getDefaultTags={type}
672
678
  isFullMode={isFullMode}
@@ -866,6 +872,7 @@ export function SlideBoxContent(props) {
866
872
  restrictPersonalization={restrictPersonalization}
867
873
  isAnonymousType={isAnonymousType}
868
874
  onPersonalizationTokensChange={onPersonalizationTokensChange}
875
+ onContentValidityChange={onContentValidityChange}
869
876
  />
870
877
  ) : (
871
878
  <MobilePushNew
@@ -897,6 +904,7 @@ export function SlideBoxContent(props) {
897
904
  restrictPersonalization={restrictPersonalization}
898
905
  isAnonymousType={isAnonymousType}
899
906
  onPersonalizationTokensChange={onPersonalizationTokensChange}
907
+ onContentValidityChange={onContentValidityChange}
900
908
  />
901
909
  )
902
910
  )}
@@ -939,6 +947,7 @@ export function SlideBoxContent(props) {
939
947
  restrictPersonalization={restrictPersonalization}
940
948
  isAnonymousType={isAnonymousType}
941
949
  onPersonalizationTokensChange={onPersonalizationTokensChange}
950
+ onContentValidityChange={onContentValidityChange}
942
951
  />
943
952
  ) : (
944
953
  <MobilePushNew
@@ -976,6 +985,7 @@ export function SlideBoxContent(props) {
976
985
  creativesMode={creativesMode}
977
986
  restrictPersonalization={restrictPersonalization}
978
987
  isAnonymousType={isAnonymousType}
988
+ onContentValidityChange={onContentValidityChange}
979
989
  />
980
990
  )
981
991
  )}
@@ -1085,6 +1095,7 @@ export function SlideBoxContent(props) {
1085
1095
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
1086
1096
  handleTestAndPreview={handleTestAndPreview}
1087
1097
  handleCloseTestAndPreview={handleCloseTestAndPreview}
1098
+ onContentValidityChange={onContentValidityChange}
1088
1099
  createNew/>
1089
1100
  )}
1090
1101
 
@@ -1107,6 +1118,7 @@ export function SlideBoxContent(props) {
1107
1118
  eventContextTags={eventContextTags}
1108
1119
  waitEventContextTags={waitEventContextTags}
1109
1120
  showLiquidErrorInFooter={showLiquidErrorInFooter}
1121
+ onContentValidityChange={onContentValidityChange}
1110
1122
  createNew/> }
1111
1123
 
1112
1124
  {isCreateWhatsapp && (<Whatsapp
@@ -1120,6 +1132,7 @@ export function SlideBoxContent(props) {
1120
1132
  handleTestAndPreview={handleTestAndPreview}
1121
1133
  handleCloseTestAndPreview={handleCloseTestAndPreview}
1122
1134
  isTestAndPreviewMode={isTestAndPreviewMode}
1135
+ onContentValidityChange={onContentValidityChange}
1123
1136
  />
1124
1137
  )}
1125
1138
 
@@ -1145,6 +1158,7 @@ export function SlideBoxContent(props) {
1145
1158
  search: '',
1146
1159
  }}
1147
1160
  showLiquidErrorInFooter={showLiquidErrorInFooter}
1161
+ onContentValidityChange={onContentValidityChange}
1148
1162
  />
1149
1163
  )}
1150
1164
 
@@ -1270,6 +1284,7 @@ export function SlideBoxContent(props) {
1270
1284
  restrictPersonalization={restrictPersonalization}
1271
1285
  isAnonymousType={isAnonymousType}
1272
1286
  waitEventContextTags={waitEventContextTags}
1287
+ onContentValidityChange={onContentValidityChange}
1273
1288
  />
1274
1289
  )}
1275
1290
  {isCreateRcs && (<Rcs
@@ -51,6 +51,8 @@ function SlideBoxFooter(props) {
51
51
  hasPersonalizationTokenError: hasPersonalizationTokenErrorProp = false,
52
52
  /** When set (e.g. SMS library create), overrides `creativesTemplatesSave` (“Done”) for the primary button */
53
53
  primarySaveButtonMessage,
54
+ /** Live content-validity signal reported by the channel's own editor (currently SMS create) — true disables Save/Done. */
55
+ isContentInvalid = false,
54
56
  } = props;
55
57
  // Calculate if buttons should be disabled
56
58
  // Only apply validation state checks for EMAIL channel in HTML Editor mode (not BEE/DragDrop)
@@ -193,7 +195,7 @@ function SlideBoxFooter(props) {
193
195
  <CapRow useLegacy>
194
196
  <CapButton
195
197
  onClick={onSave}
196
- disabled={isTemplateNameEmpty || fetchingCmsData || shouldDisableButtons || hasPersonalizationTokenError}
198
+ disabled={isTemplateNameEmpty || fetchingCmsData || shouldDisableButtons || hasPersonalizationTokenError || isContentInvalid}
197
199
  >
198
200
  {getSaveButtonLabel()}
199
201
  </CapButton>
@@ -201,7 +203,7 @@ function SlideBoxFooter(props) {
201
203
  <CapButton
202
204
  type="secondary"
203
205
  onClick={onTestAndPreview}
204
- disabled={shouldDisableButtons || hasPersonalizationTokenError}
206
+ disabled={shouldDisableButtons || hasPersonalizationTokenError || isContentInvalid}
205
207
  style={{ marginLeft: '8px' }}
206
208
  >
207
209
  <FormattedMessage {...messages.testAndPreview} />
@@ -272,6 +274,7 @@ SlideBoxFooter.propTypes = {
272
274
  id: PropTypes.string,
273
275
  defaultMessage: PropTypes.string,
274
276
  }),
277
+ isContentInvalid: PropTypes.bool,
275
278
  };
276
279
 
277
280
  SlideBoxFooter.defaultProps = {
@@ -300,5 +303,6 @@ SlideBoxFooter.defaultProps = {
300
303
  formData: [],
301
304
  hasPersonalizationTokenError: false,
302
305
  primarySaveButtonMessage: undefined,
306
+ isContentInvalid: false,
303
307
  };
304
308
  export default SlideBoxFooter;
@@ -164,7 +164,6 @@ export class Creatives extends React.Component {
164
164
  isFullMode: props.isFullMode,
165
165
  useLocalTemplates,
166
166
  });
167
-
168
167
  this.state = {
169
168
  isLoadingContent: true,
170
169
  templateStep: 1, //modeSelection: for template name and (for email selecting upload or editor), and createTemplate to create template create based on modeSelection
@@ -198,6 +197,9 @@ export class Creatives extends React.Component {
198
197
  errorsAcknowledged: false, // Flag to track if user has acknowledged errors by clicking redirection icon
199
198
  },
200
199
  hasPersonalizationTokenError: false, // Track personalization token errors in form
200
+ // Live content-validity signal for channels that report it (currently SMS create) —
201
+ // false by default so unwired channels are never blocked by this flag.
202
+ isContentInvalid: false,
201
203
  };
202
204
  this.creativesTemplateSteps = {
203
205
  1: 'modeSelection',
@@ -413,7 +415,7 @@ export class Creatives extends React.Component {
413
415
  this.setState({ isGetFormData: false });
414
416
  };
415
417
 
416
- mapCarouselDataToCreatives = (cards) => cards.map((card) => {
418
+ mapCarouselDataToCreatives = (cards) => (cards || []).map((card) => {
417
419
  const {
418
420
  cardVarMapped, bodyTemplate, media, buttons, mediaType,
419
421
  } = card || {};
@@ -1894,6 +1896,26 @@ export class Creatives extends React.Component {
1894
1896
  this.setState({ isDiscardMessage: true });
1895
1897
  }
1896
1898
 
1899
+ // Reported live by SMS's own content editor (Sms/Create and Sms/Edit's componentDidUpdate)
1900
+ // so the Done/Save button reflects an empty/invalid form immediately, instead of only
1901
+ // failing silently on click.
1902
+ handleEmbeddedSmsFooterValidity = (validity) => {
1903
+ const isContentInvalid = !!validity?.isMessageEmpty;
1904
+ if (this.state.isContentInvalid !== isContentInvalid) {
1905
+ this.setState({ isContentInvalid });
1906
+ }
1907
+ }
1908
+
1909
+ // Generic counterpart of the above for every other channel wrapper (Email, MobilePush,
1910
+ // Zalo, WebPush, RCS, WhatsApp, Viber) — each reports { isContentEmpty } live from its own
1911
+ // form state so the Done/Preview-and-test buttons disable immediately, not just on click.
1912
+ handleContentValidityChange = (validity) => {
1913
+ const isContentInvalid = !!validity?.isContentEmpty;
1914
+ if (this.state.isContentInvalid !== isContentInvalid) {
1915
+ this.setState({ isContentInvalid });
1916
+ }
1917
+ }
1918
+
1897
1919
 
1898
1920
  // NEW: Handler for Test and Preview button
1899
1921
  handleTestAndPreview = () => {
@@ -2214,6 +2236,7 @@ export class Creatives extends React.Component {
2214
2236
  isTestAndPreviewMode,
2215
2237
  inAppEditorType,
2216
2238
  htmlEditorValidationState,
2239
+ isContentInvalid,
2217
2240
  } = this.state;
2218
2241
  const useLocalTemplates = get(
2219
2242
  this.props,
@@ -2411,6 +2434,8 @@ export class Creatives extends React.Component {
2411
2434
  handleCloseTestAndPreview={this.handleCloseTestAndPreview}
2412
2435
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2413
2436
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2437
+ onEmbeddedSmsFooterValidity={this.handleEmbeddedSmsFooterValidity}
2438
+ onContentValidityChange={this.handleContentValidityChange}
2414
2439
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2415
2440
  localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2416
2441
  />
@@ -2433,6 +2458,7 @@ export class Creatives extends React.Component {
2433
2458
  shouldShowDoneFooter={this.shouldShowDoneFooter}
2434
2459
  fetchingCmsData={fetchingCmsData}
2435
2460
  isTemplateNameEmpty={isTemplateNameEmpty}
2461
+ isContentInvalid={isContentInvalid}
2436
2462
  isLiquidValidationError={isLiquidValidationError}
2437
2463
  errorMessages={liquidErrorMessage}
2438
2464
  currentTab={activeFormBuilderTab}
@@ -2462,7 +2488,7 @@ export class Creatives extends React.Component {
2462
2488
  )}
2463
2489
  handleClose={this.handleCloseSlideBox}
2464
2490
  show={showSlideBox}
2465
- size="size-xl"
2491
+ size="size-l"
2466
2492
  />
2467
2493
  </SlideBoxWrapper>
2468
2494
  );
@@ -658,4 +658,33 @@ describe('SlideBoxFooter — isBEEEditor detection in create mode via emailCreat
658
658
  expect(screen.getByRole('button', { name: /done/i })).toBeInTheDocument();
659
659
  });
660
660
  });
661
+ });
662
+
663
+ describe('SlideBoxFooter — isContentInvalid (live content-validity signal, e.g. empty SMS body)', () => {
664
+ it('disables Save/Done when isContentInvalid is true, independent of currentChannel/htmlEditorValidationState', () => {
665
+ renderComponent({
666
+ ...baseFooterProps,
667
+ currentChannel: 'SMS',
668
+ isContentInvalid: true,
669
+ });
670
+ expect(screen.getByRole('button', { name: /update/i })).toBeDisabled();
671
+ });
672
+
673
+ it('disables the Test & Preview button when isContentInvalid is true', () => {
674
+ renderComponent({
675
+ ...baseFooterProps,
676
+ currentChannel: 'SMS',
677
+ isContentInvalid: true,
678
+ showTestAndPreviewButton: true,
679
+ });
680
+ expect(screen.getByRole('button', { name: /preview.*test/i })).toBeDisabled();
681
+ });
682
+
683
+ it('leaves Save/Done enabled when isContentInvalid is false (default)', () => {
684
+ renderComponent({
685
+ ...baseFooterProps,
686
+ currentChannel: 'SMS',
687
+ });
688
+ expect(screen.getByRole('button', { name: /update/i })).not.toBeDisabled();
689
+ });
661
690
  });
@@ -28,10 +28,12 @@ exports[`Test SlideBoxContent container campaign message, add creative click rcs
28
28
  }
29
29
  onCallTaskSubmit={[Function]}
30
30
  onChannelChange={[Function]}
31
+ onContentValidityChange={[Function]}
31
32
  onCreateComplete={[MockFunction]}
32
33
  onCreateNew={[Function]}
33
34
  onCreateNextStep={[Function]}
34
35
  onEmailModeChange={[Function]}
36
+ onEmbeddedSmsFooterValidity={[Function]}
35
37
  onEnterTemplateName={[Function]}
36
38
  onFTPSubmit={[MockFunction]}
37
39
  onFacebookSubmit={[MockFunction]}
@@ -89,7 +91,7 @@ exports[`Test SlideBoxContent container campaign message, add creative click rcs
89
91
  />
90
92
  }
91
93
  show={true}
92
- size="size-xl"
94
+ size="size-l"
93
95
  />
94
96
  </CreativesSlideBoxWrapper>
95
97
  `;
@@ -122,10 +124,12 @@ exports[`Test SlideBoxContent container campaign message, add creative click wha
122
124
  }
123
125
  onCallTaskSubmit={[Function]}
124
126
  onChannelChange={[Function]}
127
+ onContentValidityChange={[Function]}
125
128
  onCreateComplete={[MockFunction]}
126
129
  onCreateNew={[Function]}
127
130
  onCreateNextStep={[Function]}
128
131
  onEmailModeChange={[Function]}
132
+ onEmbeddedSmsFooterValidity={[Function]}
129
133
  onEnterTemplateName={[Function]}
130
134
  onFTPSubmit={[MockFunction]}
131
135
  onFacebookSubmit={[MockFunction]}
@@ -183,7 +187,7 @@ exports[`Test SlideBoxContent container campaign message, add creative click wha
183
187
  />
184
188
  }
185
189
  show={true}
186
- size="size-xl"
190
+ size="size-l"
187
191
  />
188
192
  </CreativesSlideBoxWrapper>
189
193
  `;
@@ -216,10 +220,12 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit all data
216
220
  }
217
221
  onCallTaskSubmit={[Function]}
218
222
  onChannelChange={[Function]}
223
+ onContentValidityChange={[Function]}
219
224
  onCreateComplete={[MockFunction]}
220
225
  onCreateNew={[Function]}
221
226
  onCreateNextStep={[Function]}
222
227
  onEmailModeChange={[Function]}
228
+ onEmbeddedSmsFooterValidity={[Function]}
223
229
  onEnterTemplateName={[Function]}
224
230
  onFTPSubmit={[MockFunction]}
225
231
  onFacebookSubmit={[MockFunction]}
@@ -281,6 +287,7 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit all data
281
287
  }
282
288
  }
283
289
  isAnonymousType={false}
290
+ isContentInvalid={false}
284
291
  isContinueButtonDisabled={false}
285
292
  isCreatingTemplate={false}
286
293
  isEmptyContent={false}
@@ -320,7 +327,7 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit all data
320
327
  />
321
328
  }
322
329
  show={false}
323
- size="size-xl"
330
+ size="size-l"
324
331
  />
325
332
  </CreativesSlideBoxWrapper>
326
333
  `;
@@ -353,10 +360,12 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit min data
353
360
  }
354
361
  onCallTaskSubmit={[Function]}
355
362
  onChannelChange={[Function]}
363
+ onContentValidityChange={[Function]}
356
364
  onCreateComplete={[MockFunction]}
357
365
  onCreateNew={[Function]}
358
366
  onCreateNextStep={[Function]}
359
367
  onEmailModeChange={[Function]}
368
+ onEmbeddedSmsFooterValidity={[Function]}
360
369
  onEnterTemplateName={[Function]}
361
370
  onFTPSubmit={[MockFunction]}
362
371
  onFacebookSubmit={[MockFunction]}
@@ -418,6 +427,7 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit min data
418
427
  }
419
428
  }
420
429
  isAnonymousType={false}
430
+ isContentInvalid={false}
421
431
  isContinueButtonDisabled={false}
422
432
  isCreatingTemplate={false}
423
433
  isEmptyContent={false}
@@ -457,7 +467,7 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit min data
457
467
  />
458
468
  }
459
469
  show={false}
460
- size="size-xl"
470
+ size="size-l"
461
471
  />
462
472
  </CreativesSlideBoxWrapper>
463
473
  `;
@@ -490,10 +500,12 @@ exports[`Test SlideBoxContent container it should clear the url, on channel chan
490
500
  }
491
501
  onCallTaskSubmit={[Function]}
492
502
  onChannelChange={[Function]}
503
+ onContentValidityChange={[Function]}
493
504
  onCreateComplete={[MockFunction]}
494
505
  onCreateNew={[Function]}
495
506
  onCreateNextStep={[Function]}
496
507
  onEmailModeChange={[Function]}
508
+ onEmbeddedSmsFooterValidity={[Function]}
497
509
  onEnterTemplateName={[Function]}
498
510
  onFTPSubmit={[MockFunction]}
499
511
  onFacebookSubmit={[MockFunction]}
@@ -555,6 +567,7 @@ exports[`Test SlideBoxContent container it should clear the url, on channel chan
555
567
  }
556
568
  }
557
569
  isAnonymousType={false}
570
+ isContentInvalid={false}
558
571
  isContinueButtonDisabled={false}
559
572
  isCreatingTemplate={false}
560
573
  isEmptyContent={false}
@@ -594,7 +607,7 @@ exports[`Test SlideBoxContent container it should clear the url, on channel chan
594
607
  />
595
608
  }
596
609
  show={false}
597
- size="size-xl"
610
+ size="size-l"
598
611
  />
599
612
  </CreativesSlideBoxWrapper>
600
613
  `;
@@ -10,6 +10,7 @@ const {
10
10
  whatsappGetCreativeData2,
11
11
  whatsappGetTemplateData1,
12
12
  whatsappGetTemplateData2,
13
+ whatsappGetTemplateDataNullCards,
13
14
  rcsTemplates,
14
15
  rcsEditTemplateData,
15
16
  smsEditTemplateData,
@@ -77,6 +78,17 @@ describe('Test SlideBoxContent container', () => {
77
78
  expect(handleCloseCreatives).toHaveBeenCalledWith(true);
78
79
  });
79
80
 
81
+ it('does not throw when templateConfigs.cards is explicitly null (regression: CCS sends null rather than omitting the field on non-carousel WhatsApp templates, and mapCarouselDataToCreatives called .map() straight off it)', () => {
82
+ expect(() =>
83
+ renderFunction(
84
+ 'WHATSAPP',
85
+ 'editTemplate',
86
+ whatsappTemplates,
87
+ whatsappGetTemplateDataNullCards,
88
+ ),
89
+ ).not.toThrow();
90
+ });
91
+
80
92
  it('it should clear the url, on channel change from new whatsapp to another', () => {
81
93
  renderFunction(
82
94
  'WHATSAPP',
@@ -30,7 +30,7 @@ import getEventsMap from '../eventsMap';
30
30
  import { GA } from '@capillarytech/cap-ui-utils';
31
31
  import { CREATE, TRACK_CREATE_MPUSH } from '../../App/constants';
32
32
  import { MOBILE_PUSH } from '../../CreativesContainer/constants';
33
- import { getContent } from '../commonMethods';
33
+ import { getContent, getMobilePushEmbeddedContentValidity } from '../commonMethods';
34
34
  import { getCdnUrl } from '../../../utils/cdnTransformation';
35
35
  import injectReducer from '../../../utils/injectReducer';
36
36
  import injectSaga from '../../../utils/injectSaga';
@@ -67,6 +67,22 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
67
67
  }
68
68
  this.hasFetchedInitialTagsRef = false;
69
69
  this.lastFetchedTagContextRef = null;
70
+ // Tracks the last content-emptiness value reported to the parent so componentDidUpdate
71
+ // does not dispatch on every render. Intentionally undefined (not true) so the first
72
+ // render always reports the real validity rather than assuming the form starts invalid.
73
+ this._lastReportedMobilePushContentEmpty = undefined;
74
+ }
75
+ componentDidUpdate() {
76
+ // Reports live form validity so the host (CreativesContainer) can disable Done/Preview
77
+ // and Test as soon as the message content is cleared — mirrors Sms/Create's own reporting.
78
+ if (typeof this.props.onContentValidityChange !== 'function') {
79
+ return;
80
+ }
81
+ const validity = getMobilePushEmbeddedContentValidity(this.state?.formData, this.state?.tabCount);
82
+ const isContentEmpty = !!validity.isContentEmpty;
83
+ if (this._lastReportedMobilePushContentEmpty === isContentEmpty) return;
84
+ this._lastReportedMobilePushContentEmpty = isContentEmpty;
85
+ this.props.onContentValidityChange({ isContentEmpty });
70
86
  }
71
87
  componentWillMount = () => {
72
88
  if (this.props.route.name === 'view') {
@@ -2072,6 +2088,7 @@ Create.propTypes = {
2072
2088
  getFormLibraryData: PropTypes.func,
2073
2089
  onPreviewContentClicked: PropTypes.func,
2074
2090
  onTestContentClicked: PropTypes.func,
2091
+ onContentValidityChange: PropTypes.func,
2075
2092
  eventContextTags: PropTypes.array,
2076
2093
  waitEventContextTags: PropTypes.object,
2077
2094
  getLiquidTags: PropTypes.func,
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { shallow } from 'enzyme';
3
+ import { Create } from '../index';
4
+
5
+ // Shallow-render the plain class (bypassing the withCreatives/Redux/saga HOC) so this test
6
+ // only exercises the componentDidUpdate → onContentValidityChange wiring in isolation.
7
+ // Mirrors app/v2Containers/Sms/Create/index.js's own reporting and
8
+ // app/v2Containers/MobilePush/Edit/test/contentValidity.test.js.
9
+
10
+ const baseProps = () => ({
11
+ actions: {},
12
+ globalActions: {
13
+ fetchSchemaForEntity: jest.fn(),
14
+ },
15
+ Templates: { selectedWeChatAccount: { id: 'acc-1' } },
16
+ Create: {},
17
+ params: {},
18
+ location: { query: { type: 'library', module: 'default' } },
19
+ route: { name: 'mobilepush' },
20
+ router: { push: jest.fn() },
21
+ metaEntities: {},
22
+ intl: { formatMessage: (m) => (m && (m.defaultMessage || m.id)) || '' },
23
+ isFullMode: false,
24
+ });
25
+
26
+ describe('MobilePush/Create — onContentValidityChange reporting', () => {
27
+ it('reports isContentEmpty: true on mount when both Android title and message are empty', () => {
28
+ const onContentValidityChange = jest.fn();
29
+ const wrapper = shallow(<Create {...baseProps()} onContentValidityChange={onContentValidityChange} />);
30
+ wrapper.setState({
31
+ formData: { 0: { 'message-title': '', 'message-editor': '' } },
32
+ tabCount: 1,
33
+ });
34
+
35
+ expect(onContentValidityChange).toHaveBeenCalledWith(
36
+ expect.objectContaining({ isContentEmpty: true }),
37
+ );
38
+ });
39
+
40
+ it('reports isContentEmpty: false once the Android message has content', () => {
41
+ const onContentValidityChange = jest.fn();
42
+ const wrapper = shallow(<Create {...baseProps()} onContentValidityChange={onContentValidityChange} />);
43
+ wrapper.setState({
44
+ formData: { 0: { 'message-title': '', 'message-editor': '' } },
45
+ tabCount: 1,
46
+ });
47
+ onContentValidityChange.mockClear();
48
+
49
+ wrapper.setState({
50
+ formData: { 0: { 'message-title': 'Hello', 'message-editor': 'World' } },
51
+ tabCount: 1,
52
+ });
53
+
54
+ expect(onContentValidityChange).toHaveBeenCalledWith(
55
+ expect.objectContaining({ isContentEmpty: false }),
56
+ );
57
+ });
58
+
59
+ it('re-reports empty when a previously entered message is cleared out', () => {
60
+ const onContentValidityChange = jest.fn();
61
+ const wrapper = shallow(<Create {...baseProps()} onContentValidityChange={onContentValidityChange} />);
62
+ wrapper.setState({
63
+ formData: { 0: { 'message-title': 'Existing', 'message-editor': 'content' } },
64
+ tabCount: 1,
65
+ });
66
+ onContentValidityChange.mockClear();
67
+
68
+ wrapper.setState({
69
+ formData: { 0: { 'message-title': '', 'message-editor': '' } },
70
+ tabCount: 1,
71
+ });
72
+
73
+ expect(onContentValidityChange).toHaveBeenCalledWith(
74
+ expect.objectContaining({ isContentEmpty: true }),
75
+ );
76
+ });
77
+
78
+ it('does not throw and does not report when onContentValidityChange is not provided', () => {
79
+ const wrapper = shallow(<Create {...baseProps()} />);
80
+ expect(() => {
81
+ wrapper.setState({ formData: { 0: { 'message-title': '', 'message-editor': '' } }, tabCount: 1 });
82
+ }).not.toThrow();
83
+ });
84
+
85
+ it('does not dispatch again on a re-render that does not change validity (dedup guard)', () => {
86
+ const onContentValidityChange = jest.fn();
87
+ const wrapper = shallow(<Create {...baseProps()} onContentValidityChange={onContentValidityChange} />);
88
+ wrapper.setState({ formData: { 0: { 'message-title': '', 'message-editor': '' } }, tabCount: 1 });
89
+ onContentValidityChange.mockClear();
90
+
91
+ // Unrelated state change — content emptiness is unchanged.
92
+ wrapper.setState({ currentTab: 1 });
93
+
94
+ expect(onContentValidityChange).not.toHaveBeenCalled();
95
+ });
96
+ });
@@ -28,7 +28,7 @@ import withCreatives from '../../../hoc/withCreatives';
28
28
  import { gtmPush } from '../../../utils/gtmTrackers';
29
29
 
30
30
  import getEventsMap from '../eventsMap';
31
- import {getPrimaryCtaFields, getSecondaryCtaFields, getLinkTypeFields, getContent} from '../commonMethods';
31
+ import {getPrimaryCtaFields, getSecondaryCtaFields, getLinkTypeFields, getContent, getMobilePushEmbeddedContentValidity} from '../commonMethods';
32
32
  import { GA } from '@capillarytech/cap-ui-utils';
33
33
  import { EDIT, TRACK_EDIT_MPUSH } from '../../App/constants';
34
34
  import { MOBILE_PUSH } from '../../CreativesContainer/constants';
@@ -85,6 +85,10 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
85
85
  this.hasFetchedInitialTagsRef = false;
86
86
  // Guard: avoid duplicate fetch when multiple TagList instances trigger same context
87
87
  this.lastFetchedTagContextRef = null;
88
+ // Tracks the last content-emptiness value reported to the parent so componentDidUpdate
89
+ // does not dispatch on every render. Intentionally undefined (not true) so the first
90
+ // render always reports the real validity rather than assuming the form starts invalid.
91
+ this._lastReportedMobilePushContentEmpty = undefined;
88
92
  }
89
93
  componentWillMount() {
90
94
  this.props.actions.getWeCrmAccounts("mobilepush");
@@ -126,6 +130,18 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
126
130
  parent.postMessage(JSON.stringify(response), '*');
127
131
  }
128
132
  }
133
+ componentDidUpdate() {
134
+ // Reports live form validity so the host (CreativesContainer) can disable Done/Preview
135
+ // and Test as soon as the message content is cleared — mirrors Sms/Edit's own reporting.
136
+ if (typeof this.props.onContentValidityChange !== 'function') {
137
+ return;
138
+ }
139
+ const validity = getMobilePushEmbeddedContentValidity(this.state.formData, this.state.tabCount);
140
+ const isContentEmpty = !!validity.isContentEmpty;
141
+ if (this._lastReportedMobilePushContentEmpty === isContentEmpty) return;
142
+ this._lastReportedMobilePushContentEmpty = isContentEmpty;
143
+ this.props.onContentValidityChange({ isContentEmpty });
144
+ }
129
145
  componentWillReceiveProps(nextProps) {
130
146
  if (nextProps.params?.id !== this.props.params?.id) {
131
147
  this.hasFetchedInitialTagsRef = false;
@@ -2337,6 +2353,7 @@ Edit.propTypes = {
2337
2353
  onValidationFail: PropTypes.func,
2338
2354
  onPreviewContentClicked: PropTypes.func,
2339
2355
  onTestContentClicked: PropTypes.func,
2356
+ onContentValidityChange: PropTypes.func,
2340
2357
  creativesMode: PropTypes.string,
2341
2358
  eventContextTags: PropTypes.array,
2342
2359
  waitEventContextTags: PropTypes.object,