@capillarytech/creatives-library 8.0.318 → 8.0.320

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 (139) hide show
  1. package/constants/unified.js +15 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/utils/templateVarUtils.js +172 -0
  7. package/utils/tests/templateVarUtils.test.js +160 -0
  8. package/v2Components/CapTagList/index.js +10 -0
  9. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  16. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  19. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  20. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  21. package/v2Components/CommonTestAndPreview/index.js +693 -155
  22. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  23. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  24. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  25. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  26. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  27. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  29. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  30. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  31. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  32. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  33. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  34. package/v2Components/FormBuilder/index.js +7 -1
  35. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  36. package/v2Components/SmsFallback/constants.js +73 -0
  37. package/v2Components/SmsFallback/index.js +956 -0
  38. package/v2Components/SmsFallback/index.scss +265 -0
  39. package/v2Components/SmsFallback/messages.js +78 -0
  40. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  41. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  42. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  43. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  44. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  45. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  46. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  47. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  48. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  49. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  50. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  51. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  52. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  53. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  54. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  55. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  56. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  57. package/v2Containers/CommunicationFlow/constants.js +200 -0
  58. package/v2Containers/CommunicationFlow/index.js +102 -0
  59. package/v2Containers/CommunicationFlow/messages.js +346 -0
  60. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  61. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  62. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  63. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  64. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  65. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  66. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  67. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  68. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  69. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  70. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  71. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  72. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  75. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  76. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  77. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  78. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  79. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  80. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  81. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  82. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  83. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  84. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  85. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  86. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  87. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  88. package/v2Containers/CreativesContainer/constants.js +12 -0
  89. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  90. package/v2Containers/CreativesContainer/index.js +289 -99
  91. package/v2Containers/CreativesContainer/index.scss +51 -1
  92. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  93. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  94. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  95. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  96. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  97. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  98. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  99. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  100. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  101. package/v2Containers/Rcs/constants.js +32 -1
  102. package/v2Containers/Rcs/index.js +950 -873
  103. package/v2Containers/Rcs/index.scss +85 -6
  104. package/v2Containers/Rcs/messages.js +10 -1
  105. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  106. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  107. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  108. package/v2Containers/Rcs/tests/index.test.js +41 -38
  109. package/v2Containers/Rcs/tests/mockData.js +38 -0
  110. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  111. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  112. package/v2Containers/Rcs/utils.js +358 -10
  113. package/v2Containers/Sms/Create/index.js +81 -36
  114. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  115. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  116. package/v2Containers/SmsTrai/Create/index.js +9 -4
  117. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  118. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  119. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  120. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  121. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  122. package/v2Containers/SmsWrapper/index.js +37 -8
  123. package/v2Containers/TagList/index.js +6 -0
  124. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  125. package/v2Containers/Templates/_templates.scss +61 -2
  126. package/v2Containers/Templates/actions.js +11 -0
  127. package/v2Containers/Templates/constants.js +2 -0
  128. package/v2Containers/Templates/index.js +90 -40
  129. package/v2Containers/Templates/sagas.js +57 -12
  130. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  131. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  132. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  133. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  134. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  135. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  136. package/v2Containers/TemplatesV2/index.js +86 -23
  137. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  138. package/v2Containers/Whatsapp/index.js +3 -20
  139. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -1,9 +1,5 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import {
4
- CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
5
- } from '@capillarytech/cap-ui-library/styled/variables';
6
-
7
3
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
8
4
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
9
5
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
@@ -13,12 +9,11 @@ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
13
9
  import { injectIntl, FormattedMessage } from 'react-intl';
14
10
  import classnames from 'classnames';
15
11
  import {
16
- isEmpty, get, forEach, cloneDeep, debounce,
12
+ isEmpty, get, forEach, cloneDeep, debounce, pick,
17
13
  } from 'lodash';
18
14
  import { connect } from 'react-redux';
19
15
  import { createStructuredSelector } from 'reselect';
20
16
  import { bindActionCreators, compose } from 'redux';
21
- import styled from 'styled-components';
22
17
  import { GA } from '@capillarytech/cap-ui-utils';
