@capillarytech/creatives-library 8.0.353-alpha.5 → 8.0.353-alpha.6

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 (136) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapTagList/index.js +10 -0
  15. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +213 -21
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  22. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +0 -17
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -146
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +138 -48
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -4
  29. package/v2Components/CommonTestAndPreview/index.js +691 -235
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +0 -159
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -256
  42. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -2
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -198
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +36 -26
  46. package/v2Components/FormBuilder/index.js +11 -6
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +956 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +38 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -31
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +15 -3
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/App/constants.js +0 -3
  71. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  72. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  73. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  74. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  75. package/v2Containers/CreativesContainer/constants.js +9 -0
  76. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  77. package/v2Containers/CreativesContainer/index.js +322 -103
  78. package/v2Containers/CreativesContainer/index.scss +51 -1
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  88. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  89. package/v2Containers/Rcs/constants.js +119 -10
  90. package/v2Containers/Rcs/index.js +2445 -813
  91. package/v2Containers/Rcs/index.scss +280 -8
  92. package/v2Containers/Rcs/messages.js +34 -3
  93. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  94. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  95. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  96. package/v2Containers/Rcs/tests/index.test.js +152 -121
  97. package/v2Containers/Rcs/tests/mockData.js +38 -0
  98. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  99. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  100. package/v2Containers/Rcs/utils.js +478 -11
  101. package/v2Containers/Sms/Create/index.js +106 -40
  102. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  103. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  104. package/v2Containers/SmsTrai/Create/index.js +9 -4
  105. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  106. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  107. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  108. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  109. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  110. package/v2Containers/SmsWrapper/index.js +37 -8
  111. package/v2Containers/TagList/index.js +6 -0
  112. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  113. package/v2Containers/Templates/_templates.scss +166 -9
  114. package/v2Containers/Templates/actions.js +11 -0
  115. package/v2Containers/Templates/constants.js +2 -0
  116. package/v2Containers/Templates/index.js +122 -120
  117. package/v2Containers/Templates/sagas.js +56 -12
  118. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  119. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  120. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  121. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  122. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  123. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  124. package/v2Containers/TemplatesV2/index.js +86 -23
  125. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  126. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  127. package/v2Containers/WebPush/Create/index.js +8 -91
  128. package/v2Containers/WebPush/Create/index.scss +0 -7
  129. package/v2Containers/Whatsapp/index.js +3 -20
  130. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  131. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +0 -169
  132. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +0 -522
  133. package/v2Containers/App/tests/constants.test.js +0 -61
  134. package/v2Containers/Templates/tests/webpush.test.js +0 -375
  135. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +0 -338
  136. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +0 -325
@@ -33,6 +33,10 @@ import injectReducer from '../../../utils/injectReducer';
33
33
  import v2SmsCreateReducer from './reducer';
34
34
  import * as globalActions from '../../Cap/actions';
35
35
  import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
36
+ import {
37
+ getSmsEmbeddedFooterValidity,
38
+ getSmsMessageFromFormData,
39
+ } from '../smsFormDataHelpers';
36
40
 
37
41
  export class Create extends React.Component { // eslint-disable-line react/prefer-stateless-function
38
42
  constructor(props) {
@@ -53,6 +57,11 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
53
57
  isTestAndPreviewMode: false,
54
58
  pendingGetFormData: false,
55
59
  };
60
+ // Tracks the last validity value reported to SmsFallback so componentDidUpdate
61
+ // does not dispatch on every render and create an infinite update loop.
62
+ // Intentionally undefined (not true) so the first render always reports the
63
+ // real validity rather than assuming the form starts invalid.
64
+ this._lastReportedSmsFooterInvalid = undefined;
56
65
  this.saveFormData = this.saveFormData.bind(this);
57
66
  this.onFormDataChange = this.onFormDataChange.bind(this);
58
67
  this.onTemplateNameChange = this.onTemplateNameChange.bind(this);
@@ -159,7 +168,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
159
168
  layout: 'SMS',
160
169
  type: 'TAG',
161
170
  context: this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default',
162
- embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
171
+ embedded: this.props.forceFullTagContext
172
+ ? 'full'
173
+ : (this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full'),
163
174
  };
164
175
  if (this.props.getDefaultTags) {
165
176
  query.context = this.props.getDefaultTags;
@@ -172,6 +183,28 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
172
183
  }
173
184
  }
