@capillarytech/creatives-library 8.0.353-alpha.4 → 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 (125) 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/RcsPreviewContent.js +157 -15
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -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 +691 -186
  29. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  30. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  31. package/v2Components/CommonTestAndPreview/sagas.js +25 -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 +36 -26
  43. package/v2Components/FormBuilder/index.js +74 -166
  44. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  45. package/v2Components/SmsFallback/constants.js +73 -0
  46. package/v2Components/SmsFallback/index.js +956 -0
  47. package/v2Components/SmsFallback/index.scss +265 -0
  48. package/v2Components/SmsFallback/messages.js +78 -0
  49. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -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 +223 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -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 +38 -23
  58. package/v2Components/TemplatePreview/constants.js +2 -0
  59. package/v2Components/TemplatePreview/index.js +143 -31
  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 +79 -0
  73. package/v2Containers/CreativesContainer/index.js +346 -163
  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/index.js +1 -1
  85. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  86. package/v2Containers/Rcs/constants.js +119 -10
  87. package/v2Containers/Rcs/index.js +2445 -813
  88. package/v2Containers/Rcs/index.scss +280 -8
  89. package/v2Containers/Rcs/messages.js +34 -3
  90. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  91. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  92. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  93. package/v2Containers/Rcs/tests/index.test.js +152 -121
  94. package/v2Containers/Rcs/tests/mockData.js +38 -0
  95. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  96. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  97. package/v2Containers/Rcs/utils.js +478 -11
  98. package/v2Containers/Sms/Create/index.js +106 -40
  99. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  100. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  101. package/v2Containers/SmsTrai/Create/index.js +9 -4
  102. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  103. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  104. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  105. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  106. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  107. package/v2Containers/SmsWrapper/index.js +37 -8
  108. package/v2Containers/TagList/index.js +6 -0
  109. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  110. package/v2Containers/Templates/_templates.scss +166 -9
  111. package/v2Containers/Templates/actions.js +11 -0
  112. package/v2Containers/Templates/constants.js +2 -0
  113. package/v2Containers/Templates/index.js +120 -52
  114. package/v2Containers/Templates/sagas.js +56 -12
  115. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  116. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  117. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  118. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  119. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  120. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  121. package/v2Containers/TemplatesV2/index.js +86 -23
  122. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  123. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  124. package/v2Containers/Whatsapp/index.js +3 -20
  125. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -1,53 +1,5 @@
1
1
  import React from 'react';
2
-
3
- // Isolated input for the email template name field.
4
- // Manages its own value in local state so keystrokes only re-render this
5
- // small component, not the entire CreativesContainer → Email → FormBuilder tree.
6
- class TemplateNameInputField extends React.Component {
7
- constructor(props) {
8
- super(props);
9
- this.state = { localValue: props.initialValue || '' };
10
- }
11
-
12
- componentDidUpdate(prevProps) {
13
- // Sync from props only when the external value changed AND the user hasn't
14
- // diverged from the previous prop value. This handles async data-load in edit
15
- // mode without overwriting what the user is actively typing.
16
- if (
17
- prevProps.initialValue !== this.props.initialValue &&
18
- this.state.localValue === (prevProps.initialValue || '')
19
- ) {
20
- this.setState({ localValue: this.props.initialValue || '' });
21
- }
22
- }
23
-
24
- handleChange = (ev) => {
25
- const { value } = ev.currentTarget;
26
- this.setState({ localValue: value });
27
- if (this.props.onChange) this.props.onChange(value);
28
- };
29
-
30
- handleBlur = () => {
31
- if (this.props.onBlur) this.props.onBlur(this.state.localValue);
32
- };
33
-
34
- render() {
35
- const { onChange: _onChange, initialValue: _initialValue, onBlur: _ob, ...rest } = this.props;
36
- return (
37
- <CapInput
38
- {...rest}
39
- value={this.state.localValue}
40
- onChange={this.handleChange}
41
- onBlur={this.handleBlur}
42
- />
43
- );
44
- }
45
- }
46
2
  import PropTypes from 'prop-types';
47
- import {
48
- CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
49
- } from '@capillarytech/cap-ui-library/styled/variables';
50
-
51
3
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
52
4
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
53
5
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
@@ -57,12 +9,11 @@ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
57
9
  import { injectIntl, FormattedMessage } from 'react-intl';
58
10
  import classnames from 'classnames';
59
11
  import {
60
- isEmpty, get, forEach, cloneDeep, debounce,
12
+ isEmpty, get, forEach, cloneDeep, debounce, pick,
61
13
  } from 'lodash';