23
18
  import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
24
19
 
@@ -47,6 +42,8 @@ import {
47
42
  import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants';
48
43
  import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
49
44
  import { RCS_STATUSES } from '../Rcs/constants';
45
+ import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
46
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
50
47
  import { CREATIVE } from '../Facebook/constants';
51
48
  import { LOYALTY } from '../App/constants';
52
49
  import {
@@ -61,6 +58,11 @@ import { capSagaForFetchSchemaForEntity, capSagaLiquidEntity } from '../Cap/saga
61
58
  import { v2TemplateSagaWatchGetDefaultBeeTemplates } from '../Templates/sagas';
62
59
  import { DYNAMIC_URL } from '../../v2Components/CapWhatsappCTA/constants';
63
60
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
61
+ import SlideBoxWrapper from './CreativesSlideBoxWrapper';
62
+ import {
63
+ computeLiquidFooterUpdateFromFormBuilder,
64
+ getSlideBoxWrapperMarginFromLiquidErrors,
65
+ } from './embeddedSlideboxUtils';
64
66
 
65
67
  import {
66
68
  transformChannelPayload,
@@ -69,51 +71,24 @@ import {
69
71
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
70
72
  import { BIG_HTML } from '../InApp/constants';
71
73
 
72
- /**
73
- * Returns true if value is "deep empty": no errors present.
74
- * - null/undefined: empty
75
- * - string: empty if length === 0
76
- * - array: empty if length === 0
77
- * - plain object (e.g. { android: [], ios: [], generic: [] }): empty only if every value is deep-empty
78
- */
79
- function isDeepEmpty(value) {
80
- if (value == null) return true;
81
- if (typeof value === 'string') return value.length === 0;
82
- if (Array.isArray(value)) return value.length === 0;
83
- if (typeof value === 'object') {
84
- return Object.values(value).every(isDeepEmpty);
85
- }
86
- return false;
87
- }
88
-
89
74
  const classPrefix = 'add-creatives-section';
90
75
  const CREATIVES_CONTAINER = 'creativesContainer';
91
76
 
92
- const SlideBoxWrapper = styled.div`
93
- .cap-slide-box-v2-container{
94
- .slidebox-header, .slidebox-content-container{
95
- margin-bottom: ${({ slideBoxWrapperMargin }) => `${slideBoxWrapperMargin}`};
96
- padding: 0 rem;
97
- &.has-footer{
98
- overflow-x: hidden;
99
- }
100
- }
101
- .slidebox-footer{
102
- /* Only apply margin-bottom to footer when ErrorInfoNote is shown in footer (BEE editor) */
103
- /* For HTML Editor, errors are shown in ValidationErrorDisplay (inside content area), so no footer margin needed */
104
- margin-bottom: ${({ shouldApplyFooterMargin }) => (shouldApplyFooterMargin ? `${CAP_SPACE_16}` : '0')};
105
- padding: 0 rem;
106
- &.has-footer{
107
- overflow-x: hidden;
108
- }
109
- }
110
- }
111
- `;
112
77
  export class Creatives extends React.Component {
113
78
  constructor(props) {
114
79
  super(props);
115
80
 
116
- const initialSlidBoxContent = this.getSlideBoxContent({ mode: props.creativesMode, templateData: props.templateData, isFullMode: props.isFullMode });
81
+ const useLocalTemplates = get(
82
+ props,
83
+ 'localTemplatesConfig.useLocalTemplates',
84
+ get(props, 'useLocalTemplates', false),
85
+ );
86
+ const initialSlidBoxContent = this.getSlideBoxContent({
87
+ mode: props.creativesMode,
88
+ templateData: props.templateData,
89
+ isFullMode: props.isFullMode,
90
+ useLocalTemplates,
91
+ });
117
92
 
118
93
  this.state = {
119
94
  isLoadingContent: true,
@@ -160,7 +135,13 @@ export class Creatives extends React.Component {
160
135
  }
161
136
 
162
137
  componentWillUnmount() {
163
- if (get(this.props, 'location.query.type', '') === "embedded") {
138
+ const isEmbedded = get(this.props, 'location.query.type', '') === "embedded";
139
+ const useLocalTemplates = get(
140
+ this.props,
141
+ 'localTemplatesConfig.useLocalTemplates',
142
+ get(this.props, 'useLocalTemplates', false),
143
+ );
144
+ if (isEmbedded && !useLocalTemplates) {
164
145
  this.props.templateActions.resetTemplateStoreData();
165
146
  }
166
147
  this.props.globalActions.clearMetaEntities();
@@ -763,14 +744,41 @@ export class Creatives extends React.Component {
763
744
  creativeName = "",
764
745
  channel = constants.RCS,
765
746
  accountId = "",
747
+ rcsCardVarMapped,
766
748
  } = templateData || {};
767
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
749
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
750
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
751
+ const {
752
+ cardDisplayTitle: _omitDispTitleIn,
753
+ cardDisplayDescription: _omitDispDescIn,
754
+ ...cardContent
755
+ } = firstCardIn;
768
756
  const Status = RCS_STATUSES.approved || '';
757
+ const mergedCardVarMapped = (() => {
758
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
759
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
760
+ const nestedRecord =
761
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
762
+ ? nestedCardVarMapped
763
+ : {};
764
+ const rootRecord =
765
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
766
+ ? rootMirrorCardVarMapped
767
+ : {};
768
+ const mergedFromRootAndNested = { ...rootRecord, ...nestedRecord };
769
+ return Object.keys(mergedFromRootAndNested).length > 0 ? mergedFromRootAndNested : null;
770
+ })();
771
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
772
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
773
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
774
+ const includeRootRcsCardVarMapped =
775
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
769
776
 
770
777
  creativesTemplateData = {
771
778
  type: channel,
772
779
  edit: true,
773
780
  name: creativeName,
781
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
774
782
  versions: {
775
783
  base: {
776
784
  content: {
@@ -781,6 +789,7 @@ export class Creatives extends React.Component {
781
789
  cardContent: [
782
790
  {
783
791
  ...cardContent,
792
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
784
793
  Status,
785
794
  },
786
795
  ],
@@ -931,7 +940,10 @@ export class Creatives extends React.Component {
931
940
  return newExpandableDetails;
932
941
  }
933
942
 
934
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
943
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
944
+ const channel = String(
945
+ channelParam || template?.type || get(template, 'value.type') || '',
946
+ ).toUpperCase();
935
947
  let templateData = { channel };
936
948
  switch (channel) {
937
949
  case constants.SMS:
@@ -1225,7 +1237,7 @@ export class Creatives extends React.Component {
1225
1237
  };
1226
1238
  }
1227
1239
  break;
1228
- case constants.FACEBOOK: {
1240
+ case constants.FACEBOOK:
1229
1241
  if (template.value) {
1230
1242
  const FacebookAd = template?.value?.versions?.base?.content?.FacebookAd;
1231
1243
  const { type } = FacebookAd[0];
@@ -1269,36 +1281,109 @@ export class Creatives extends React.Component {
1269
1281
  selectedMarketingObjective: template.value.selectedMarketingObjective,
1270
1282
  };
1271
1283
  }
1272
- }
1273
1284
  break;
1274
- case constants.RCS: {
1285
+ case constants.RCS:
1275
1286
  if (template.value) {
1276
- const { name = "", versions = {} } = {
1277
- } = template.value || {};
1278
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1287
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1288
+ const { name = "", versions = {} } = template.value || {};
1289
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1290
+ const fromRecords = {
1291
+ ...(templateRecords?.smsFallBackContent || {}),
1292
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1293
+ };
1294
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1295
+ if (
1296
+ !smsFallbackPayload
1297
+ || typeof smsFallbackPayload !== 'object'
1298
+ || Object.keys(smsFallbackPayload).length === 0
1299
+ ) {
1300
+ return false;
1301
+ }
1302
+ const fallbackBodyText = String(
1303
+ smsFallbackPayload.smsContent
1304
+ ?? smsFallbackPayload.smsTemplateContent
1305
+ ?? smsFallbackPayload.message
1306
+ ?? smsFallbackPayload.content
1307
+ ?? '',
1308
+ ).trim();
1309
+ const fallbackTemplateName = String(
1310
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1311
+ ).trim();
1312
+ const rcsSmsFallbackVarMapped =
1313
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1314
+ const hasVarMappedEntries =
1315
+ rcsSmsFallbackVarMapped != null
1316
+ && typeof rcsSmsFallbackVarMapped === 'object'
1317
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1318
+ return (
1319
+ fallbackBodyText !== ''
1320
+ || fallbackTemplateName !== ''
1321
+ || hasVarMappedEntries
1322
+ );
1323
+ };
1324
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1325
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1326
+ ? { ...fromRecords, ...fromSubmit }
1327
+ : { ...fromSubmit, ...fromRecords };
1279
1328
  const {
1280
- cardContent = [],
1329
+ cardContent: cardContentFromSubmit = [],
1281
1330
  contentType = "",
1282
1331
  cardType = "",
1283
1332
  cardSettings = {},
1284
1333
  accountId = "",
1285
1334
  } = get(versions, 'base.content.RCS.rcsContent', {});
1335
+ const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1336
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1337
+ ? cardContentFromSubmit[0]
1338
+ : null;
1339
+ const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
1340
+ cardContentFromSubmit,
1341
+ rootRcsCardVarMappedFromSubmit,
1342
+ isFullModeForRcsConsumerPayload,
1343
+ );
1286
1344
  const rcsContent = {
1287
1345
  contentType,
1288
1346
  cardType,
1289
1347
  cardSettings,
1290
1348
  cardContent,
1291
1349
  };
1350
+ const cardVarMappedFromFirstRcsCard =
1351
+ firstCardFromSubmit?.cardVarMapped != null
1352
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1353
+ ? firstCardFromSubmit.cardVarMapped
1354
+ : null;
1355
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1356
+ cardVarMappedFromFirstRcsCard
1357
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1358
+ && isFullModeForRcsConsumerPayload === true;
1292
1359
  templateData = {
1293
1360
  channel,
1294
1361
  creativeName: name,
1295
1362
  rcsContent,
1296
1363
  accountId: accountId,
1364
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1365
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1366
+ : {}),
1297
1367
  };
1368
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1369
+ // so reopening the editor restores fallback text and tag mappings.
1370
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1371
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1372
+ const smsText =
1373
+ smsFallBackContent.message
1374
+ ?? smsFallBackContent.smsContent
1375
+ ?? smsFallBackContent.smsTemplateContent
1376
+ ?? '';
1377
+ templateData.smsFallBackContent = {
1378
+ ...smsFallBackContent,
1379
+ ...(String(smsText).trim() !== ''
1380
+ ? { message: String(smsText).trim() }
1381
+ : {}),
1382
+ };
1383
+ }
1298
1384
  }
1299
- }
1300
1385
  break;
1301
- case constants.ZALO: {
1386
+ case constants.ZALO:
1302
1387
  if (template.value) {
1303
1388
  templateData = {
1304
1389
  ...template.value,
@@ -1307,7 +1392,6 @@ export class Creatives extends React.Component {
1307
1392
  delete templateData.type;
1308
1393
  }
1309
1394
  }
1310
- }
1311
1395
  break;
1312
1396
  case constants.WEBPUSH: {
1313
1397
  if (template.value) {
@@ -1414,7 +1498,10 @@ export class Creatives extends React.Component {
1414
1498
  return templateData;
1415
1499
  };
1416
1500
 
1417
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1501
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1502
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1503
+ return 'templates';
1504
+ }
1418
1505
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1419
1506
  if (mode === 'create' && isFullMode) {
1420
1507
  creativesMode = 'createTemplate';
@@ -1502,24 +1589,110 @@ export class Creatives extends React.Component {
1502
1589
  getFormData = (template) => {
1503
1590
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1504
1591
  // (e.g. when user fixes validation error by typing, we must not auto-close the slidebox)
1505
- this.setState({ isGetFormData: false });
1506
- if (template.validity) {
1507
- this.setState(
1508
- {},
1509
- () => {
1510
- const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1511
- const channel = templateData.type;
1512
- const creativesData = this.getCreativesData(channel, template, templateData);// convers data to consumer understandable format
1513
- creativesData.then((data) => {
1514
- this.logGTMEvent(channel, data);
1515
- this.processCentralCommsMetaId(channel, data);
1592
+ this.setState(
1593
+ (prevState) => {
1594
+ const next = { isGetFormData: false };
1595
+ if (!template.validity) {
1596
+ return next;
1597
+ }
1598
+ const baseTd = prevState.templateData || template;
1599
+ const channel = (
1600
+ baseTd?.type
1601
+ || template?.type
1602
+ || get(template, 'value.type')
1603
+ || ''
1604
+ ).toUpperCase();
1605
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1606
+ // (parent may not merge getCreativesData back into templateData).
1607
+ if (this.props.isFullMode === false && template.value) {
1608
+ if (channel === constants.RCS) {
1609
+ const smsFallBackFromPayload = get(
1610
+ template.value,
1611
+ 'versions.base.content.RCS.smsFallBackContent',
1612
+ );
1613
+ const rcsCardVarMappedFromPayload = get(
1614
+ template.value,
1615
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1616
+ );
1617
+ next.templateData = {
1618
+ ...baseTd,
1619
+ type: constants.RCS,
1620
+ name: template?.value?.name,
1621
+ versions: template?.value?.versions,
1622
+ ...(smsFallBackFromPayload != null
1623
+ && typeof smsFallBackFromPayload === 'object'
1624
+ && Object.keys(smsFallBackFromPayload).length > 0
1625
+ ? { smsFallBackContent: smsFallBackFromPayload }
1626
+ : {}),
1627
+ ...(rcsCardVarMappedFromPayload != null
1628
+ && typeof rcsCardVarMappedFromPayload === 'object'
1629
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1630
+ : {}),
1631
+ };
1632
+ if (template._id) {
1633
+ next.templateData._id = template._id;
1634
+ }
1635
+ } else if (channel === constants.SMS) {
1636
+ const submittedSmsTemplateValue = template?.value;
1637
+ const smsVersions =
1638
+ submittedSmsTemplateValue?.history != null
1639
+ ? submittedSmsTemplateValue
1640
+ : {
1641
+ base: submittedSmsTemplateValue?.base,
1642
+ history: submittedSmsTemplateValue?.base
1643
+ ? [submittedSmsTemplateValue.base]
1644
+ : [],
1645
+ };
1646
+ next.templateData = {
1647
+ ...baseTd,
1648
+ type: constants.SMS,
1649
+ name: baseTd?.name || 'Campaign message SMS content',
1650
+ versions: smsVersions,
1651
+ };
1652
+ if (template?._id) {
1653
+ next.templateData._id = template._id;
1654
+ }
1655
+ }
1656
+ }
1657
+ return next;
1658
+ },
1659
+ () => {
1660
+ if (!template.validity) {
1661
+ return;
1662
+ }
1663
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1664
+ const channelForConsumer = String(
1665
+ templateData.type
1666
+ || template.type
1667
+ || get(template, 'value.type')
1668
+ || '',
1669
+ ).toUpperCase();
1670
+ const creativesData = this.getCreativesData(
1671
+ channelForConsumer,
1672
+ template,
1673
+ this.state.templateData || template,
1674
+ );// convers data to consumer understandable format
1675
+ creativesData.then((data) => {
1676
+ this.logGTMEvent(channelForConsumer, data);
1677
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1678
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1516
1679
  });
1517
- },
1518
- );
1519
- }
1680
+ });
1681
+ },
1682
+ );
1520
1683
  }
1521
1684
 
1522
- processCentralCommsMetaId = (channel, creativesData) => {
1685
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1686
+ const { closeSlideBoxAfterSubmit = false } = options;
1687
+ const maybeCloseLibrarySlideBox = () => {
1688
+ if (
1689
+ closeSlideBoxAfterSubmit
1690
+ && this.props.isFullMode === false
1691
+ && typeof this.handleCloseSlideBox === 'function'
1692
+ ) {
1693
+ this.handleCloseSlideBox();
1694
+ }
1695
+ };
1523
1696
  // Create the payload for the centralcommnsmetaId API call
1524
1697
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1525
1698
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1545,6 +1718,7 @@ export class Creatives extends React.Component {
1545
1718
  if (result?.status?.code === 200) {
1546
1719
  setMetaData(result);
1547
1720
  this.props.getCreativesData(creativesData);
1721
+ maybeCloseLibrarySlideBox();
1548
1722
  } else {
1549
1723
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1550
1724
  }
@@ -1555,6 +1729,7 @@ export class Creatives extends React.Component {
1555
1729
  } else {
1556
1730
  // If not a loyalty module or different action, should work as usual
1557
1731
  this.props.getCreativesData(creativesData);
1732
+ maybeCloseLibrarySlideBox();
1558
1733
  }
1559
1734
  };
1560
1735
 
@@ -1587,7 +1762,9 @@ export class Creatives extends React.Component {
1587
1762
  }
1588
1763
  this.setState((prevState) => ({
1589
1764
  ...prevState,
1590
- templateData: undefined,
1765
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1766
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1767
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1591
1768
  showSlideBox: false,
1592
1769
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1593
1770
  isLiquidValidationError: false,
@@ -1798,21 +1975,12 @@ export class Creatives extends React.Component {
1798
1975
  }
1799
1976
 
1800
1977
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1801
- const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1802
- const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1803
- const hasLiquid = !isDeepEmpty(liquidMsgs);
1804
- const hasStandard = !isDeepEmpty(standardMsgs);
1805
- const isLiquidValidationError = hasLiquid || hasStandard;
1806
- // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1807
- const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
1808
- if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
1809
- return;
1810
- }
1811
- this.setState({
1812
- isLiquidValidationError,
1813
- liquidErrorMessage: errorMessagesFromFormBuilder,
1814
- activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
1978
+ const next = computeLiquidFooterUpdateFromFormBuilder(errorMessagesFromFormBuilder, currentFormBuilderTab, {
1979
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
1980
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
1815
1981
  });
1982
+ if (next == null) return;
1983
+ this.setState(next);
1816
1984
  }
1817
1985
 
1818
1986
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -1935,6 +2103,11 @@ export class Creatives extends React.Component {
1935
2103
  inAppEditorType,
1936
2104
  htmlEditorValidationState,
1937
2105
  } = this.state;
2106
+ const useLocalTemplates = get(
2107
+ this.props,
2108
+ 'localTemplatesConfig.useLocalTemplates',
2109
+ get(this.props, 'useLocalTemplates', false),
2110
+ );
1938
2111
  const {
1939
2112
  isFullMode,
1940
2113
  creativesMode,
@@ -1986,14 +2159,7 @@ export class Creatives extends React.Component {
1986
2159
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
1987
2160
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
1988
2161
 
1989
- // Calculate margin for header/content (always apply if there are errors, regardless of editor type)
1990
- const slideBoxWrapperMargin = (get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0 && get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0)
1991
- ? CAP_SPACE_64
1992
- : get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0
1993
- ? CAP_SPACE_56
1994
- : get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0
1995
- ? CAP_SPACE_32
1996
- : 0;
2162
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
1997
2163
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
1998
2164
 
1999
2165
  // Compute anonymous user type and channel restrictions
@@ -2022,7 +2188,10 @@ export class Creatives extends React.Component {
2022
2188
  <SlideBoxWrapper
2023
2189
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2024
2190
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2025
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2191
+ className={classnames(
2192
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2193
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2194
+ )}
2026
2195
  >
2027
2196
  <CapSlideBox
2028
2197
  header={
@@ -2047,12 +2216,13 @@ export class Creatives extends React.Component {
2047
2216
  smsRegister={smsRegister}
2048
2217
  handleClose={this.handleCloseSlideBox}
2049
2218
  moduleType={this.props.messageDetails?.type}
2219
+ useLocalTemplates={useLocalTemplates}
2050
2220
  />
2051
2221
  )}
2052
2222
  content={(
2053
2223
  <SlideBoxContent
2054
2224
  key="creatives-container-slidebox-content"
2055
- onSelectTemplate={this.onSelectTemplate}
2225
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2056
2226
  onCreateComplete={getCreativesData}
2057
2227
  onPreviewTemplate={this.onPreviewTemplate}
2058
2228
  slidBoxContent={slidBoxContent}
@@ -2128,7 +2298,8 @@ export class Creatives extends React.Component {
2128
2298
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2129
2299
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2130
2300
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2131
- />
2301
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2302
+ />
2132
2303
  )}
2133
2304
  footer={this.shouldShowFooter() ? (
2134
2305
  <SlideBoxFooter
@@ -2218,6 +2389,25 @@ Creatives.propTypes = {
2218
2389
  formatMessage: PropTypes.func,
2219
2390
  }),
2220
2391
  stopValidation: PropTypes.func,
2392
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2393
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2394
+ localTemplatesConfig: PropTypes.shape({
2395
+ useLocalTemplates: PropTypes.bool,
2396
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2397
+ localTemplatesLoading: PropTypes.bool,
2398
+ localTemplatesFilterContent: PropTypes.node,
2399
+ localTemplatesSentinelContent: PropTypes.node,
2400
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2401
+ localTemplatesUseSkeleton: PropTypes.bool,
2402
+ }),
2403
+ useLocalTemplates: PropTypes.bool,
2404
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2405
+ localTemplatesLoading: PropTypes.bool,
2406
+ localTemplatesFilterContent: PropTypes.node,
2407
+ localTemplatesSentinelContent: PropTypes.node,
2408
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2409
+ localTemplatesUseSkeleton: PropTypes.bool,
2410
+ onSelectTemplate: PropTypes.func,
2221
2411
  };
2222
2412
  const mapStatesToProps = () => createStructuredSelector({
2223
2413
  isLoading: isLoadingSelector(),
@@ -2,6 +2,43 @@
2
2
 
3
3
  $classPrefix: add-creatives-section;
4
4
 
5
+ /* Local SMS template picker: fill slidebox height; global .v2-pagination-container uses 100vh-20rem and leaves a dead zone inside slideboxes */
6
+ .#{$classPrefix}.creatives-slidebox--local-sms-templates {
7
+ .cap-slide-box-v2-container {
8
+ display: flex;
9
+ flex-direction: column;
10
+ min-height: 0;
11
+ max-height: 100vh;
12
+ }
13
+
14
+ .slidebox-content-container {
15
+ flex: 1;
16
+ min-height: 0;
17
+ display: flex;
18
+ flex-direction: column;
19
+ overflow: hidden;
20
+ }
21
+
22
+ .slidebox-content-container > div {
23
+ flex: 1;
24
+ min-height: 0;
25
+ display: flex;
26
+ flex-direction: column;
27
+ overflow: hidden;
28
+ }
29
+
30
+ /* TemplatesV2 root: fill slidebox so the template grid can flex instead of using 100vh-based pagination height */
31
+ .slidebox-content-container .creatives-templates-container--local-sms.library-mode {
32
+ flex: 1;
33
+ min-height: 0;
34
+ display: flex;
35
+ flex-direction: column;
36
+ overflow: hidden;
37
+ height: auto;
38
+ max-height: 100%;
39
+ }
40
+ }
41
+
5
42
  .#{$classPrefix} {
6
43
  &.creatives-library-mode{
7
44
  .sms-create-container, .sms-email-container{
@@ -80,5 +117,18 @@ $classPrefix: add-creatives-section;
80
117
  }
81
118
 
82
119
  .template-footer-width {
83
- width: 100%;;
120
+ width: 100%;
121
+ }
122
+
123
+ .slidebox-footer-actions {
124
+ display: flex;
125
+ flex-wrap: nowrap;
126
+ align-items: center;
127
+ gap: 0.75rem;
128
+ min-width: 0;
129
+
130
+ .ant-btn,
131
+ button {
132
+ flex-shrink: 0;
133
+ }
84
134
  }