174
185
 
186
+ componentDidUpdate() {
187
+ if (!this.props.embeddedSmsFallback || typeof this.props.onEmbeddedSmsFooterValidity !== 'function') {
188
+ return;
189
+ }
190
+ const validity = getSmsEmbeddedFooterValidity(this.state.formData, this.state.tabCount);
191
+ const isTemplateNameEmpty = !!validity.isTemplateNameEmpty;
192
+ const isMessageEmpty = !!validity.isMessageEmpty;
193
+ // Only dispatch when the validity tuple changes. Dispatching unconditionally caused
194
+ // an infinite loop: componentDidUpdate → dispatch → SmsFallback re-render →
195
+ // SmsCreate re-render → componentDidUpdate → ... even when state was unchanged.
196
+ // Tracking both fields individually (not just the combined isInvalid flag) ensures
197
+ // the parent receives an updated validity object when individual fields flip even
198
+ // while the combined invalid state stays the same (e.g. true→true with different fields).
199
+ if (
200
+ this._lastReportedSmsFooterTemplateNameEmpty === isTemplateNameEmpty &&
201
+ this._lastReportedSmsFooterMessageEmpty === isMessageEmpty
202
+ ) return;
203
+ this._lastReportedSmsFooterTemplateNameEmpty = isTemplateNameEmpty;
204
+ this._lastReportedSmsFooterMessageEmpty = isMessageEmpty;
205
+ this.props.onEmbeddedSmsFooterValidity(validity);
206
+ }
207
+
175
208
  componentWillUnmount() {
176
209
  if (this.pendingGetFormDataTimeout) {
177
210
  clearTimeout(this.pendingGetFormDataTimeout);
@@ -275,6 +308,10 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
275
308
  }
276
309
  const result = {};
277
310
  result.base = baseData;
311
+ /* Root field used by FormBuilder; embedded getFormSubscriptionData reads value.base */
312
+ if (formData['template-name'] !== undefined) {
313
+ result.base['template-name'] = formData['template-name'];
314
+ }
278
315
  if (this.state.isValid) {
279
316
  const msgObj = charCount.updateCharCount(baseData['sms-editor']);
280
317
  result.base.msg_count = msgObj.msgCount;
@@ -884,7 +921,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
884
921
  layout: 'SMS',
885
922
  type: 'TAG',
886
923
  context: (data && data.toLowerCase() === 'all') ? 'default' : (data && data.toLowerCase()),
887
- embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
924
+ embedded: this.props.forceFullTagContext
925
+ ? 'full'
926
+ : (this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full'),
888
927
  };
889
928
  this.props.globalActions.fetchSchemaForEntity(query);
890
929
  }
@@ -892,11 +931,29 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
892
931
  removeStandAlone() {
893
932
  const schema = _.cloneDeep(this.state.schema);
894
933
  const childSections = _.get(schema, 'standalone.sections[0].childSections');
895
- childSections.splice(2, 1);
896
- const fields = _.get(childSections, '[1].childSections[0].childSections');//removing template name section
897
- fields.splice(0, 1);
898
- _.set(childSections, '[1].childSections[0].childSections', fields);
899
- _.set(schema, 'standalone.sections[0].childSections', childSections);
934
+ if (childSections) {
935
+ /* In-form Save / Test row (index 2) only exists when the schema has > 2 sections. */
936
+ if (childSections.length > 2) {
937
+ childSections.splice(2, 1);
938
+ }
939
+ /*
940
+ * Creatives library drops the standalone template-name block because `SlideBoxHeader`
941
+ * shows the name. This is independent of the section count — guard it separately so
942
+ * it still runs even when childSections.length <= 2 (e.g. schema arrives pre-trimmed).
943
+ * RCS SMS fallback uses the same slidebox footer but keeps the name in the form.
944
+ */
945
+ if (!this.props.embeddedSmsFallback) {
946
+ const fields = _.get(childSections, '[1].childSections[0].childSections');
947
+ if (fields && fields.length > 0) {
948
+ fields.splice(0, 1);
949
+ _.set(childSections, '[1].childSections[0].childSections', fields);
950
+ }
951
+ }
952
+ _.set(schema, 'standalone.sections[0].childSections', childSections);
953
+ }
954
+ // Always increment loadingStatus — isSmsLoading() requires >= 2 in library mode
955
+ // (isFullMode=false). The early return previously skipped this, leaving the
956
+ // spinner stuck forever when the schema had <= 2 childSections.
900
957
  this.setState({ schema, loadingStatus: this.state.loadingStatus + 1 });
901
958
  }
902
959
 
@@ -941,37 +998,8 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
941
998
  this.setState({startValidation: false});
942
999
  }