62
14
  import { connect } from 'react-redux';
63
15
  import { createStructuredSelector } from 'reselect';
64
16
  import { bindActionCreators, compose } from 'redux';
65
- import styled from 'styled-components';
66
17
  import { GA } from '@capillarytech/cap-ui-utils';
67
18
  import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
68
19
 
@@ -76,6 +27,7 @@ import SlideBoxContent from './SlideBoxContent';
76
27
  import * as constants from './constants';
77
28
  import * as commonUtil from '../../utils/common';
78
29
  import { gtmPush } from '../../utils/gtmTrackers';
30
+ import { normalizeRcsMessageContentForApi } from '../../utils/rcsPayloadUtils';
79
31
  import './index.scss';
80
32
  import * as templateActions from '../Templates/actions';
81
33
  import * as globalActions from '../Cap/actions';
@@ -91,6 +43,9 @@ import {
91
43
  import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants';
92
44
  import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
93
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';
94
49
  import { CREATIVE } from '../Facebook/constants';
95
50
  import { LOYALTY } from '../App/constants';
96
51
  import {
@@ -105,6 +60,11 @@ import { capSagaForFetchSchemaForEntity, capSagaLiquidEntity } from '../Cap/saga
105
60
  import { v2TemplateSagaWatchGetDefaultBeeTemplates } from '../Templates/sagas';
106
61
  import { DYNAMIC_URL } from '../../v2Components/CapWhatsappCTA/constants';
107
62
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
63
+ import SlideBoxWrapper from './CreativesSlideBoxWrapper';
64
+ import {
65
+ computeLiquidFooterUpdateFromFormBuilder,
66
+ getSlideBoxWrapperMarginFromLiquidErrors,
67
+ } from './embeddedSlideboxUtils';
108
68
 
109
69
  import {
110
70
  transformChannelPayload,
@@ -113,51 +73,24 @@ import {
113
73
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
114
74
  import { BIG_HTML } from '../InApp/constants';
115
75
 
116
- /**
117
- * Returns true if value is "deep empty": no errors present.
118
- * - null/undefined: empty
119
- * - string: empty if length === 0
120
- * - array: empty if length === 0
121
- * - plain object (e.g. { android: [], ios: [], generic: [] }): empty only if every value is deep-empty
122
- */
123
- function isDeepEmpty(value) {
124
- if (value == null) return true;
125
- if (typeof value === 'string') return value.length === 0;
126
- if (Array.isArray(value)) return value.length === 0;
127
- if (typeof value === 'object') {
128
- return Object.values(value).every(isDeepEmpty);
129
- }
130
- return false;
131
- }
132
-
133
76
  const classPrefix = 'add-creatives-section';
134
77
  const CREATIVES_CONTAINER = 'creativesContainer';
135
78
 
136
- const SlideBoxWrapper = styled.div`
137
- .cap-slide-box-v2-container{
138
- .slidebox-header, .slidebox-content-container{
139
- margin-bottom: ${({ slideBoxWrapperMargin }) => `${slideBoxWrapperMargin}`};
140
- padding: 0 rem;
141
- &.has-footer{
142
- overflow-x: hidden;
143
- }
144
- }
145
- .slidebox-footer{
146
- /* Only apply margin-bottom to footer when ErrorInfoNote is shown in footer (BEE editor) */
147
- /* For HTML Editor, errors are shown in ValidationErrorDisplay (inside content area), so no footer margin needed */
148
- margin-bottom: ${({ shouldApplyFooterMargin }) => (shouldApplyFooterMargin ? `${CAP_SPACE_16}` : '0')};
149
- padding: 0 rem;
150
- &.has-footer{
151
- overflow-x: hidden;
152
- }
153
- }
154
- }
155
- `;
156
79
  export class Creatives extends React.Component {
157
80
  constructor(props) {
158
81
  super(props);
159
82
 
160
- 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
+ });
161
94
 
162
95
  this.state = {
163
96
  isLoadingContent: true,
@@ -204,7 +137,13 @@ export class Creatives extends React.Component {
204
137
  }
205
138
 
206
139
  componentWillUnmount() {
207
- 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) {
208
147
  this.props.templateActions.resetTemplateStoreData();
209
148
  }
210
149
  this.props.globalActions.clearMetaEntities();
@@ -812,15 +751,69 @@ export class Creatives extends React.Component {
812
751
  smsFallBackContent = {},
813
752
  creativeName = "",
814
753
  channel = constants.RCS,
754
+ rcsCardVarMapped,
815
755
  accountId = "",
816
756
  } = templateData || {};
817
- const cardContent = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
757
+ const { isFullMode: isFullModeForRcsPayload } = this.props;
758
+ const isCarouselRcs = (rcsContent?.cardType || '').toString().toLowerCase() === 'carousel';
759
+ const firstCardIn = (rcsContent.cardContent && rcsContent.cardContent[0]) || {};
760
+ const {
761
+ cardDisplayTitle: _omitDispTitleIn,
762
+ cardDisplayDescription: _omitDispDescIn,
763
+ ...cardContent
764
+ } = firstCardIn;
818
765
  const Status = RCS_STATUSES.approved || '';
766
+ const mergedCardVarMapped = (() => {
767
+ const nestedCardVarMapped = cardContent?.cardVarMapped;
768
+ const rootMirrorCardVarMapped = rcsCardVarMapped;
769
+ const nestedRecord =
770
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
771
+ ? nestedCardVarMapped
772
+ : {};
773
+ const rootRecord =
774
+ rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
775
+ ? rootMirrorCardVarMapped
776
+ : {};
777
+ const mergedFromRootAndNested = {
778
+ ...pickRcsCardVarMappedEntries(rootRecord),
779
+ ...pickRcsCardVarMappedEntries(nestedRecord),
780
+ };
781
+ return Object.keys(mergedFromRootAndNested).length > 0
782
+ ? mergedFromRootAndNested
783
+ : null;
784
+ })();
785
+ // Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
786
+ // slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
787
+ // Use `=== true` so omitted/undefined `isFullMode` does not behave like library (avoids duplicate on approval payload).
788
+ const includeRootRcsCardVarMapped =
789
+ mergedCardVarMapped && isFullModeForRcsPayload === true;
790
+
791
+ const builtCardContent = isCarouselRcs
792
+ ? (rcsContent.cardContent || []).map((card, idx) => {
793
+ const {
794
+ cardDisplayTitle: _dt,
795
+ cardDisplayDescription: _dd,
796
+ ...restCard
797
+ } = card || {};
798
+ return {
799
+ ...restCard,
800
+ ...(idx === 0 && mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
801
+ Status,
802
+ };
803
+ })
804
+ : [
805
+ {
806
+ ...cardContent,
807
+ ...(mergedCardVarMapped ? { cardVarMapped: mergedCardVarMapped } : {}),
808
+ Status,
809
+ },
810
+ ];
819
811
 
820
812
  creativesTemplateData = {
821
813
  type: channel,
822
814
  edit: true,
823
815
  name: creativeName,
816
+ ...(includeRootRcsCardVarMapped ? { rcsCardVarMapped: mergedCardVarMapped } : {}),
824
817
  versions: {
825
818
  base: {
826
819
  content: {
@@ -828,12 +821,7 @@ export class Creatives extends React.Component {
828
821
  rcsContent: {
829
822
  ...rcsContent,
830
823
  ...(accountId && !isFullMode && { accountId }),
831
- cardContent: [
832
- {
833
- ...cardContent,
834
- Status,
835
- },
836
- ],
824
+ cardContent: builtCardContent,
837
825
  },
838
826
  smsFallBackContent,
839
827
  },
@@ -981,7 +969,10 @@ export class Creatives extends React.Component {
981
969
  return newExpandableDetails;
982
970
  }
983
971
 
984
- getCreativesData = async (channel, template, templateRecords) => { //from creatives to consumers
972
+ getCreativesData = async (channelParam, template, templateRecords) => { //from creatives to consumers
973
+ const channel = String(
974
+ channelParam || template?.type || get(template, 'value.type') || '',
975
+ ).toUpperCase();
985
976
  let templateData = { channel };
986
977
  switch (channel) {
987
978
  case constants.SMS:
@@ -1328,28 +1319,104 @@ export class Creatives extends React.Component {
1328
1319
  break;
1329
1320
  case constants.RCS:
1330
1321
  if (template.value) {
1331
- const { name = "", versions = {} } = {
1332
- } = template.value || {};
1333
- const smsFallBackContent = get(versions, 'base.content.RCS.smsFallBackContent', {});
1322
+ const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
1323
+ const { name = "", versions = {} } = template.value || {};
1324
+ const fromSubmit = get(versions, 'base.content.RCS.smsFallBackContent', {});
1325
+ const fromRecords = {
1326
+ ...(templateRecords?.smsFallBackContent || {}),
1327
+ ...(get(templateRecords, 'versions.base.content.RCS.smsFallBackContent') || {}),
1328
+ };
1329
+ const hasMeaningfulRcsSmsFallback = (smsFallbackPayload) => {
1330
+ if (
1331
+ !smsFallbackPayload
1332
+ || typeof smsFallbackPayload !== 'object'
1333
+ || Object.keys(smsFallbackPayload).length === 0
1334
+ ) {
1335
+ return false;
1336
+ }
1337
+ const fallbackBodyText = String(
1338
+ smsFallbackPayload.smsContent
1339
+ ?? smsFallbackPayload.smsTemplateContent
1340
+ ?? smsFallbackPayload.message
1341
+ ?? smsFallbackPayload.content
1342
+ ?? '',
1343
+ ).trim();
1344
+ const fallbackTemplateName = String(
1345
+ smsFallbackPayload.smsTemplateName ?? smsFallbackPayload.templateName ?? '',
1346
+ ).trim();
1347
+ const rcsSmsFallbackVarMapped =
1348
+ smsFallbackPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
1349
+ const hasVarMappedEntries =
1350
+ rcsSmsFallbackVarMapped != null
1351
+ && typeof rcsSmsFallbackVarMapped === 'object'
1352
+ && Object.keys(rcsSmsFallbackVarMapped).length > 0;
1353
+ return (
1354
+ fallbackBodyText !== ''
1355
+ || fallbackTemplateName !== ''
1356
+ || hasVarMappedEntries
1357
+ );
1358
+ };
1359
+ // If submit has only empty strings, do not let it wipe fallback mirrored on templateRecords (library round-trip).
1360
+ const smsFallBackContent = hasMeaningfulRcsSmsFallback(fromSubmit)
1361
+ ? { ...fromRecords, ...fromSubmit }
1362
+ : { ...fromSubmit, ...fromRecords };
1334
1363
  const {
1335
- cardContent = [],
1364
+ cardContent: cardContentFromSubmit = [],
1336
1365
  contentType = "",
1337
1366
  cardType = "",
1338
1367
  cardSettings = {},
1339
1368
  accountId = "",
1340
1369
  } = get(versions, 'base.content.RCS.rcsContent', {});
1370
+ const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
1371
+ const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
1372
+ ? cardContentFromSubmit[0]
1373
+ : null;
1374
+ const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
1375
+ cardContentFromSubmit,
1376
+ rootRcsCardVarMappedFromSubmit,
1377
+ isFullModeForRcsConsumerPayload,
1378
+ );
1341
1379
  const rcsContent = {
1342
1380
  contentType,
1343
1381
  cardType,
1344
1382
  cardSettings,
1345
1383
  cardContent,
1346
1384
  };
1385
+ const cardVarMappedFromFirstRcsCard =
1386
+ firstCardFromSubmit?.cardVarMapped != null
1387
+ && typeof firstCardFromSubmit.cardVarMapped === 'object'
1388
+ ? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
1389
+ : null;
1390
+ const includeRootRcsCardVarMappedOnConsumerPayload =
1391
+ cardVarMappedFromFirstRcsCard
1392
+ && Object.keys(cardVarMappedFromFirstRcsCard).length > 0
1393
+ && isFullModeForRcsConsumerPayload === true;
1347
1394
  templateData = {
1348
1395
  channel,
1349
1396
  creativeName: name,
1350
1397
  rcsContent,
1351
1398
  accountId,
1399
+ ...(includeRootRcsCardVarMappedOnConsumerPayload
1400
+ ? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
1401
+ : {}),
1352
1402
  };
1403
+ // Library / campaign consumers round-trip templateData via getTemplateData; include SMS fallback
1404
+ // so reopening the editor restores fallback text and tag mappings.
1405
+ // cap-campaigns-v2 API expects `smsFallBackContent.message` (see normalizeRcsMessageContentForApi).
1406
+ if (hasMeaningfulRcsSmsFallback(smsFallBackContent)) {
1407
+ const smsText =
1408
+ smsFallBackContent.message
1409
+ ?? smsFallBackContent.smsContent
1410
+ ?? smsFallBackContent.smsTemplateContent
1411
+ ?? '';
1412
+ templateData.smsFallBackContent = {
1413
+ ...smsFallBackContent,
1414
+ ...(String(smsText).trim() !== ''
1415
+ ? { message: String(smsText).trim() }
1416
+ : {}),
1417
+ };
1418
+ }
1419
+ normalizeRcsMessageContentForApi(templateData);
1353
1420
  }
1354
1421
  break;
1355
1422
  case constants.ZALO:
@@ -1467,7 +1534,10 @@ export class Creatives extends React.Component {
1467
1534
  return templateData;
1468
1535
  };
1469
1536
 
1470
- getSlideBoxContent({ mode, templateData, isFullMode }) {
1537
+ getSlideBoxContent({ mode, templateData, isFullMode, useLocalTemplates }) {
1538
+ if (useLocalTemplates && mode === 'create' && isEmpty(templateData)) {
1539
+ return 'templates';
1540
+ }
1471
1541
  let creativesMode = isFullMode ? 'createTemplate' : 'templates';// for library mode templates page is initial mode and for full mode createTemplates
1472
1542
  if (mode === 'create' && isFullMode) {
1473
1543
  creativesMode = 'createTemplate';
@@ -1555,24 +1625,110 @@ export class Creatives extends React.Component {
1555
1625
  getFormData = (template) => {
1556
1626
  // Always reset isGetFormData so the child does not re-send form data on every re-render
1557
1627
  // (e.g. when user fixes validation error by typing, we must not auto-close the slidebox)
1558
- this.setState({ isGetFormData: false });
1559
- if (template.validity) {
1560
- this.setState(
1561
- {},
1562
- () => {
1563
- const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1564
- const channel = templateData.type;
1565
- const creativesData = this.getCreativesData(channel, template, templateData);// convers data to consumer understandable format
1566
- creativesData.then((data) => {
1567
- this.logGTMEvent(channel, data);
1568
- this.processCentralCommsMetaId(channel, data);
1628
+ this.setState(
1629
+ (prevState) => {
1630
+ const next = { isGetFormData: false };
1631
+ if (!template.validity) {
1632
+ return next;
1633
+ }
1634
+ const baseTd = prevState.templateData || template;
1635
+ const channel = (
1636
+ baseTd?.type
1637
+ || template?.type
1638
+ || get(template, 'value.type')
1639
+ || ''
1640
+ ).toUpperCase();
1641
+ // Library mode: persist last submitted creatives shape so reopening still hydrates the editor
1642
+ // (parent may not merge getCreativesData back into templateData).
1643
+ if (this.props.isFullMode === false && template.value) {
1644
+ if (channel === constants.RCS) {
1645
+ const smsFallBackFromPayload = get(
1646
+ template.value,
1647
+ 'versions.base.content.RCS.smsFallBackContent',
1648
+ );
1649
+ const rcsCardVarMappedFromPayload = get(
1650
+ template.value,
1651
+ 'versions.base.content.RCS.rcsContent.cardContent[0].cardVarMapped',
1652
+ );
1653
+ next.templateData = {
1654
+ ...baseTd,
1655
+ type: constants.RCS,
1656
+ name: template?.value?.name,
1657
+ versions: template?.value?.versions,
1658
+ ...(smsFallBackFromPayload != null
1659
+ && typeof smsFallBackFromPayload === 'object'
1660
+ && Object.keys(smsFallBackFromPayload).length > 0
1661
+ ? { smsFallBackContent: smsFallBackFromPayload }
1662
+ : {}),
1663
+ ...(rcsCardVarMappedFromPayload != null
1664
+ && typeof rcsCardVarMappedFromPayload === 'object'
1665
+ ? { rcsCardVarMapped: rcsCardVarMappedFromPayload }
1666
+ : {}),
1667
+ };
1668
+ if (template._id) {
1669
+ next.templateData._id = template._id;
1670
+ }
1671
+ } else if (channel === constants.SMS) {
1672
+ const submittedSmsTemplateValue = template?.value;
1673
+ const smsVersions =
1674
+ submittedSmsTemplateValue?.history != null
1675
+ ? submittedSmsTemplateValue
1676
+ : {
1677
+ base: submittedSmsTemplateValue?.base,
1678
+ history: submittedSmsTemplateValue?.base
1679
+ ? [submittedSmsTemplateValue.base]
1680
+ : [],
1681
+ };
1682
+ next.templateData = {
1683
+ ...baseTd,
1684
+ type: constants.SMS,
1685
+ name: baseTd?.name || 'Campaign message SMS content',
1686
+ versions: smsVersions,
1687
+ };
1688
+ if (template?._id) {
1689
+ next.templateData._id = template._id;
1690
+ }
1691
+ }
1692
+ }
1693
+ return next;
1694
+ },
1695
+ () => {
1696
+ if (!template.validity) {
1697
+ return;
1698
+ }
1699
+ const templateData = this.state.templateData ? this.state.templateData : template; //select existing or create new content
1700
+ const channelForConsumer = String(
1701
+ templateData.type
1702
+ || template.type
1703
+ || get(template, 'value.type')
1704
+ || '',
1705
+ ).toUpperCase();
1706
+ const creativesData = this.getCreativesData(
1707
+ channelForConsumer,
1708
+ template,
1709
+ this.state.templateData || template,
1710
+ );// convers data to consumer understandable format
1711
+ creativesData.then((data) => {
1712
+ this.logGTMEvent(channelForConsumer, data);
1713
+ this.processCentralCommsMetaId(channelForConsumer, data, {
1714
+ closeSlideBoxAfterSubmit: template.closeSlideBoxAfterSubmit,
1569
1715
  });
1570
- },
1571
- );
1572
- }
1716
+ });
1717
+ },
1718
+ );
1573
1719
  }
1574
1720
 
1575
- processCentralCommsMetaId = (channel, creativesData) => {
1721
+ processCentralCommsMetaId = (channel, creativesData, options = {}) => {
1722
+ const { closeSlideBoxAfterSubmit = false } = options;
1723
+ const maybeCloseLibrarySlideBox = () => {
1724
+ if (
1725
+ closeSlideBoxAfterSubmit
1726
+ && this.props.isFullMode === false
1727
+ && typeof this.handleCloseSlideBox === 'function'
1728
+ ) {
1729
+ this.handleCloseSlideBox();
1730
+ }
1731
+ };
1576
1732
  // Create the payload for the centralcommnsmetaId API call
1577
1733
  const { isLoyaltyModule = false, loyaltyMetaData = {} } = this.props;
1578
1734
  const { actionName, setMetaData = () => { } } = loyaltyMetaData;
@@ -1598,6 +1754,7 @@ export class Creatives extends React.Component {
1598
1754
  if (result?.status?.code === 200) {
1599
1755
  setMetaData(result);
1600
1756
  this.props.getCreativesData(creativesData);
1757
+ maybeCloseLibrarySlideBox();
1601
1758
  } else {
1602
1759
  CapNotification.error({ message: <FormattedMessage {...messages.somethingWentWrong} /> });
1603
1760
  }
@@ -1608,6 +1765,7 @@ export class Creatives extends React.Component {
1608
1765
  } else {
1609
1766
  // If not a loyalty module or different action, should work as usual
1610
1767
  this.props.getCreativesData(creativesData);
1768
+ maybeCloseLibrarySlideBox();
1611
1769
  }
1612
1770
  };
1613
1771
 
@@ -1640,7 +1798,9 @@ export class Creatives extends React.Component {
1640
1798
  }
1641
1799
  this.setState((prevState) => ({
1642
1800
  ...prevState,
1643
- templateData: undefined,
1801
+ // Library mode (isFullMode === false): retain last template so reopening still has RCS payload.
1802
+ // Undefined isFullMode defaults to full-mode close behavior (clear templateData).
1803
+ ...(this.props.isFullMode !== false ? { templateData: undefined } : {}),
1644
1804
  showSlideBox: false,
1645
1805
  liquidErrorMessage: { STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] },
1646
1806
  isLiquidValidationError: false,
@@ -1797,22 +1957,30 @@ export class Creatives extends React.Component {
1797
1957
  } />
1798
1958
  )
1799
1959
 
1800
- templateNameComponentInput = ({ formData, onFormDataChange, name }) => (
1801
- <TemplateNameInputField
1802
- initialValue={name}
1803
- suffix={<span />}
1804
- onBlur={(committedValue) => {
1805
- this.performTemplateNameUpdate(committedValue, formData, onFormDataChange);
1806
- this.setState({ isEditName: false });
1807
- }}
1808
- onChange={(value) => {
1809
- const isEmptyTemplateName = !value.trim();
1810
- if (this.state.isTemplateNameEmpty !== isEmptyTemplateName) {
1811
- this.setState({ isTemplateNameEmpty: isEmptyTemplateName });
1812
- }
1813
- }}
1814
- />
1815
- )
1960
+ templateNameComponentInput = ({ formData, onFormDataChange, name }) => {
1961
+ // Use local state for immediate UI feedback, fallback to prop value
1962
+ const displayValue = this.state.localTemplateName !== '' ? this.state.localTemplateName : name;
1963
+
1964
+ return (
1965
+ <CapInput
1966
+ value={displayValue}
1967
+ suffix={<span />}
1968
+ onBlur={() => {
1969
+ this.setState({
1970
+ isEditName: false,
1971
+ localTemplateName: '', // Clear local state on blur
1972
+ }, () => {
1973
+ this.showTemplateName({ formData, onFormDataChange });
1974
+ });
1975
+ }}
1976
+ onChange={(ev) => {
1977
+ const { value } = ev.currentTarget;
1978
+ // Use optimized update for better performance
1979
+ this.updateTemplateNameImmediately(value, formData, onFormDataChange);
1980
+ }}
1981
+ />
1982
+ );
1983
+ }
1816
1984
 
1817
1985
  showTemplateName = ({ formData, onFormDataChange }) => { //gets called from email/index after template data is fetched
1818
1986
  const {
@@ -1843,21 +2011,17 @@ export class Creatives extends React.Component {
1843
2011
  }
1844
2012
 
1845
2013
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1846
- const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1847
- const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1848
- const hasLiquid = !isDeepEmpty(liquidMsgs);
1849
- const hasStandard = !isDeepEmpty(standardMsgs);
1850
- const isLiquidValidationError = hasLiquid || hasStandard;
1851
- // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1852
- const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
1853
- if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
1854
- return;
1855
- }
1856
- this.setState({
1857
- isLiquidValidationError,
1858
- liquidErrorMessage: errorMessagesFromFormBuilder,
1859
- activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
1860
- });
2014
+ const next = computeLiquidFooterUpdateFromFormBuilder(
2015
+ errorMessagesFromFormBuilder,
2016
+ this.state.liquidErrorMessage,
2017
+ currentFormBuilderTab,
2018
+ {
2019
+ previousIsLiquidValidationError: this.state.isLiquidValidationError,
2020
+ currentChannelUpper: this.state.currentChannel?.toUpperCase(),
2021
+ },
2022
+ );
2023
+ if (next == null) return;
2024
+ this.setState(next);
1861
2025
  }
1862
2026
 
1863
2027
  // Callback to update HTML Editor validation state (called from EmailWrapper)
@@ -1980,6 +2144,11 @@ export class Creatives extends React.Component {
1980
2144
  inAppEditorType,
1981
2145
  htmlEditorValidationState,
1982
2146
  } = this.state;
2147
+ const useLocalTemplates = get(
2148
+ this.props,
2149
+ 'localTemplatesConfig.useLocalTemplates',
2150
+ get(this.props, 'useLocalTemplates', false),
2151
+ );
1983
2152
  const {
1984
2153
  isFullMode,
1985
2154
  creativesMode,
@@ -1998,7 +2167,6 @@ export class Creatives extends React.Component {
1998
2167
  smsRegister,
1999
2168
  enableNewChannels,
2000
2169
  eventContextTags,
2001
- waitEventContextTags = {},
2002
2170
  isLoyaltyModule,
2003
2171
  loyaltyMetaData = {},
2004
2172
  } = this.props;
@@ -2032,14 +2200,7 @@ export class Creatives extends React.Component {
2032
2200
  // IMPORTANT: Never show ErrorInfoNote in footer when in HTML Editor mode, even if liquidErrorMessage exists
2033
2201
  const shouldShowErrorInfoNoteInFooter = isHTMLEditorMode ? false : hasBEEEditorErrors;
2034
2202
 
2035
- // Calculate margin for header/content (always apply if there are errors, regardless of editor type)
2036
- const slideBoxWrapperMargin = (get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0 && get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0)
2037
- ? CAP_SPACE_64
2038
- : get(liquidErrorMessage, 'LIQUID_ERROR_MSG.length', 0) > 0
2039
- ? CAP_SPACE_56
2040
- : get(liquidErrorMessage, 'STANDARD_ERROR_MSG.length', 0) > 0
2041
- ? CAP_SPACE_32
2042
- : 0;
2203
+ const slideBoxWrapperMargin = getSlideBoxWrapperMarginFromLiquidErrors(liquidErrorMessage);
2043
2204
  /* TODO: Instead of passing down same props separately to each component down, write common function to these props and pass it accordingly */
2044
2205
 
2045
2206
  // Compute anonymous user type and channel restrictions
@@ -2068,7 +2229,10 @@ export class Creatives extends React.Component {
2068
2229
  <SlideBoxWrapper
2069
2230
  slideBoxWrapperMargin={slideBoxWrapperMargin}
2070
2231
  shouldApplyFooterMargin={shouldShowErrorInfoNoteInFooter}
2071
- className={classnames(`${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`)}
2232
+ className={classnames(
2233
+ `${classPrefix} ${isFullMode ? 'creatives-full-mode' : 'creatives-library-mode'} ${mapTemplateCreate ? 'map-template-create' : ''}`,
2234
+ useLocalTemplates && slidBoxContent === 'templates' && 'creatives-slidebox--local-sms-templates',
2235
+ )}
2072
2236
  >
2073
2237
  <CapSlideBox
2074
2238
  header={
@@ -2093,12 +2257,13 @@ export class Creatives extends React.Component {
2093
2257
  smsRegister={smsRegister}
2094
2258
  handleClose={this.handleCloseSlideBox}
2095
2259
  moduleType={this.props.messageDetails?.type}
2260
+ useLocalTemplates={useLocalTemplates}
2096
2261
  />
2097
2262
  )}
2098
2263
  content={(
2099
2264
  <SlideBoxContent
2100
2265
  key="creatives-container-slidebox-content"
2101
- onSelectTemplate={this.onSelectTemplate}
2266
+ onSelectTemplate={this.props.onSelectTemplate != null ? this.props.onSelectTemplate : this.onSelectTemplate}
2102
2267
  onCreateComplete={getCreativesData}
2103
2268
  onPreviewTemplate={this.onPreviewTemplate}
2104
2269
  slidBoxContent={slidBoxContent}
@@ -2166,7 +2331,6 @@ export class Creatives extends React.Component {
2166
2331
  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.
2167
2332
  hostName={this.props?.hostName || ''}
2168
2333
  eventContextTags={eventContextTags}
2169
- waitEventContextTags={waitEventContextTags}
2170
2334
  isLoyaltyModule={isLoyaltyModule}
2171
2335
  loyaltyMetaData={loyaltyMetaData}
2172
2336
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
@@ -2175,7 +2339,8 @@ export class Creatives extends React.Component {
2175
2339
  isTestAndPreviewMode={(() => this.state.isTestAndPreviewMode)()}
2176
2340
  onHtmlEditorValidationStateChange={this.updateHtmlEditorValidationState}
2177
2341
  onPersonalizationTokensChange={this.handlePersonalizationTokensChange}
2178
- />
2342
+ localTemplatesConfig={pick(this.props.localTemplatesConfig || this.props, constants.LOCAL_TEMPLATE_CONFIG_KEYS)}
2343
+ />
2179
2344
  )}
2180
2345
  footer={this.shouldShowFooter() ? (
2181
2346
  <SlideBoxFooter
@@ -2260,13 +2425,31 @@ Creatives.propTypes = {
2260
2425
  orgUnitId: PropTypes.number,
2261
2426
  hostName: PropTypes.string,
2262
2427
  eventContextTags: PropTypes.array,
2263
- waitEventContextTags: PropTypes.object,
2264
2428
  loyaltyTagFetchingDependencies: PropTypes.object,
2265
2429
  customerType: PropTypes.string,
2266
2430
  intl: PropTypes.shape({
2267
2431
  formatMessage: PropTypes.func,
2268
2432
  }),
2269
2433
  stopValidation: PropTypes.func,
2434
+ // Local template list (e.g. for SMS fallback): when set, TemplatesV2 uses these instead of Redux.
2435
+ // All optional. Pass either localTemplatesConfig (object) or individual props below.
2436
+ localTemplatesConfig: PropTypes.shape({
2437
+ useLocalTemplates: PropTypes.bool,
2438
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2439
+ localTemplatesLoading: PropTypes.bool,
2440
+ localTemplatesFilterContent: PropTypes.node,
2441
+ localTemplatesSentinelContent: PropTypes.node,
2442
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2443
+ localTemplatesUseSkeleton: PropTypes.bool,
2444
+ }),
2445
+ useLocalTemplates: PropTypes.bool,
2446
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
2447
+ localTemplatesLoading: PropTypes.bool,
2448
+ localTemplatesFilterContent: PropTypes.node,
2449
+ localTemplatesSentinelContent: PropTypes.node,
2450
+ localTemplatesScrollContainerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
2451
+ localTemplatesUseSkeleton: PropTypes.bool,
2452
+ onSelectTemplate: PropTypes.func,
2270
2453
  };
2271
2454
  const mapStatesToProps = () => createStructuredSelector({
2272
2455
  isLoading: isLoadingSelector(),