@capillarytech/creatives-library 8.0.345-alpha.16 → 8.0.345-alpha.17

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 (127) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +13 -0
  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 +167 -109
  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 +70 -49
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -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/RcsPreviewContent.js +160 -15
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  26. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  27. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  28. package/v2Components/CommonTestAndPreview/index.js +676 -186
  29. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  30. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  31. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  32. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  33. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  36. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  37. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  38. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  40. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  41. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  42. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  43. package/v2Components/FormBuilder/index.js +8 -10
  44. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  45. package/v2Components/SmsFallback/constants.js +73 -0
  46. package/v2Components/SmsFallback/index.js +955 -0
  47. package/v2Components/SmsFallback/index.scss +265 -0
  48. package/v2Components/SmsFallback/messages.js +78 -0
  49. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -0
  50. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  51. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  52. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
  55. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  56. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  57. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  58. package/v2Components/TemplatePreview/constants.js +2 -0
  59. package/v2Components/TemplatePreview/index.js +143 -28
  60. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  61. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  62. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  63. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  64. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  65. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  66. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  67. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  68. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  69. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  70. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  71. package/v2Containers/CreativesContainer/constants.js +9 -0
  72. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  73. package/v2Containers/CreativesContainer/index.js +294 -96
  74. package/v2Containers/CreativesContainer/index.scss +51 -1
  75. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  77. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  81. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  82. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  83. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  84. package/v2Containers/Email/reducer.js +3 -12
  85. package/v2Containers/Email/sagas.js +4 -9
  86. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +0 -4
  87. package/v2Containers/Email/tests/reducer.test.js +0 -47
  88. package/v2Containers/Email/tests/sagas.test.js +6 -146
  89. package/v2Containers/Rcs/constants.js +119 -10
  90. package/v2Containers/Rcs/index.js +2456 -815
  91. package/v2Containers/Rcs/index.scss +276 -6
  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 +100 -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 +636 -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 +163 -2
  114. package/v2Containers/Templates/actions.js +11 -0
  115. package/v2Containers/Templates/constants.js +2 -0
  116. package/v2Containers/Templates/index.js +119 -54
  117. package/v2Containers/Templates/sagas.js +57 -12
  118. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  119. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  120. package/v2Containers/Templates/tests/sagas.test.js +193 -123
  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/Whatsapp/index.js +3 -20
  127. 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
 
@@ -32,6 +27,7 @@ import SlideBoxContent from './SlideBoxContent';
32
27
  import * as constants from './constants';
33
28
  import * as commonUtil from '../../utils/common';
34
29
  import { gtmPush } from '../../utils/gtmTrackers';
30
+ import { normalizeRcsMessageContentForApi } from '../../utils/rcsPayloadUtils';
35
31
  import './index.scss';
36
32
  import * as templateActions from '../Templates/actions';
37
33
  import * as globalActions from '../Cap/actions';