943
1000
 
944
- getTemplateContent = () => {
945
- // Get SMS content from formData
946
- if (!this.state.formData || !Array.isArray(this.state.formData) || this.state.formData.length === 0) {
947
- return '';
948
- }
949
- const currentTabData = this.state.formData[this.state.currentTab - 1];
950
- if (!currentTabData) {
951
- return '';
952
- }
953
-
954
- // PRIORITY 1: Check direct path first (most common for SMS)
955
- // This handles: formData[0]['sms-editor']
956
- if (currentTabData['sms-editor']) {
957
- return currentTabData['sms-editor'];
958
- }
959
-
960
- // PRIORITY 2: Check activeTab structure (for versioned templates)
961
- // This handles: formData[0][activeTab]['sms-editor']
962
- const activeTab = currentTabData?.activeTab || 'base';
963
- if (currentTabData[activeTab]?.['sms-editor']) {
964
- return currentTabData[activeTab]['sms-editor'];
965
- }
966
-
967
- // PRIORITY 3: Check base explicitly (fallback)
968
- // This handles: formData[0]['base']['sms-editor']
969
- if (currentTabData['base']?.['sms-editor']) {
970
- return currentTabData['base']['sms-editor'];
971
- }
972
-
973
- return '';
974
- }
1001
+ getTemplateContent = () =>
1002
+ getSmsMessageFromFormData(this.state.formData, this.state.currentTab);
975
1003
 
976
1004
  handleTestAndPreview = () => {
977
1005
  // If parent is managing state (props exist), call parent handler
@@ -1000,6 +1028,35 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
1000
1028
  }
1001
1029
 