@@ -47,6 +43,9 @@ import {
47
43
  import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants';
48
44
  import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
49
45
  import { RCS_STATUSES } from '../Rcs/constants';
46
+ import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
47
+ import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
48
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
50
49
  import { CREATIVE } from '../Facebook/constants';
51
50
  import { LOYALTY } from '../App/constants';
52
51
  import {
@@ -61,6 +60,11 @@ import { capSagaForFetchSchemaForEntity, capSagaLiquidEntity } from '../Cap/saga
61
60
  import { v2TemplateSagaWatchGetDefaultBeeTemplates } from '../Templates/sagas';
62
61
  import { DYNAMIC_URL } from '../../v2Components/CapWhatsappCTA/constants';
63
62
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
63
+ import SlideBoxWrapper from './CreativesSlideBoxWrapper';
64
+ import {
65
+ computeLiquidFooterUpdateFromFormBuilder,
66
+ getSlideBoxWrapperMarginFromLiquidErrors,
67
+ } from './embeddedSlideboxUtils';
64
68
 
65
69
  import {
66
70
  transformChannelPayload,
@@ -69,51 +73,24 @@ import {
69
73
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
70
74
  import { BIG_HTML } from '../InApp/constants';
71
75
 
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
76
  const classPrefix = 'add-creatives-section';
90
77
  const CREATIVES_CONTAINER = 'creativesContainer';
91
78
 
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
79
  export class Creatives extends React.Component {
113
80
  constructor(props) {
114
81
  super(props);
115
82
 
116
- const initialSlidBoxContent = this.getSlideBoxContent({ mode: props.creativesMode, templateData: props.templateData, isFullMode: props.isFullMode });
83
+ const useLocalTemplates = get(
84
+ props,
85
+ 'localTemplatesConfig.useLocalTemplates',
86
+ get(props, 'useLocalTemplates', false),
87
+ );
88
+ const initialSlidBoxContent = this.getSlideBoxContent({
89
+ mode: props.creativesMode,
90
+ templateData: props.templateData,
91
+ isFullMode: props.isFullMode,
92
+ useLocalTemplates,
93
+ });
117
94
 
118
95
  this.state = {
119
96
  isLoadingContent: true,
@@ -160,7 +137,13 @@ export class Creatives extends React.Component {
160
137
  }
161
138
 
162
139
  componentWillUnmount() {
163
- if (get(this.props, 'location.query.type', '') === "embedded") {
140
+ const isEmbedded = get(this.props, 'location.query.type', '') === "embedded";
141
+ const useLocalTemplates = get(
142
+ this.props,
143
+ 'localTemplatesConfig.useLocalTemplates',
144
+ get(this.props, 'useLocalTemplates', false),
145
+ );
146
+ if (isEmbedded && !useLocalTemplates) {
164
147
  this.props.templateActions.resetTemplateStoreData();
165
148
  }
166
149
  this.props.globalActions.clearMetaEntities();
@@ -762,15 +745,47 @@ export class Creatives extends React.Component {
762
745
  smsFallBackContent = {},
763
746
  creativeName = "",
764
747
  channel = constants.RCS,
748
+ rcsCardVarMapped,
765
749
  accountId = "",
766
750
  } = templateData || {};
767
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
751
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
752
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
753
+ const {
754
+ cardDisplayTitle: _omitDispTitleIn,
755
+ cardDisplayDescription: _omitDispDescIn,
756
+ ...cardContent
757
+ } = firstCardIn;
768
758
  const Status = RCS_STATUSES.approved || '';
759
+ const mergedCardVarMapped = (() => {
760
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
761
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
762
+ const nestedRecord =
763
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
764
+ ? nestedCardVarMapped
765
+ : {};
766
+ const rootRecord =
767
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
768
+ ? rootMirrorCardVarMapped
769
+ : {};
770
+ const mergedFromRootAndNested = {
771
+ ...pickRcsCardVarMappedEntries(rootRecord),
772
+ ...pickRcsCardVarMappedEntries(nestedRecord),
773
+ };
774
+ return Object.keys(mergedFromRootAndNested).length > 0
775
+ ? mergedFromRootAndNested
776
+ : null;
777
+ })();
778
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
779
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
780
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
781
+ const includeRootRcsCardVarMapped =
782
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
769
783
 
770
784
  creativesTemplateData = {
771
785
  type: channel,
772
786
  edit: true,
773
787
  name: creativeName,
788
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
774
789
  versions: {
775
790
  base: {
776
791
  content: {
@@ -781,6 +796,7 @@ export class Creatives extends React.Component {
781
796
  cardContent: [
782
797
  {
783
798
  ...cardContent,
799
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
784
800
  Status,
785
801
  },
786
802
  ],
@@ -931,7 +947,10 @@ export class Creatives extends React.Component {
931
947
  return newExpandableDetails;
932
948
  }
933
949
 
934
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
950
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
951
+ const channel = String(
952
+ channelParam || template?.type || get(template, 'value.type') || '',
953
+ ).toUpperCase();
935
954
  let templateData = { channel };
936
955
  switch (channel) {
937
956
  case constants.SMS:
@@ -1272,28 +1291,104 @@ export class Creatives extends React.Component {
1272
1291
  break;
1273
1292
  case constants.RCS:
1274
1293
  if (template.value) {
1275
- const { name = "", versions = {} } = {
1276
- } = template.value || {};
1277
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1294
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1295
+ const { name = "", versions = {} } = template.value || {};
1296
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1297
+ const fromRecords = {
1298
+ ...(templateRecords?.smsFallBackContent || {}),
1299
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1300
+ };
1301
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1302
+ if (
1303
+ !smsFallbackPayload
1304
+ || typeof smsFallbackPayload !== 'object'
1305
+ || Object.keys(smsFallbackPayload).length === 0
1306
+ ) {
1307
+ return false;
1308
+ }
1309
+ const fallbackBodyText = String(
1310
+ smsFallbackPayload.smsContent
1311
+ ?? smsFallbackPayload.smsTemplateContent
1312
+ ?? smsFallbackPayload.message
1313
+ ?? smsFallbackPayload.content
1314
+ ?? '',
1315
+ ).trim();
1316
+ const fallbackTemplateName = String(
1317
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1318
+ ).trim();
1319
+ const rcsSmsFallbackVarMapped =
1320
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1321
+ const hasVarMappedEntries =
1322
+ rcsSmsFallbackVarMapped != null
1323
+ && typeof rcsSmsFallbackVarMapped === 'object'
1324
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1325
+ return (
1326
+ fallbackBodyText !== ''
1327
+ || fallbackTemplateName !== ''
1328
+ || hasVarMappedEntries
1329
+ );
1330
+ };
1331
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1332
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1333
+ ? { ...fromRecords, ...fromSubmit }
1334
+ : { ...fromSubmit, ...fromRecords };
1278
1335
  const {
1279
- cardContent = [],
1336
+ cardContent: cardContentFromSubmit = [],
1280
1337
  contentType = "",
1281
1338
  cardType = "",
1282
1339
  cardSettings = {},
1283
1340
  accountId = "",
1284
1341
  } = get(versions, 'base.content.RCS.rcsContent', {});
1342
+ const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1343
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1344
+ ? cardContentFromSubmit[0]
1345
+ : null;
1346
+ const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
1347
+ cardContentFromSubmit,
1348
+ rootRcsCardVarMappedFromSubmit,
1349
+ isFullModeForRcsConsumerPayload,
1350
+ );
1285
1351
  const rcsContent = {
1286
1352
  contentType,
1287
1353
  cardType,
1288
1354
  cardSettings,
1289
1355
  cardContent,
1290
1356
  };
1357
+ const cardVarMappedFromFirstRcsCard =
1358
+ firstCardFromSubmit?.cardVarMapped != null
1359
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1360
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1361
+ : null;
1362
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1363
+ cardVarMappedFromFirstRcsCard
1364
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1365
+ && isFullModeForRcsConsumerPayload === true;
1291
1366
  templateData = {
1292
1367
  channel,
1293
1368
  creativeName: name,
1294
1369
  rcsContent,
1295
1370
  accountId,
1371
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1372
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1373
+ : {}),
1296
1374
  };
1375
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1376
+ // so reopening the editor restores fallback text and tag mappings.
1377
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1378
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1379
+ const smsText =
1380
+ smsFallBackContent.message
1381
+ ?? smsFallBackContent.smsContent
1382
+ ?? smsFallBackContent.smsTemplateContent
1383
+ ?? '';
1384
+ templateData.smsFallBackContent = {
1385
+ ...smsFallBackContent,
1386
+ ...(String(smsText).trim() !== ''
1387
+ ? { message: String(smsText).trim() }
1388
+ : {}),
1389
+ };
1390
+ }
1391
+ normalizeRcsMessageContentForApi(templateData);
1297
1392
  }
1298
1393
  break;
1299
1394
  case constants.ZALO:
@@ -1411,7 +1506,10 @@ export class Creatives extends React.Component {
1411
1506
  return templateData;
1412
1507
  };
1413
1508
 
1414
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1509
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1510
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1511
+ return 'templates';
1512
+ }
1415
1513
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1416
1514
  if (mode === 'create' && isFullMode) {
1417
1515
  creativesMode = 'createTemplate';
@@ -1499,24 +1597,110 @@ export class Creatives extends React.Component {
1499
1597
  getFormData = (template) => {
1500
1598
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1501
1599
  // (e.g. when user fixes validation error by typing, we must not auto-close the slidebox)
1502
- this.setState({ isGetFormData: false });
1503
- if (template.validity) {
1504
- this.setState(
1505
- {},
1506
- () => {
1507
- const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1508
- const channel = templateData.type;
1509
- const creativesData = this.getCreativesData(channel, template, templateData);// convers data to consumer understandable format
1510
- creativesData.then((data) => {
1511
- this.logGTMEvent(channel, data);
1512
- this.processCentralCommsMetaId(channel, data);
1600
+ this.setState(
1601
+ (prevState) => {
1602
+ const next = { isGetFormData: false };
1603
+ if (!template.validity) {
1604
+ return next;
1605
+ }
1606
+ const baseTd = prevState.templateData || template;
1607
+ const channel = (
1608
+ baseTd?.type
1609
+ || template?.type
1610
+ || get(template, 'value.type')
1611
+ || ''
1612
+ ).toUpperCase();
1613
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1614
+ // (parent may not merge getCreativesData back into templateData).
1615
+ if (this.props.isFullMode === false && template.value) {
1616
+ if (channel === constants.RCS) {
1617
+ const smsFallBackFromPayload = get(
1618
+ template.value,
1619
+ 'versions.base.content.RCS.smsFallBackContent',
1620
+ );
1621
+ const rcsCardVarMappedFromPayload = get(
1622
+ template.value,
1623
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1624
+ );
1625
+ next.templateData = {
1626
+ ...baseTd,
1627
+ type: constants.RCS,
1628
+ name: template?.value?.name,
1629
+ versions: template?.value?.versions,
1630
+ ...(smsFallBackFromPayload != null
1631
+ && typeof smsFallBackFromPayload === 'object'
1632
+ && Object.keys(smsFallBackFromPayload).length > 0
1633
+ ? { smsFallBackContent: smsFallBackFromPayload }
1634
+ : {}),
1635
+ ...(rcsCardVarMappedFromPayload != null
1636
+ && typeof rcsCardVarMappedFromPayload === 'object'
1637
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1638
+ : {}),
1639
+ };
1640
+ if (template._id) {
1641
+ next.templateData._id = template._id;
1642
+ }
1643
+ } else if (channel === constants.SMS) {
1644
+ const submittedSmsTemplateValue = template?.value;
1645
+ const smsVersions =
1646
+ submittedSmsTemplateValue?.history != null
1647
+ ? submittedSmsTemplateValue
1648
+ : {
1649
+ base: submittedSmsTemplateValue?.base,
1650
+ history: submittedSmsTemplateValue?.base
1651
+ ? [submittedSmsTemplateValue.base]
1652
+ : [],
1653
+ };
1654
+ next.templateData = {
1655
+ ...baseTd,
1656
+ type: constants.SMS,
1657
+ name: baseTd?.name || 'Campaign message SMS content',
1658
+ versions: smsVersions,
1659
+ };
1660
+ if (template?._id) {
1661
+ next.templateData._id = template._id;
1662
+ }
1663
+ }
1664
+ }
1665
+ return next;
1666
+ },
1667
+ () => {
1668
+ if (!template.validity) {
1669
+ return;
1670
+ }
1671
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1672
+ const channelForConsumer = String(
1673
+ templateData.type
1674
+ || template.type
1675
+ || get(template, 'value.type')
1676
+ || '',
1677
+ ).toUpperCase();
1678
+ const creativesData = this.getCreativesData(
1679
+ channelForConsumer,
1680
+ template,
1681
+ this.state.templateData || template,
1682
+ );// convers data to consumer understandable format
1683
+ creativesData.then((data) => {
1684
+ this.logGTMEvent(channelForConsumer, data);
1685
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1686
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1513
1687
  });
1514
- },
1515
- );
1516
- }
1688
+ });
1689
+ },
1690
+ );
1517
1691
  }
1518
1692
 
1519
- processCentralCommsMetaId = (channel, creativesData) => {
1693
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1694
+ const { closeSlideBoxAfterSubmit = false } = options;
1695
+ const maybeCloseLibrarySlideBox = () => {
1696
+ if (
1697
+ closeSlideBoxAfterSubmit
1698
+ && this.props.isFullMode === false
1699
+ && typeof this.handleCloseSlideBox === 'function'
1700
+ ) {
1701
+ this.handleCloseSlideBox();
1702
+ }
1703
+ };
1520
1704
  // Create the payload for the centralcommnsmetaId API call
1521
1705
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1522
1706
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1542,6 +1726,7 @@ export class Creatives extends React.Component {
1542
1726
  if (result?.status?.code === 200) {
1543
1727
  setMetaData(result);
1544
1728
  this.props.getCreativesData(creativesData);
1729
+ maybeCloseLibrarySlideBox();
1545
1730
  } else {
1546
1731
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1547
1732
  }
@@ -1552,6 +1737,7 @@ export class Creatives extends React.Component {
1552
1737
  } else {
1553
1738
  // If not a loyalty module or different action, should work as usual
1554
1739
  this.props.getCreativesData(creativesData);
1740
+ maybeCloseLibrarySlideBox();
1555
1741
  }
1556
1742
  };
1557
1743
 
@@ -1584,7 +1770,9 @@ export class Creatives extends React.Component {
1584
1770
  }
1585
1771
  this.setState((prevState) => ({
1586
1772
  ...prevState,
1587
- templateData: undefined,
1773
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1774
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1775
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1588
1776
  showSlideBox: false,
1589
1777
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1590
1778
  isLiquidValidationError: false,
@@ -1795,21 +1983,12 @@ export class Creatives extends React.Component {
1795
1983
  }
1796
1984
 
1797
1985
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1798
- const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1799
- const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1800
- const hasLiquid = !isDeepEmpty(liquidMsgs);
1801
- const hasStandard = !isDeepEmpty(standardMsgs);
1802
- const isLiquidValidationError = hasLiquid || hasStandard;
1803
- // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1804
- const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
1805
- if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
1806
- return;
1807
- }
1808
- this.setState({
1809
- isLiquidValidationError,
1810
- liquidErrorMessage: errorMessagesFromFormBuilder,
1811
- activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
1986
+ const next = computeLiquidFooterUpdateFromFormBuilder(errorMessagesFromFormBuilder, currentFormBuilderTab, {
1987
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
1988
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
1812
1989
  });
1990
+ if (next == null) return;
1991
+ this.setState(next);
1813
1992
  }
1814
1993
 
1815
1994
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -1932,6 +2111,11 @@ export class Creatives extends React.Component {
1932
2111
  inAppEditorType,
1933
2112
  htmlEditorValidationState,
1934
2113
  } = this.state;
2114
+ const useLocalTemplates = get(
2115
+ this.props,
2116
+ 'localTemplatesConfig.useLocalTemplates',
2117
+ get(this.props, 'useLocalTemplates', false),
2118
+ );
1935
2119
  const {
1936
2120
  isFullMode,
1937
2121
  creativesMode,
@@ -1950,7 +2134,6 @@ export class Creatives extends React.Component {
1950
2134
  smsRegister,
1951
2135
  enableNewChannels,
1952
2136
  eventContextTags,
1953
- waitEventContextTags = {},
1954
2137
  isLoyaltyModule,
1955
2138
  loyaltyMetaData = {},
1956
2139
  } = this.props;
@@ -1984,14 +2167,7 @@ export class Creatives extends React.Component {
1984
2167
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
1985
2168
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
1986
2169
 
1987
- // Calculate margin for header/content (always apply if there are errors, regardless of editor type)
1988
- const slideBoxWrapperMargin = (get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0 && get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0)
1989
- ? CAP_SPACE_64
1990
- : get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0
1991
- ? CAP_SPACE_56
1992
- : get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0
1993
- ? CAP_SPACE_32
1994
- : 0;
2170
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
1995
2171
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
1996
2172
 
1997
2173
  // Compute anonymous user type and channel restrictions
@@ -2020,7 +2196,10 @@ export class Creatives extends React.Component {
2020
2196
  <SlideBoxWrapper
2021
2197
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2022
2198
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2023
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2199
+ className={classnames(
2200
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2201
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2202
+ )}
2024
2203
  >
2025
2204
  <CapSlideBox
2026
2205
  header={
@@ -2045,12 +2224,13 @@ export class Creatives extends React.Component {
2045
2224
  smsRegister={smsRegister}
2046
2225
  handleClose={this.handleCloseSlideBox}
2047
2226
  moduleType={this.props.messageDetails?.type}
2227
+ useLocalTemplates={useLocalTemplates}
2048
2228
  />
2049
2229
  )}
2050
2230
  content={(
2051
2231
  <SlideBoxContent
2052
2232
  key="creatives-container-slidebox-content"
2053
- onSelectTemplate={this.onSelectTemplate}
2233
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2054
2234
  onCreateComplete={getCreativesData}
2055
2235
  onPreviewTemplate={this.onPreviewTemplate}
2056
2236
  slidBoxContent={slidBoxContent}
@@ -2118,7 +2298,6 @@ export class Creatives extends React.Component {
2118
2298
  creativesMode={creativesMode} // An existing prop that we're using here. Required to ensure correct account details in Edit or Preview in case of Embedded mode.
2119
2299
  hostName={this.props?.hostName || ''}
2120
2300
  eventContextTags={eventContextTags}
2121
- waitEventContextTags={waitEventContextTags}
2122
2301
  isLoyaltyModule={isLoyaltyModule}
2123
2302
  loyaltyMetaData={loyaltyMetaData}
2124
2303
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
@@ -2127,7 +2306,8 @@ export class Creatives extends React.Component {
2127
2306
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2128
2307
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2129
2308
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2130
- />
2309
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2310
+ />
2131
2311
  )}
2132
2312
  footer={this.shouldShowFooter() ? (
2133
2313
  <SlideBoxFooter
@@ -2211,13 +2391,31 @@ Creatives.propTypes = {
2211
2391
  orgUnitId: PropTypes.number,
2212
2392
  hostName: PropTypes.string,
2213
2393
  eventContextTags: PropTypes.array,
2214
- waitEventContextTags: PropTypes.object,
2215
2394
  loyaltyTagFetchingDependencies: PropTypes.object,
2216
2395
  customerType: PropTypes.string,
2217
2396
  intl: PropTypes.shape({
2218
2397
  formatMessage: PropTypes.func,
2219
2398
  }),
2220
2399
  stopValidation: PropTypes.func,
2400
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2401
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2402
+ localTemplatesConfig: PropTypes.shape({
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
+ }),
2411
+ useLocalTemplates: PropTypes.bool,
2412
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2413
+ localTemplatesLoading: PropTypes.bool,
2414
+ localTemplatesFilterContent: PropTypes.node,
2415
+ localTemplatesSentinelContent: PropTypes.node,
2416
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2417
+ localTemplatesUseSkeleton: PropTypes.bool,
2418
+ onSelectTemplate: PropTypes.func,
2221
2419
  };
2222
2420
  const mapStatesToProps = () => createStructuredSelector({
2223
2421
  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
  }