1002
1030
  saveFormData() {
1031
+ /*
1032
+ * RCS SMS fallback slidebox (embeddedSmsFallback): parent passes isFullMode from RCS, but we must not
1033
+ * call createTemplate — that spins CapSpin on createTemplateInProgress and is not the embedded contract.
1034
+ * Same as library: hand off form payload via getFormSubscriptionData.
1035
+ */
1036
+ if (this.props.embeddedSmsFallback && this.props.getFormSubscriptionData) {
1037
+ const { isTemplateNameEmpty, isMessageEmpty } = getSmsEmbeddedFooterValidity(
1038
+ this.state.formData,
1039
+ this.state.tabCount,
1040
+ );
1041
+ if (isTemplateNameEmpty || isMessageEmpty) {
1042
+ this.setState({ startValidation: true, pendingGetFormData: false });
1043
+ if (this.props.onValidationFail) {
1044
+ this.props.onValidationFail();
1045
+ }
1046
+ return;
1047
+ }
1048
+ const payload = this.getFormData();
1049
+ if (!payload.validity) {
1050
+ if (this.props.onValidationFail) {
1051
+ this.props.onValidationFail();
1052
+ }
1053
+ this.setState({ pendingGetFormData: false, startValidation: false });
1054
+ return;
1055
+ }
1056
+ this.props.getFormSubscriptionData(payload);
1057
+ this.setState({ pendingGetFormData: false, startValidation: false });
1058
+ return;
1059
+ }
1003
1060
  // In library mode: FormBuilder calls onSubmit only after liquid validation succeeds.
1004
1061
  // Submit to parent here so the slidebox can close with valid data.
1005
1062
  if (!this.props.isFullMode) {
@@ -1099,7 +1156,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
1099
1156
  onTestContentClicked={this.props.onTestContentClicked}
1100
1157
  onPreviewContentClicked={this.props.onPreviewContentClicked}
1101
1158
  eventContextTags={this.props?.eventContextTags}
1102
- waitEventContextTags={this.props?.waitEventContextTags}
1159
+ tagListGetPopupContainer={this.props.tagListGetPopupContainer}
1160
+ tagListPopoverOverlayStyle={this.props.tagListPopoverOverlayStyle}
1161
+ tagListPopoverOverlayClassName={this.props.tagListPopoverOverlayClassName}
1103
1162
  />
1104
1163
  </CapColumn>
1105
1164
  </CapRow>
@@ -1112,6 +1171,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
1112
1171
  formData={this.state.formData}
1113
1172
  content={this.getTemplateContent()}
1114
1173
  currentChannel={SMS}
1174
+ smsRegister={this.props.smsRegister}
1115
1175
  />
1116
1176
  </div>
1117
1177
  );
@@ -1136,11 +1196,17 @@ Create.propTypes = {
1136
1196
  isLoadingMetaEntities: PropTypes.bool,
1137
1197
  selectedOfferDetails: PropTypes.array,
1138
1198
  eventContextTags: PropTypes.array,
1139
- waitEventContextTags: PropTypes.object,
1140
1199
  showTestAndPreviewSlidebox: PropTypes.bool,
1141
1200
  handleTestAndPreview: PropTypes.func,
1142
1201
  handleCloseTestAndPreview: PropTypes.func,
1143
1202
  isTestAndPreviewMode: PropTypes.bool,
1203
+ smsRegister: PropTypes.any,
1204
+ forceFullTagContext: PropTypes.bool,
1205
+ embeddedSmsFallback: PropTypes.bool,
1206
+ onEmbeddedSmsFooterValidity: PropTypes.func,
1207
+ tagListGetPopupContainer: PropTypes.func,
1208
+ tagListPopoverOverlayStyle: PropTypes.object,
1209
+ tagListPopoverOverlayClassName: PropTypes.string,
1144
1210
  };
1145
1211
 
1146
1212
  const mapStateToProps = createStructuredSelector({
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared SMS FormBuilder formData helpers for Sms/Create (and any embedded host).
3
+ */
4
+
5
+ /**
6
+ * @param {object} formData FormBuilder state (same shape as Sms/Create `this.state.formData`)
7
+ * @param {number} currentTab 1-based tab index
8
+ * @returns {string} Raw message body or ''
9
+ */
10
+ export function getSmsMessageFromFormData(formData, currentTab) {
11
+ if (formData == null || typeof formData !== 'object') {
12
+ return '';
13
+ }
14
+ const tab = currentTab != null && currentTab > 0 ? currentTab : 1;
15
+ const currentTabData = formData[tab - 1];
16
+ if (currentTabData && typeof currentTabData === 'object') {
17
+ const versionedKey = tab > 1 ? `sms-editor${tab}` : 'sms-editor';
18
+ if (Object.prototype.hasOwnProperty.call(currentTabData, versionedKey)) {
19
+ const v = currentTabData[versionedKey];
20
+ // Key exists — commit to this version's value rather than falling through to base.
21
+ // Treat null/undefined as empty so a cleared version correctly reports as empty.
22
+ return (v != null && v !== '') ? String(v) : '';
23
+ }
24
+ if (currentTabData['sms-editor'] != null) {
25
+ return String(currentTabData['sms-editor']);
26
+ }
27
+ const activeTab = currentTabData.activeTab || 'base';
28
+ if (currentTabData[activeTab]?.['sms-editor'] != null) {
29
+ return String(currentTabData[activeTab]['sms-editor']);
30
+ }
31
+ if (currentTabData.base?.['sms-editor'] != null) {
32
+ return String(currentTabData.base['sms-editor']);
33
+ }
34
+ }
35
+ const rootBase = formData.base;
36
+ if (rootBase && typeof rootBase === 'object' && rootBase['sms-editor'] != null) {
37
+ return String(rootBase['sms-editor']);
38
+ }
39
+ return '';
40
+ }
41
+
42
+ /**
43
+ * @param {number} [tabCount] Total number of versions/tabs. When >1 all versions are checked.
44
+ * @returns {{ isTemplateNameEmpty: boolean, isMessageEmpty: boolean }}
45
+ */
46
+ export function getSmsEmbeddedFooterValidity(formData, tabCount) {
47
+ const rawName = formData?.['template-name'];
48
+ const name = rawName != null && rawName !== '' ? String(rawName).trim() : '';
49
+
50
+ // Check ALL versions: if any version's message is empty, Done should be disabled.
51
+ // With a single version this is equivalent to the previous single-tab check.
52
+ const count = tabCount != null && tabCount > 1 ? tabCount : 1;
53
+ let isMessageEmpty = false;
54
+ for (let i = 1; i <= count; i++) {
55
+ const content = getSmsMessageFromFormData(formData, i);
56
+ const msg = content != null && content !== '' ? String(content).trim() : '';
57
+ if (!msg) {
58
+ isMessageEmpty = true;
59
+ break;
60
+ }
61
+ }
62
+
63
+ return {
64
+ isTemplateNameEmpty: !name,
65
+ isMessageEmpty,
66
+ };
67
+ }
@@ -0,0 +1,253 @@
1
+ import {
2
+ getSmsMessageFromFormData,
3
+ getSmsEmbeddedFooterValidity,
4
+ } from '../smsFormDataHelpers';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // getSmsMessageFromFormData
8
+ // ---------------------------------------------------------------------------
9
+
10
+ describe('getSmsMessageFromFormData', () => {
11
+ describe('null / invalid formData guard', () => {
12
+ it('returns empty string when formData is null', () => {
13
+ expect(getSmsMessageFromFormData(null, 1)).toBe('');
14
+ });
15
+
16
+ it('returns empty string when formData is undefined', () => {
17
+ expect(getSmsMessageFromFormData(undefined, 1)).toBe('');
18
+ });
19
+
20
+ it('returns empty string when formData is a string (not an object)', () => {
21
+ expect(getSmsMessageFromFormData('bad', 1)).toBe('');
22
+ });
23
+
24
+ it('returns empty string when formData is a number', () => {
25
+ expect(getSmsMessageFromFormData(42, 1)).toBe('');
26
+ });
27
+ });
28
+
29
+ describe('currentTab normalisation', () => {
30
+ it('defaults to tab 1 when currentTab is null', () => {
31
+ const formData = { 0: { 'sms-editor': 'hello' } };
32
+ expect(getSmsMessageFromFormData(formData, null)).toBe('hello');
33
+ });
34
+
35
+ it('defaults to tab 1 when currentTab is undefined', () => {
36
+ const formData = { 0: { 'sms-editor': 'hello' } };
37
+ expect(getSmsMessageFromFormData(formData, undefined)).toBe('hello');
38
+ });
39
+
40
+ it('defaults to tab 1 when currentTab is 0', () => {
41
+ const formData = { 0: { 'sms-editor': 'hello' } };
42
+ expect(getSmsMessageFromFormData(formData, 0)).toBe('hello');
43
+ });
44
+
45
+ it('defaults to tab 1 when currentTab is negative', () => {
46
+ const formData = { 0: { 'sms-editor': 'hello' } };
47
+ expect(getSmsMessageFromFormData(formData, -5)).toBe('hello');
48
+ });
49
+ });
50
+
51
+ describe('versioned key lookup', () => {
52
+ it('uses "sms-editor" key for tab 1', () => {
53
+ const formData = { 0: { 'sms-editor': 'tab1 msg' } };
54
+ expect(getSmsMessageFromFormData(formData, 1)).toBe('tab1 msg');
55
+ });
56
+
57
+ it('uses "sms-editor2" versioned key for tab 2', () => {
58
+ const formData = { 1: { 'sms-editor2': 'tab2 msg', 'sms-editor': 'fallback' } };
59
+ expect(getSmsMessageFromFormData(formData, 2)).toBe('tab2 msg');
60
+ });
61
+
62
+ it('uses "sms-editor3" versioned key for tab 3', () => {
63
+ const formData = { 2: { 'sms-editor3': 'tab3 msg' } };
64
+ expect(getSmsMessageFromFormData(formData, 3)).toBe('tab3 msg');
65
+ });
66
+
67
+ it('returns empty string when versioned key exists but is null (cleared version)', () => {
68
+ const formData = { 1: { 'sms-editor2': null } };
69
+ expect(getSmsMessageFromFormData(formData, 2)).toBe('');
70
+ });
71
+
72
+ it('returns empty string when versioned key exists but is empty string', () => {
73
+ const formData = { 1: { 'sms-editor2': '' } };
74
+ expect(getSmsMessageFromFormData(formData, 2)).toBe('');
75
+ });
76
+
77
+ it('coerces a numeric value in versioned key to string', () => {
78
+ const formData = { 1: { 'sms-editor2': 12345 } };
79
+ expect(getSmsMessageFromFormData(formData, 2)).toBe('12345');
80
+ });
81
+ });
82
+
83
+ describe('"sms-editor" flat key fallback (versioned key absent)', () => {
84
+ it('falls back to "sms-editor" when versioned key is absent for tab 2', () => {
85
+ const formData = { 1: { 'sms-editor': 'flat fallback' } };
86
+ expect(getSmsMessageFromFormData(formData, 2)).toBe('flat fallback');
87
+ });
88
+ });
89
+
90
+ describe('activeTab nested fallback', () => {
91
+ it('falls back to activeTab["sms-editor"] when neither versioned key nor flat "sms-editor" is present', () => {
92
+ // The versioned key must be absent entirely for the activeTab fallback to be reached.
93
+ // (If 'sms-editor' exists but is null, the function commits to '' without falling through.)
94
+ const formData = {
95
+ 0: {
96
+ activeTab: 'variant1',
97
+ variant1: { 'sms-editor': 'variant msg' },
98
+ },
99
+ };
100
+ expect(getSmsMessageFromFormData(formData, 1)).toBe('variant msg');
101
+ });
102
+
103
+ it('falls back to base["sms-editor"] when activeTab entry lacks "sms-editor" and flat key is absent', () => {
104
+ const formData = {
105
+ 0: {
106
+ activeTab: 'variant1',
107
+ variant1: {},
108
+ base: { 'sms-editor': 'base msg' },
109
+ },
110
+ };
111
+ expect(getSmsMessageFromFormData(formData, 1)).toBe('base msg');
112
+ });
113
+
114
+ it('defaults activeTab to "base" when activeTab prop is not set and flat key is absent', () => {
115
+ const formData = {
116
+ 0: {
117
+ base: { 'sms-editor': 'default base msg' },
118
+ },
119
+ };
120
+ expect(getSmsMessageFromFormData(formData, 1)).toBe('default base msg');
121
+ });
122
+ });
123
+
124
+ describe('root formData.base fallback', () => {
125
+ it('falls back to formData.base["sms-editor"] when tab slot is missing', () => {
126
+ const formData = {
127
+ base: { 'sms-editor': 'root base msg' },
128
+ };
129
+ expect(getSmsMessageFromFormData(formData, 1)).toBe('root base msg');
130
+ });
131
+
132
+ it('returns empty string when even root base has no "sms-editor"', () => {
133
+ const formData = { base: {} };
134
+ expect(getSmsMessageFromFormData(formData, 1)).toBe('');
135
+ });
136
+
137
+ it('returns empty string when formData has no relevant keys at all', () => {
138
+ expect(getSmsMessageFromFormData({}, 1)).toBe('');
139
+ });
140
+ });
141
+ });
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // getSmsEmbeddedFooterValidity
145
+ // ---------------------------------------------------------------------------
146
+
147
+ describe('getSmsEmbeddedFooterValidity', () => {
148
+ describe('isTemplateNameEmpty', () => {
149
+ it('is true when template-name is absent', () => {
150
+ const formData = { 0: { 'sms-editor': 'msg' } };
151
+ const { isTemplateNameEmpty } = getSmsEmbeddedFooterValidity(formData, 1);
152
+ expect(isTemplateNameEmpty).toBe(true);
153
+ });
154
+
155
+ it('is true when template-name is empty string', () => {
156
+ const formData = { 'template-name': '', 0: { 'sms-editor': 'msg' } };
157
+ const { isTemplateNameEmpty } = getSmsEmbeddedFooterValidity(formData, 1);
158
+ expect(isTemplateNameEmpty).toBe(true);
159
+ });
160
+
161
+ it('is true when template-name is whitespace only', () => {
162
+ const formData = { 'template-name': ' ', 0: { 'sms-editor': 'msg' } };
163
+ const { isTemplateNameEmpty } = getSmsEmbeddedFooterValidity(formData, 1);
164
+ expect(isTemplateNameEmpty).toBe(true);
165
+ });
166
+
167
+ it('is false when template-name is a non-empty string', () => {
168
+ const formData = { 'template-name': 'My Template', 0: { 'sms-editor': 'msg' } };
169
+ const { isTemplateNameEmpty } = getSmsEmbeddedFooterValidity(formData, 1);
170
+ expect(isTemplateNameEmpty).toBe(false);
171
+ });
172
+ });
173
+
174
+ describe('isMessageEmpty — single tab', () => {
175
+ it('is true when message is empty string', () => {
176
+ const formData = { 'template-name': 'T', 0: { 'sms-editor': '' } };
177
+ const { isMessageEmpty } = getSmsEmbeddedFooterValidity(formData, 1);
178
+ expect(isMessageEmpty).toBe(true);
179
+ });
180
+
181
+ it('is true when message is whitespace only', () => {
182
+ const formData = { 'template-name': 'T', 0: { 'sms-editor': ' ' } };
183
+ const { isMessageEmpty } = getSmsEmbeddedFooterValidity(formData, 1);
184
+ expect(isMessageEmpty).toBe(true);
185
+ });
186
+
187
+ it('is true when no sms-editor key at all', () => {
188
+ const formData = { 'template-name': 'T', 0: {} };
189
+ const { isMessageEmpty } = getSmsEmbeddedFooterValidity(formData, 1);
190
+ expect(isMessageEmpty).toBe(true);
191
+ });
192
+
193
+ it('is false when message is non-empty', () => {
194
+ const formData = { 'template-name': 'T', 0: { 'sms-editor': 'Hello world' } };
195
+ const { isMessageEmpty } = getSmsEmbeddedFooterValidity(formData, 1);
196
+ expect(isMessageEmpty).toBe(false);
197
+ });
198
+ });
199
+
200
+ describe('isMessageEmpty — multiple tabs', () => {
201
+ it('is false when all tabs have non-empty messages', () => {
202
+ const formData = {
203
+ 'template-name': 'T',
204
+ 0: { 'sms-editor': 'Tab 1 message' },
205
+ 1: { 'sms-editor2': 'Tab 2 message' },
206
+ 2: { 'sms-editor3': 'Tab 3 message' },
207
+ };
208
+ const { isMessageEmpty } = getSmsEmbeddedFooterValidity(formData, 3);
209
+ expect(isMessageEmpty).toBe(false);
210
+ });
211
+
212
+ it('is true when the second tab has an empty message', () => {
213
+ const formData = {
214
+ 'template-name': 'T',
215
+ 0: { 'sms-editor': 'Tab 1 message' },
216
+ 1: { 'sms-editor2': '' },
217
+ };
218
+ const { isMessageEmpty } = getSmsEmbeddedFooterValidity(formData, 2);
219
+ expect(isMessageEmpty).toBe(true);
220
+ });
221
+
222
+ it('is true when the third tab has no message', () => {
223
+ const formData = {
224
+ 'template-name': 'T',
225
+ 0: { 'sms-editor': 'msg1' },
226
+ 1: { 'sms-editor2': 'msg2' },
227
+ 2: {},
228
+ };
229
+ const { isMessageEmpty } = getSmsEmbeddedFooterValidity(formData, 3);
230
+ expect(isMessageEmpty).toBe(true);
231
+ });
232
+
233
+ it('treats tabCount=1 as single-tab check even when not explicitly provided', () => {
234
+ const formData = { 'template-name': 'T', 0: { 'sms-editor': 'msg' } };
235
+ const { isMessageEmpty } = getSmsEmbeddedFooterValidity(formData);
236
+ expect(isMessageEmpty).toBe(false);
237
+ });
238
+ });
239
+
240
+ describe('both fields together', () => {
241
+ it('returns both empty when template name absent and message empty', () => {
242
+ const formData = { 0: { 'sms-editor': '' } };
243
+ const result = getSmsEmbeddedFooterValidity(formData, 1);
244
+ expect(result).toEqual({ isTemplateNameEmpty: true, isMessageEmpty: true });
245
+ });
246
+
247
+ it('returns both non-empty when template name and message are present', () => {
248
+ const formData = { 'template-name': 'My Template', 0: { 'sms-editor': 'Hello!' } };
249
+ const result = getSmsEmbeddedFooterValidity(formData, 1);
250
+ expect(result).toEqual({ isTemplateNameEmpty: false, isMessageEmpty: false });
251
+ });
252
+ });
253
+ });
@@ -70,6 +70,7 @@ export const SmsTraiCreate = (props) => {
70
70
  onCreateComplete,
71
71
  isFullMode,
72
72
  onShowTemplates,
73
+ embeddedSmsFallback,
73
74
  traiSms: {
74
75
  duplicateDetails = {},
75
76
  duplicateDetailsError = '',
@@ -659,9 +660,11 @@ export const SmsTraiCreate = (props) => {
659
660
 
660
661
  const createCallback = ({ errorMessage }) => {
661
662
  if (!errorMessage) {
662
- CapNotification.success({
663
- message: formatMessage(messages.smsCreateNotification),
664
- });
663
+ if (!embeddedSmsFallback) {
664
+ CapNotification.success({
665
+ message: formatMessage(messages.smsCreateNotification),
666
+ });
667
+ }
665
668
  actions.clearCreateResponse();
666
669
  } else {
667
670
  CapNotification.error({
@@ -741,7 +744,9 @@ export const SmsTraiCreate = (props) => {
741
744
  { templates: savedData },
742
745
  (resp, errorMessage) => {
743
746
  createCallback({ errorMessage });
744
- if (isFullMode) {
747
+ if (embeddedSmsFallback) {
748
+ if (!errorMessage) onCreateComplete(savedData);
749
+ } else if (isFullMode) {
745
750
  onCreateComplete();
746
751
  } else {
747
752
  onShowTemplates();
@@ -5,6 +5,8 @@
5
5
  */
6
6
 
7
7
  export const CHARLIMIT = 40;
8
+ /** Display / soft cap label for DLT SMS body length (matches product UI). */
9
+ export const SMS_TRAI_CONTENT_MAX_LENGTH = 1024;
8
10
  export const SMS = 'SMS';
9
11
  export const SMS_TRAI_VAR = '{#var#}';
10
12
  export const TAG = 'TAG';