@capillarytech/creatives-library 9.0.28 → 9.0.29-alpha.2
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.
- package/constants/unified.js +29 -0
- package/global-styles.js +1 -1
- package/package.json +1 -1
- package/services/tests/api.test.js +35 -20
- package/utils/commonUtils.js +22 -1
- package/utils/rcsPayloadUtils.js +99 -0
- package/utils/templateVarUtils.js +198 -0
- package/utils/tests/rcsPayloadUtils.test.js +243 -0
- package/utils/tests/templateVarUtils.test.js +204 -0
- package/v2Components/CapActionButton/constants.js +20 -1
- package/v2Components/CapActionButton/index.js +193 -144
- package/v2Components/CapActionButton/index.scss +232 -14
- package/v2Components/CapActionButton/messages.js +19 -3
- package/v2Components/CapActionButton/tests/index.test.js +42 -18
- package/v2Components/CapImageUpload/index.js +2 -2
- package/v2Components/CapTagList/index.js +9 -0
- package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
- package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
- package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
- package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
- package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
- package/v2Components/CommonTestAndPreview/constants.js +35 -2
- package/v2Components/CommonTestAndPreview/index.js +770 -231
- package/v2Components/CommonTestAndPreview/messages.js +45 -3
- package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
- package/v2Components/CommonTestAndPreview/sagas.js +25 -6
- package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
- package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
- package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
- package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
- package/v2Components/CommonTestAndPreview/utils.js +84 -0
- package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
- package/v2Components/SmsFallback/constants.js +94 -0
- package/v2Components/SmsFallback/index.js +958 -0
- package/v2Components/SmsFallback/index.scss +266 -0
- package/v2Components/SmsFallback/messages.js +78 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
- package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
- package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
- package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
- package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
- package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
- package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +147 -32
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
- package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
- package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
- package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
- package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
- package/v2Components/VarSegmentMessageEditor/index.js +125 -0
- package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
- package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
- package/v2Containers/CreativesContainer/constants.js +11 -0
- package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
- package/v2Containers/CreativesContainer/index.js +328 -106
- package/v2Containers/CreativesContainer/index.scss +102 -1
- package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
- package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
- package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
- package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
- package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
- package/v2Containers/Email/index.js +3 -63
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
- package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
- package/v2Containers/Rcs/carouselUtils.js +256 -0
- package/v2Containers/Rcs/components/CarouselCard.js +315 -0
- package/v2Containers/Rcs/components/CarouselCardButtons.js +93 -0
- package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
- package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
- package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
- package/v2Containers/Rcs/constants.js +137 -16
- package/v2Containers/Rcs/index.js +1912 -958
- package/v2Containers/Rcs/index.scss +443 -8
- package/v2Containers/Rcs/messages.js +45 -22
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +522 -0
- package/v2Containers/Rcs/tests/CarouselCard.test.js +465 -0
- package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
- package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
- package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
- package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
- package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
- package/v2Containers/Rcs/tests/index.test.js +400 -40
- package/v2Containers/Rcs/tests/mockData.js +38 -0
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
- package/v2Containers/Rcs/tests/utils.test.js +762 -30
- package/v2Containers/Rcs/utils.js +571 -12
- package/v2Containers/Sms/Create/index.js +115 -48
- package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
- package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
- package/v2Containers/SmsTrai/Create/index.js +9 -4
- package/v2Containers/SmsTrai/Edit/constants.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +678 -169
- package/v2Containers/SmsTrai/Edit/index.scss +126 -0
- package/v2Containers/SmsTrai/Edit/messages.js +14 -4
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
- package/v2Containers/SmsWrapper/index.js +37 -8
- package/v2Containers/TagList/index.js +5 -0
- package/v2Containers/Templates/TemplatesActionBar.js +101 -0
- package/v2Containers/Templates/_templates.scss +196 -12
- package/v2Containers/Templates/actions.js +11 -0
- package/v2Containers/Templates/constants.js +2 -0
- package/v2Containers/Templates/index.js +131 -59
- package/v2Containers/Templates/sagas.js +57 -13
- package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
- package/v2Containers/Templates/tests/sagas.test.js +199 -16
- package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
- package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
- package/v2Containers/TemplatesV2/index.js +88 -25
- package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
- package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
- package/v2Containers/Whatsapp/index.js +3 -20
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/* eslint-disable no-unused-expressions */
|
|
2
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
3
3
|
import { bindActionCreators } from 'redux';
|
|
4
4
|
import { createStructuredSelector } from 'reselect';
|
|
5
|
-
import {
|
|
5
|
+
import { FormattedMessage } from 'react-intl';
|
|
6
6
|
import get from 'lodash/get';
|
|
7
7
|
import isEmpty from 'lodash/isEmpty';
|
|
8
8
|
import cloneDeep from 'lodash/cloneDeep';
|
|
9
9
|
import isNil from 'lodash/isNil';
|
|
10
|
-
import styled from 'styled-components';
|
|
11
10
|
import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
|
|
12
11
|
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
13
12
|
import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
|
|
@@ -23,8 +22,6 @@ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
|
23
22
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
24
23
|
import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
25
24
|
import CapImage from '@capillarytech/cap-ui-library/CapImage';
|
|
26
|
-
import CapCard from '@capillarytech/cap-ui-library/CapCard';
|
|
27
|
-
import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
|
|
28
25
|
import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
|
|
29
26
|
import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
|
|
30
27
|
import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
|
|
@@ -35,6 +32,8 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
|
|
|
35
32
|
import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
|
|
36
33
|
import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
|
|
37
34
|
import CapLink from '@capillarytech/cap-ui-library/CapLink';
|
|
35
|
+
import CapTab from '@capillarytech/cap-ui-library/CapTab';
|
|
36
|
+
import { flushSync } from 'react-dom';
|
|
38
37
|
|
|
39
38
|
import {
|
|
40
39
|
CAP_G01,
|
|
@@ -50,17 +49,29 @@ import {
|
|
|
50
49
|
import CapVideoUpload from '../../v2Components/CapVideoUpload';
|
|
51
50
|
import * as globalActions from '../Cap/actions';
|
|
52
51
|
import CapActionButton from '../../v2Components/CapActionButton';
|
|
52
|
+
import TemplatePreview from '../../v2Components/TemplatePreview';
|
|
53
53
|
import { makeSelectRcs, makeSelectAccount } from './selectors';
|
|
54
54
|
import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
|
|
55
55
|
import {
|
|
56
56
|
isLoadingMetaEntities,
|
|
57
57
|
makeSelectMetaEntities,
|
|
58
58
|
setInjectedTags,
|
|
59
|
+
selectCurrentOrgDetails,
|
|
59
60
|
} from '../Cap/selectors';
|
|
60
61
|
import * as RcsActions from './actions';
|
|
61
62
|
import { isAiContentBotDisabled } from '../../utils/common';
|
|
62
|
-
import * as TemplatesActions from '../Templates/actions';
|
|
63
63
|
import './index.scss';
|
|
64
|
+
import {
|
|
65
|
+
normalizeLibraryLoadedTitleDesc,
|
|
66
|
+
mergeRcsSmsFallbackVarMapLayers,
|
|
67
|
+
applySmsFallbackHydration,
|
|
68
|
+
syncCardVarMappedSemanticsFromSlots,
|
|
69
|
+
hasMeaningfulSmsFallbackShape,
|
|
70
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData,
|
|
71
|
+
mergeSmsFallbackForLibrary,
|
|
72
|
+
buildSmsFallBackContentForPayload,
|
|
73
|
+
pickRcsCardVarMappedEntries,
|
|
74
|
+
} from './rcsLibraryHydrationUtils';
|
|
64
75
|
import {
|
|
65
76
|
RCS,
|
|
66
77
|
SMS,
|
|
@@ -75,8 +86,8 @@ import {
|
|
|
75
86
|
LIBRARY,
|
|
76
87
|
ALLOWED_IMAGE_EXTENSIONS_REGEX,
|
|
77
88
|
RCS_IMG_SIZE,
|
|
78
|
-
RCS_DLT_MODE,
|
|
79
89
|
CTA,
|
|
90
|
+
AI_CONTENT_BOT_DISABLED,
|
|
80
91
|
RCS_STATUSES,
|
|
81
92
|
TITLE_TEXT,
|
|
82
93
|
MESSAGE_TEXT,
|
|
@@ -91,9 +102,13 @@ import {
|
|
|
91
102
|
rcsVarTestRegex,
|
|
92
103
|
RCS_IMAGE_DIMENSIONS,
|
|
93
104
|
RCS_TEXT_MESSAGE_MAX_LENGTH,
|
|
105
|
+
RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
|
|
94
106
|
RCS_RICH_CARD_MAX_LENGTH,
|
|
95
107
|
RCS_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
108
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS,
|
|
109
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
96
110
|
MAX_BUTTONS,
|
|
111
|
+
INITIAL_SUGGESTIONS,
|
|
97
112
|
INITIAL_SUGGESTIONS_DATA_STOP,
|
|
98
113
|
RCS_BUTTON_TYPES,
|
|
99
114
|
titletype,
|
|
@@ -103,25 +118,67 @@ import {
|
|
|
103
118
|
SMALL,
|
|
104
119
|
MEDIUM,
|
|
105
120
|
RICHCARD,
|
|
121
|
+
HOST_INFOBIP,
|
|
122
|
+
HOST_ICS,
|
|
123
|
+
STOP,
|
|
124
|
+
RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
|
|
125
|
+
MAX_RCS_CAROUSEL_ALLOWED,
|
|
126
|
+
RCS_CAROUSEL_INITIAL_CARD,
|
|
127
|
+
RCS_CAROUSEL_INITIAL_FIRST_CARD,
|
|
128
|
+
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
129
|
+
RCS_NUMERIC_VAR_TOKEN_REGEX,
|
|
130
|
+
RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
|
|
131
|
+
RCS_TAG_AREA_FIELD_TITLE,
|
|
132
|
+
RCS_TAG_AREA_FIELD_DESC,
|
|
133
|
+
REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
|
|
134
|
+
JS_TYPES,
|
|
106
135
|
} from './constants';
|
|
107
136
|
import globalMessages from '../Cap/messages';
|
|
108
137
|
import messages from './messages';
|
|
109
138
|
import creativesMessages from '../CreativesContainer/messages';
|
|
110
139
|
import withCreatives from '../../hoc/withCreatives';
|
|
111
140
|
import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
|
|
112
|
-
import
|
|
141
|
+
import VarSegmentMessageEditor from '../../v2Components/VarSegmentMessageEditor';
|
|
142
|
+
import { ANDROID, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
|
|
113
143
|
import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
|
|
144
|
+
import { splitTemplateVarString } from '../../utils/templateVarUtils';
|
|
114
145
|
import CapImageUpload from '../../v2Components/CapImageUpload';
|
|
115
|
-
import
|
|
116
|
-
import
|
|
117
|
-
import SmsTraiEdit from '../SmsTrai/Edit';
|
|
146
|
+
import SmsFallback from '../../v2Components/SmsFallback';
|
|
147
|
+
import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
|
|
118
148
|
import TagList from '../TagList';
|
|
119
149
|
import { validateTags } from '../../utils/tagValidations';
|
|
120
|
-
import {
|
|
150
|
+
import { isTraiDLTEnable } from '../../utils/common';
|
|
121
151
|
import { isTagIncluded } from '../../utils/commonUtils';
|
|
122
152
|
import injectReducer from '../../utils/injectReducer';
|
|
123
153
|
import v2RcsReducer from './reducer';
|
|
124
|
-
import {
|
|
154
|
+
import {
|
|
155
|
+
buildRcsNumericMustachePlaceholderRegex,
|
|
156
|
+
getTemplateStatusType,
|
|
157
|
+
normalizeCardVarMapped,
|
|
158
|
+
coalesceCardVarMappedToTemplate,
|
|
159
|
+
getRcsSemanticVarNamesSpanningTitleAndDesc,
|
|
160
|
+
resolveCardVarMappedSlotValue,
|
|
161
|
+
sanitizeCardVarMappedValue,
|
|
162
|
+
checkSmsFallbackBlocksDone,
|
|
163
|
+
isSmsFallbackContentTooLong,
|
|
164
|
+
} from './utils';
|
|
165
|
+
import {
|
|
166
|
+
getCarouselImageAssetIndex,
|
|
167
|
+
getCarouselVideoAssetIndex,
|
|
168
|
+
getCarouselThumbnailAssetIndex,
|
|
169
|
+
isThumbnailAssetIndex,
|
|
170
|
+
getCarouselDimensionKey,
|
|
171
|
+
ensureFirstCardDefaultPhoneSuggestions,
|
|
172
|
+
checkDisableAddCarouselButton,
|
|
173
|
+
getNextCarouselVarToken,
|
|
174
|
+
getCarouselTitleCharacterCount,
|
|
175
|
+
getCarouselDescriptionCharacterCount,
|
|
176
|
+
buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
|
|
177
|
+
buildCarouselCardContentForPayload,
|
|
178
|
+
computeCarouselVarSlotKeys,
|
|
179
|
+
} from './carouselUtils';
|
|
180
|
+
import CarouselDimensionSelection from './components/CarouselDimensionSelection';
|
|
181
|
+
import CarouselCard from './components/CarouselCard';
|
|
125
182
|
|
|
126
183
|
|
|
127
184
|
const { Group: CapCheckboxGroup } = CapCheckbox;
|
|
@@ -135,31 +192,48 @@ export const Rcs = (props) => {
|
|
|
135
192
|
templateData = {},
|
|
136
193
|
rcsData = {},
|
|
137
194
|
thumbnailData = {}, // Add separate thumbnail data
|
|
138
|
-
templatesActions,
|
|
139
195
|
globalActions,
|
|
140
196
|
location,
|
|
141
|
-
handleClose,
|
|
142
197
|
getDefaultTags,
|
|
143
198
|
supportedTags,
|
|
144
199
|
metaEntities,
|
|
145
200
|
injectedTags,
|
|
146
201
|
loadingTags,
|
|
147
202
|
getFormData,
|
|
148
|
-
isDltEnabled,
|
|
149
203
|
smsRegister,
|
|
204
|
+
orgUnitId,
|
|
150
205
|
selectedOfferDetails,
|
|
151
206
|
eventContextTags,
|
|
152
|
-
waitEventContextTags,
|
|
153
207
|
accountData = {},
|
|
208
|
+
currentOrgDetails,
|
|
154
209
|
// TestAndPreviewSlidebox props
|
|
155
210
|
showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
|
|
156
211
|
handleTestAndPreview: propsHandleTestAndPreview,
|
|
157
212
|
handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
|
|
213
|
+
handleClose,
|
|
158
214
|
} = props || {};
|
|
159
215
|
const { formatMessage } = intl;
|
|
160
216
|
const { TextArea } = CapInput;
|
|
161
217
|
const { CapCustomCardList } = CapCustomCard;
|
|
218
|
+
|
|
219
|
+
// Defensive: React cannot render plain objects as children (crashes with
|
|
220
|
+
// "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
|
|
221
|
+
// can accidentally set an error state to an object (e.g. `{}`).
|
|
222
|
+
const normalizeErrorMessage = (err) => {
|
|
223
|
+
if (!err) return '';
|
|
224
|
+
if (React.isValidElement(err)) return err;
|
|
225
|
+
if (typeof err === JS_TYPES.STRING) return err;
|
|
226
|
+
if (typeof err === JS_TYPES.NUMBER) return String(err);
|
|
227
|
+
if (typeof err === JS_TYPES.OBJECT && typeof err.message === JS_TYPES.STRING) return err.message;
|
|
228
|
+
try {
|
|
229
|
+
return JSON.stringify(err);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return String(err);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
162
235
|
const [isEditFlow, setEditFlow] = useState(false);
|
|
236
|
+
const isEditLike = isEditFlow || !isFullMode;
|
|
163
237
|
const [tags, updateTags] = useState([]);
|
|
164
238
|
const [spin, setSpin] = useState(false);
|
|
165
239
|
//template
|
|
@@ -168,33 +242,17 @@ export const Rcs = (props) => {
|
|
|
168
242
|
const [templateMediaType, setTemplateMediaType] = useState(
|
|
169
243
|
RCS_MEDIA_TYPES.NONE,
|
|
170
244
|
);
|
|
171
|
-
const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
|
|
172
245
|
const [templateTitle, setTemplateTitle] = useState('');
|
|
173
246
|
const [templateDesc, setTemplateDesc] = useState('');
|
|
174
247
|
const [templateDescError, setTemplateDescError] = useState(false);
|
|
175
248
|
const [templateStatus, setTemplateStatus] = useState('');
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const [fallbackMessage, setFallbackMessage] = useState('');
|
|
179
|
-
const [fallbackMessageError, setFallbackMessageError] = useState(false);
|
|
180
|
-
//fallback dlt
|
|
181
|
-
const [showDltContainer, setShowDltContainer] = useState(false);
|
|
182
|
-
const [dltMode, setDltMode] = useState('');
|
|
183
|
-
const [dltEditData, setDltEditData] = useState({});
|
|
184
|
-
const [showDltCard, setShowDltCard] = useState(false);
|
|
185
|
-
const [fallbackPreviewmode, setFallbackPreviewmode] = useState(false);
|
|
186
|
-
const [dltPreviewData, setDltPreviewData] = useState('');
|
|
249
|
+
/** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
|
|
250
|
+
const [smsFallbackData, setSmsFallbackData] = useState(undefined);
|
|
187
251
|
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
|
|
188
|
-
const
|
|
252
|
+
const buttonType = RCS_BUTTON_TYPES.NONE;
|
|
189
253
|
const [suggestionError, setSuggestionError] = useState(true);
|
|
254
|
+
const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
|
|
190
255
|
const [templateType, setTemplateType] = useState('text_message');
|
|
191
|
-
const [templateHeader, setTemplateHeader] = useState('');
|
|
192
|
-
const [templateMessage, setTemplateMessage] = useState('');
|
|
193
|
-
const [templateHeaderError, setTemplateHeaderError] = useState('');
|
|
194
|
-
const [templateMessageError, setTemplateMessageError] = useState('');
|
|
195
|
-
const validVarRegex = /\{\{(\d+)\}\}/g;
|
|
196
|
-
const [updatedTitleData, setUpdatedTitleData] = useState([]);
|
|
197
|
-
const [updatedDescData, setUpdatedDescData] = useState([]);
|
|
198
256
|
const [titleVarMappedData, setTitleVarMappedData] = useState({});
|
|
199
257
|
const [descVarMappedData, setDescVarMappedData] = useState({});
|
|
200
258
|
const [titleTextAreaId, setTitleTextAreaId] = useState();
|
|
@@ -205,75 +263,63 @@ export const Rcs = (props) => {
|
|
|
205
263
|
const [rcsVideoSrc, setRcsVideoSrc] = useState({});
|
|
206
264
|
const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
|
|
207
265
|
const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
|
|
208
|
-
|
|
266
|
+
// Carousel (UI-only) state
|
|
267
|
+
const [selectedCarousel, setSelectedCarousel] = useState('');
|
|
268
|
+
const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
|
|
269
|
+
const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
|
|
270
|
+
const [carouselData, setCarouselData] = useState([]);
|
|
271
|
+
const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
|
|
272
|
+
const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
|
|
273
|
+
const [carouselResetNonce, setCarouselResetNonce] = useState(0);
|
|
274
|
+
// Title and description each track their own last-focused var slot (mirrors titleTextAreaId/
|
|
275
|
+
// descTextAreaId for the single-card flow) — a single shared id would let a click on the
|
|
276
|
+
// "Add Labels" picker next to one field silently apply to whichever field was focused last,
|
|
277
|
+
// if the user hadn't just clicked into that field's own var slot.
|
|
278
|
+
const [carouselTitleFocusedVarId, setCarouselTitleFocusedVarId] = useState('');
|
|
279
|
+
const [carouselDescFocusedVarId, setCarouselDescFocusedVarId] = useState('');
|
|
280
|
+
const [imageError, setImageError] = useState(null);
|
|
209
281
|
const [templateTitleError, setTemplateTitleError] = useState(false);
|
|
210
282
|
const [cardVarMapped, setCardVarMapped] = useState({});
|
|
283
|
+
/** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
|
|
284
|
+
const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
|
|
285
|
+
const lastHydratedRcsCardVarSignatureRef = useRef(null);
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
|
|
289
|
+
* without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
|
|
290
|
+
* fallback card / content stopped appearing until re-selected.
|
|
291
|
+
*/
|
|
292
|
+
const rcsHydrationDetails = useMemo(
|
|
293
|
+
() => (isFullMode ? rcsData?.templateDetails : templateData),
|
|
294
|
+
[isFullMode, rcsData?.templateDetails, templateData],
|
|
295
|
+
);
|
|
211
296
|
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
226
|
-
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
227
|
-
|
|
228
|
-
// Build media preview object (same pattern as getRcsPreview)
|
|
229
|
-
const mediaPreview = {};
|
|
230
|
-
if (isMediaTypeImage && rcsImageSrc) {
|
|
231
|
-
mediaPreview.rcsImageSrc = rcsImageSrc;
|
|
232
|
-
}
|
|
233
|
-
if (isMediaTypeVideo && !isMediaTypeText) {
|
|
234
|
-
// For video, use thumbnailSrc as rcsVideoSrc (same as getRcsPreview line 2104)
|
|
235
|
-
if (rcsThumbnailSrc) {
|
|
236
|
-
mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
|
|
237
|
-
} else if (rcsVideoSrc?.videoSrc) {
|
|
238
|
-
mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
|
|
239
|
-
}
|
|
240
|
-
// Also include thumbnailSrc separately if available
|
|
241
|
-
if (rcsThumbnailSrc) {
|
|
242
|
-
mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
|
|
297
|
+
/** Skip duplicate /meta/TAG fetches: same query is triggered from (1) useEffect below, (2) title TagList mount, (3) description TagList mount — each calls getTagsforContext('Outbound'). */
|
|
298
|
+
const lastTagSchemaQueryKeyRef = useRef(null);
|
|
299
|
+
/**
|
|
300
|
+
* Library: parent often passes a new `templateData` object reference every render. Re-applying the same
|
|
301
|
+
* SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
|
|
302
|
+
*/
|
|
303
|
+
const lastSmsFallbackHydrationKeyRef = useRef(null);
|
|
304
|
+
|
|
305
|
+
const fetchTagSchemaIfNewQuery = useCallback(
|
|
306
|
+
(query) => {
|
|
307
|
+
const key = JSON.stringify(query);
|
|
308
|
+
if (lastTagSchemaQueryKeyRef.current === key) {
|
|
309
|
+
return;
|
|
243
310
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// templateDesc is used for ALL types (text message body or rich card description)
|
|
250
|
-
// For UnifiedPreview, we map templateTitle -> templateHeader and templateDesc -> templateMessage
|
|
251
|
-
const contentObj = {
|
|
252
|
-
// Map templateTitle to templateHeader and templateDesc to templateMessage
|
|
253
|
-
templateHeader: templateTitle,
|
|
254
|
-
templateMessage: templateDesc,
|
|
255
|
-
...mediaPreview,
|
|
256
|
-
...(suggestions.length > 0 && {
|
|
257
|
-
suggestions: suggestions,
|
|
258
|
-
}),
|
|
259
|
-
};
|
|
311
|
+
lastTagSchemaQueryKeyRef.current = key;
|
|
312
|
+
globalActions.fetchSchemaForEntity(query);
|
|
313
|
+
},
|
|
314
|
+
[globalActions],
|
|
315
|
+
);
|
|
260
316
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
templateMediaType,
|
|
264
|
-
templateTitle,
|
|
265
|
-
templateDesc,
|
|
266
|
-
rcsImageSrc,
|
|
267
|
-
rcsVideoSrc,
|
|
268
|
-
rcsThumbnailSrc,
|
|
269
|
-
suggestions,
|
|
270
|
-
selectedDimension,
|
|
271
|
-
]);
|
|
317
|
+
// TestAndPreviewSlidebox state
|
|
318
|
+
const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
|
|
272
319
|
|
|
273
320
|
// Handle Test and Preview button click
|
|
274
321
|
const handleTestAndPreview = useCallback(() => {
|
|
275
322
|
setShowTestAndPreviewSlidebox(true);
|
|
276
|
-
setIsTestAndPreviewMode(true);
|
|
277
323
|
if (propsHandleTestAndPreview) {
|
|
278
324
|
propsHandleTestAndPreview();
|
|
279
325
|
}
|
|
@@ -297,31 +343,334 @@ export const Rcs = (props) => {
|
|
|
297
343
|
// For video
|
|
298
344
|
return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
|
|
299
345
|
};
|
|
346
|
+
/** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
|
|
347
|
+
const handleSmsFallbackEditorStateChange = useCallback((patch) => {
|
|
348
|
+
setSmsFallbackData((prev) => {
|
|
349
|
+
if (!patch || typeof patch !== 'object') return prev;
|
|
350
|
+
// Bail out when no template has been selected yet — SmsTraiEdit fires
|
|
351
|
+
// onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
|
|
352
|
+
// which would create a non-null smsFallbackData from nothing and cause
|
|
353
|
+
// the card to appear as "Untitled creative" before any save.
|
|
354
|
+
if (!prev) return prev;
|
|
355
|
+
return { ...prev, ...patch };
|
|
356
|
+
});
|
|
357
|
+
}, []);
|
|
300
358
|
|
|
359
|
+
/** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
|
|
301
360
|
const [accountId, setAccountId] = useState('');
|
|
361
|
+
/** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
|
|
362
|
+
const [wecrmAccountId, setWecrmAccountId] = useState('');
|
|
302
363
|
const [accessToken, setAccessToken] = useState('');
|
|
303
364
|
const [hostName, setHostName] = useState('');
|
|
304
365
|
const [accountName, setAccountName] = useState('');
|
|
366
|
+
const isHostInfoBip = hostName === HOST_INFOBIP;
|
|
367
|
+
const isHostIcs = hostName === HOST_ICS;
|
|
368
|
+
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
|
|
371
|
+
}, [isHostIcs]);
|
|
305
372
|
const [rcsAccount, setRcsAccount] = useState('');
|
|
306
373
|
useEffect(() => {
|
|
307
374
|
const accountObj = accountData.selectedRcsAccount || {};
|
|
308
375
|
if (!isEmpty(accountObj)) {
|
|
309
376
|
const {
|
|
377
|
+
id: wecrmId,
|
|
310
378
|
sourceAccountIdentifier = '',
|
|
311
379
|
configs = {},
|
|
312
380
|
} = accountObj;
|
|
313
|
-
|
|
314
381
|
setAccountId(sourceAccountIdentifier);
|
|
382
|
+
setWecrmAccountId(
|
|
383
|
+
wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
|
|
384
|
+
);
|
|
315
385
|
setAccessToken(configs.accessToken || '');
|
|
316
386
|
setHostName(accountObj.hostName || '');
|
|
317
387
|
setAccountName(accountObj.name || '');
|
|
318
388
|
setRcsAccount(accountObj.id || '');
|
|
389
|
+
} else {
|
|
390
|
+
setAccountId('');
|
|
391
|
+
setWecrmAccountId('');
|
|
392
|
+
setAccessToken('');
|
|
393
|
+
setHostName('');
|
|
394
|
+
setAccountName('');
|
|
319
395
|
}
|
|
320
396
|
}, [accountData.selectedRcsAccount]);
|
|
321
397
|
|
|
322
398
|
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
323
399
|
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
324
400
|
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
401
|
+
const isCarouselType = templateType === contentType.carousel;
|
|
402
|
+
|
|
403
|
+
const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
|
|
404
|
+
setCarouselData((prev = []) => {
|
|
405
|
+
const updated = cloneDeep(prev);
|
|
406
|
+
if (!updated?.[cardIndex]) return updated;
|
|
407
|
+
if (clearImage) updated[cardIndex].imageSrc = '';
|
|
408
|
+
if (clearVideo) updated[cardIndex].videoAsset = {};
|
|
409
|
+
if (clearThumb) updated[cardIndex].thumbnailSrc = '';
|
|
410
|
+
return updated;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
|
|
414
|
+
if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
|
|
415
|
+
if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const resetCarouselMediaForAllCards = () => {
|
|
419
|
+
setCarouselData((prev = []) => {
|
|
420
|
+
const updated = cloneDeep(prev);
|
|
421
|
+
updated.forEach((card) => {
|
|
422
|
+
if (!card) return;
|
|
423
|
+
card.imageSrc = '';
|
|
424
|
+
card.videoAsset = {};
|
|
425
|
+
card.thumbnailSrc = '';
|
|
426
|
+
});
|
|
427
|
+
return updated;
|
|
428
|
+
});
|
|
429
|
+
(carouselData || []).forEach((_, idx) => {
|
|
430
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
|
|
431
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
|
|
432
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
|
|
433
|
+
});
|
|
434
|
+
// Force tab panes to remount after global reset (some tab implementations cache inactive panes)
|
|
435
|
+
setCarouselResetNonce((count) => count + 1);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
|
|
439
|
+
// updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
|
|
440
|
+
// suggestions typed since the last commit (stale closure).
|
|
441
|
+
const handleCarouselValueChange = (carouselIndex, fields) => {
|
|
442
|
+
setCarouselData((prev = []) => {
|
|
443
|
+
const updated = cloneDeep(prev);
|
|
444
|
+
if (!updated[carouselIndex]) return prev;
|
|
445
|
+
fields.forEach(({ fieldName, value }) => {
|
|
446
|
+
updated[carouselIndex][fieldName] = value;
|
|
447
|
+
});
|
|
448
|
+
return updated;
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const updateCarouselErrors = (carouselIndex, patch) => {
|
|
453
|
+
setCarouselErrors((prev = []) => {
|
|
454
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
455
|
+
next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
|
|
456
|
+
return next;
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const deleteCarouselCard = (index) => {
|
|
461
|
+
let nextIdx = 0;
|
|
462
|
+
flushSync(() => {
|
|
463
|
+
setCarouselData((prev = []) => {
|
|
464
|
+
const updated = cloneDeep(prev);
|
|
465
|
+
if (index < 0 || index >= updated.length) return updated;
|
|
466
|
+
updated.splice(index, 1);
|
|
467
|
+
nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
|
|
468
|
+
return ensureFirstCardDefaultPhoneSuggestions(updated);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
setCarouselErrors((prev = []) => {
|
|
472
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
473
|
+
next.splice(index, 1);
|
|
474
|
+
return next;
|
|
475
|
+
});
|
|
476
|
+
setActiveCarouselIndex(`${nextIdx}`);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
const addCarouselCard = () => {
|
|
481
|
+
let newIndex = 0;
|
|
482
|
+
flushSync(() => {
|
|
483
|
+
setCarouselData((prev = []) => {
|
|
484
|
+
const updated = cloneDeep(prev);
|
|
485
|
+
updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
|
|
486
|
+
newIndex = updated.length - 1;
|
|
487
|
+
return updated;
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
|
|
491
|
+
setActiveCarouselIndex(`${newIndex}`);
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// Initialize carousel data when switching to carousel type
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
if (!isCarouselType) return;
|
|
497
|
+
if (!carouselData || carouselData.length === 0) {
|
|
498
|
+
setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
|
|
499
|
+
setCarouselErrors([{}]);
|
|
500
|
+
setActiveCarouselIndex('0');
|
|
501
|
+
}
|
|
502
|
+
}, [isCarouselType]);
|
|
503
|
+
|
|
504
|
+
// keep derived carousel key in sync
|
|
505
|
+
useEffect(() => {
|
|
506
|
+
if (!isCarouselType) return;
|
|
507
|
+
if (!selectedCarouselHeight || !selectedCarouselWidth) return;
|
|
508
|
+
// Required format: HEIGHT_WIDTH
|
|
509
|
+
setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
|
|
510
|
+
}, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
|
|
511
|
+
|
|
512
|
+
const renderCarouselDimensionSelection = () => {
|
|
513
|
+
if (!isCarouselType) return null;
|
|
514
|
+
return (
|
|
515
|
+
<CarouselDimensionSelection
|
|
516
|
+
isEditFlow={isEditFlow}
|
|
517
|
+
isFullMode={isFullMode}
|
|
518
|
+
selectedCarouselHeight={selectedCarouselHeight}
|
|
519
|
+
selectedCarouselWidth={selectedCarouselWidth}
|
|
520
|
+
selectedCarousel={selectedCarousel}
|
|
521
|
+
onHeightChange={(val) => {
|
|
522
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
523
|
+
resetCarouselMediaForAllCards();
|
|
524
|
+
setSelectedCarouselHeight(val);
|
|
525
|
+
}}
|
|
526
|
+
onWidthChange={(val) => {
|
|
527
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
528
|
+
resetCarouselMediaForAllCards();
|
|
529
|
+
setSelectedCarouselWidth(val);
|
|
530
|
+
}}
|
|
531
|
+
/>
|
|
532
|
+
);
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const getCarouselTabPanes = () => {
|
|
536
|
+
const tagsForPicker = getRcsTags();
|
|
537
|
+
return (carouselData || []).map((card, index) => ({
|
|
538
|
+
key: String(index),
|
|
539
|
+
tab: index + 1,
|
|
540
|
+
content: (
|
|
541
|
+
<CarouselCard
|
|
542
|
+
cardIndex={index}
|
|
543
|
+
card={card}
|
|
544
|
+
carouselLength={carouselData.length}
|
|
545
|
+
isEditFlow={isEditFlow}
|
|
546
|
+
isFullMode={isFullMode}
|
|
547
|
+
mediaRadioOptions={mediaRadioOptions}
|
|
548
|
+
cardError={carouselErrors?.[index]}
|
|
549
|
+
location={location}
|
|
550
|
+
tagsForPicker={tagsForPicker}
|
|
551
|
+
injectedTags={injectedTags}
|
|
552
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
553
|
+
formatMessage={formatMessage}
|
|
554
|
+
titleMaxLength={getTitleMaxLength()}
|
|
555
|
+
descriptionMaxLength={getDescriptionMaxLength()}
|
|
556
|
+
titleCharCount={getCarouselTitleCharacterCount(carouselData, index)}
|
|
557
|
+
descriptionCharCount={getCarouselDescriptionCharacterCount(carouselData, index)}
|
|
558
|
+
dimKey={getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth)}
|
|
559
|
+
rcsData={rcsData}
|
|
560
|
+
uploadRcsImage={uploadRcsImage}
|
|
561
|
+
uploadRcsVideo={uploadRcsVideo}
|
|
562
|
+
hostName={hostName}
|
|
563
|
+
onDelete={() => deleteCarouselCard(index)}
|
|
564
|
+
onMediaTypeChange={(value) => {
|
|
565
|
+
// Reset media fields when switching type
|
|
566
|
+
if (value === RCS_MEDIA_TYPES.IMAGE) {
|
|
567
|
+
// Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
|
|
568
|
+
clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
|
|
569
|
+
handleCarouselValueChange(index, [
|
|
570
|
+
{ fieldName: 'mediaType', value },
|
|
571
|
+
{ fieldName: 'videoAsset', value: {} },
|
|
572
|
+
{ fieldName: 'thumbnailSrc', value: '' },
|
|
573
|
+
]);
|
|
574
|
+
} else {
|
|
575
|
+
// Switching to VIDEO: clear image upload so it doesn't auto-restore.
|
|
576
|
+
clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
|
|
577
|
+
handleCarouselValueChange(index, [
|
|
578
|
+
{ fieldName: 'mediaType', value },
|
|
579
|
+
{ fieldName: 'imageSrc', value: '' },
|
|
580
|
+
]);
|
|
581
|
+
}
|
|
582
|
+
}}
|
|
583
|
+
onTitleChange={({ target: { value } }) => {
|
|
584
|
+
let error = false;
|
|
585
|
+
if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
586
|
+
error = formatMessage(messages.templateHeaderLengthError);
|
|
587
|
+
} else {
|
|
588
|
+
error = variableErrorHandling(value);
|
|
589
|
+
}
|
|
590
|
+
updateCarouselErrors(index, { title: error });
|
|
591
|
+
handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
|
|
592
|
+
}}
|
|
593
|
+
onDescriptionChange={({ target: { value } }) => {
|
|
594
|
+
let error = false;
|
|
595
|
+
if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
|
|
596
|
+
error = formatMessage(messages.templateMessageLengthError);
|
|
597
|
+
} else {
|
|
598
|
+
error = variableErrorHandling(value);
|
|
599
|
+
}
|
|
600
|
+
updateCarouselErrors(index, { description: error });
|
|
601
|
+
handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
|
|
602
|
+
}}
|
|
603
|
+
onTitleAddVar={() => appendVarToCarouselField(index, 'title')}
|
|
604
|
+
onDescriptionAddVar={() => appendVarToCarouselField(index, 'description')}
|
|
605
|
+
onTitleTagSelect={onCarouselTitleTagSelect}
|
|
606
|
+
onDescTagSelect={onCarouselDescTagSelect}
|
|
607
|
+
onContextChange={handleOnTagsContextChange}
|
|
608
|
+
renderEditMessage={renderCarouselEditMessage}
|
|
609
|
+
onFieldChange={(fieldName, value) => handleCarouselValueChange(index, [{ fieldName, value }])}
|
|
610
|
+
onSuggestionsChange={(nextSuggestions) =>
|
|
611
|
+
handleCarouselValueChange(index, [{ fieldName: 'suggestions', value: nextSuggestions }])}
|
|
612
|
+
/>
|
|
613
|
+
),
|
|
614
|
+
}));
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const renderCarouselSection = () => {
|
|
618
|
+
if (!isCarouselType) return null;
|
|
619
|
+
|
|
620
|
+
const isMaxCarouselCardsReached = MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0);
|
|
621
|
+
const addCardTooltipTitle = isMaxCarouselCardsReached
|
|
622
|
+
? formatMessage(messages.rcsCarouselMaxCardTooltip, { max: MAX_RCS_CAROUSEL_ALLOWED })
|
|
623
|
+
// "Set up the first card" only makes sense while creating a new template —
|
|
624
|
+
// in edit flow the cards already exist, so don't show it there.
|
|
625
|
+
: (!isEditFlow && checkDisableAddCarouselButton(carouselData, activeCarouselIndex) ? formatMessage(messages.rcsCarouselAddCardDisabledTooltip) : '');
|
|
626
|
+
|
|
627
|
+
// No "+" in edit flow (cards can't be added there) — and with no "+", the
|
|
628
|
+
// divider before it has nothing to separate, so skip both together instead
|
|
629
|
+
// of rendering a dangling divider.
|
|
630
|
+
const operations = isEditFlow ? null : (
|
|
631
|
+
<>
|
|
632
|
+
<CapDivider type="vertical" />
|
|
633
|
+
<CapTooltip title={addCardTooltipTitle}>
|
|
634
|
+
<span className="button-disabled-tooltip-wrapper">
|
|
635
|
+
<CapButton
|
|
636
|
+
onClick={addCarouselCard}
|
|
637
|
+
type="flat"
|
|
638
|
+
className="add-carousel-content-button"
|
|
639
|
+
disabled={
|
|
640
|
+
isEditFlow ||
|
|
641
|
+
!isFullMode ||
|
|
642
|
+
isMaxCarouselCardsReached ||
|
|
643
|
+
checkDisableAddCarouselButton(carouselData, activeCarouselIndex)
|
|
644
|
+
}
|
|
645
|
+
>
|
|
646
|
+
<CapIcon type="plus" />
|
|
647
|
+
</CapButton>
|
|
648
|
+
</span>
|
|
649
|
+
</CapTooltip>
|
|
650
|
+
</>
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
return (
|
|
654
|
+
<CapRow className={`rcs-carousel-section ${isEditFlow ? 'rcs-carousel-section--readonly-above' : ''}`}>
|
|
655
|
+
{renderCarouselDimensionSelection()}
|
|
656
|
+
<CapRow className="rcs-carousel-tab">
|
|
657
|
+
<CapTab
|
|
658
|
+
key={`rcs-carousel-tab-${carouselResetNonce}`}
|
|
659
|
+
defaultActiveKey="0"
|
|
660
|
+
activeKey={activeCarouselIndex}
|
|
661
|
+
tabBarExtraContent={operations}
|
|
662
|
+
onChange={(key) => {
|
|
663
|
+
setActiveCarouselIndex(`${key}`);
|
|
664
|
+
// reset focused var when switching cards (as per requirement)
|
|
665
|
+
setCarouselTitleFocusedVarId('');
|
|
666
|
+
setCarouselDescFocusedVarId('');
|
|
667
|
+
}}
|
|
668
|
+
panes={getCarouselTabPanes()}
|
|
669
|
+
/>
|
|
670
|
+
</CapRow>
|
|
671
|
+
</CapRow>
|
|
672
|
+
);
|
|
673
|
+
};
|
|
325
674
|
|
|
326
675
|
const mediaRadioOptions = [
|
|
327
676
|
{
|
|
@@ -330,11 +679,16 @@ export const Rcs = (props) => {
|
|
|
330
679
|
},
|
|
331
680
|
{
|
|
332
681
|
value: RCS_MEDIA_TYPES.VIDEO,
|
|
333
|
-
label: formatMessage(
|
|
682
|
+
label: formatMessage(
|
|
683
|
+
templateType === contentType.carousel
|
|
684
|
+
? messages.carouselMediaVideoOption
|
|
685
|
+
: messages.mediaVideo,
|
|
686
|
+
),
|
|
334
687
|
},
|
|
335
688
|
];
|
|
336
689
|
const aiContentBotDisabled = isAiContentBotDisabled();
|
|
337
690
|
|
|
691
|
+
|
|
338
692
|
const updateButtonChange = (data, index) => {
|
|
339
693
|
if (data && data.text) {
|
|
340
694
|
const forbiddenError = forbiddenCharactersValidation(data.text);
|
|
@@ -371,7 +725,9 @@ export const Rcs = (props) => {
|
|
|
371
725
|
if (isFullMode) return;
|
|
372
726
|
if (loadingTags || !tags || tags.length === 0) return;
|
|
373
727
|
const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
374
|
-
const
|
|
728
|
+
const slotOffset =
|
|
729
|
+
type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
|
|
730
|
+
const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
|
|
375
731
|
if (!resolved) {
|
|
376
732
|
if (type === TITLE_TEXT) setTemplateTitleError(false);
|
|
377
733
|
if (type === MESSAGE_TEXT) setTemplateDescError(false);
|
|
@@ -380,8 +736,8 @@ export const Rcs = (props) => {
|
|
|
380
736
|
|
|
381
737
|
let contentForValidation = resolved;
|
|
382
738
|
const placeholderTokens = templateStr.match(rcsVarRegex) || [];
|
|
383
|
-
placeholderTokens.forEach((
|
|
384
|
-
const escaped =
|
|
739
|
+
placeholderTokens.forEach((token) => {
|
|
740
|
+
const escaped = token.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
|
|
385
741
|
contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
|
|
386
742
|
});
|
|
387
743
|
if (!contentForValidation.trim()) {
|
|
@@ -398,10 +754,16 @@ export const Rcs = (props) => {
|
|
|
398
754
|
tagModule: getDefaultTags,
|
|
399
755
|
isFullMode,
|
|
400
756
|
}) || {};
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
757
|
+
const unsupportedTagsLengthCheck =
|
|
758
|
+
validationResponse?.unsupportedTags?.length > 0;
|
|
759
|
+
const errorMsg =
|
|
760
|
+
(unsupportedTagsLengthCheck &&
|
|
761
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
762
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
763
|
+
})) ||
|
|
764
|
+
(validationResponse.isBraceError &&
|
|
765
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
766
|
+
false;
|
|
405
767
|
if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
|
|
406
768
|
if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
|
|
407
769
|
};
|
|
@@ -414,55 +776,238 @@ export const Rcs = (props) => {
|
|
|
414
776
|
validateResolvedTagsForType(MESSAGE_TEXT);
|
|
415
777
|
}, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
|
|
416
778
|
|
|
779
|
+
useEffect(() => {
|
|
780
|
+
if (isFullMode || !isCarouselType) return;
|
|
781
|
+
if (loadingTags || !tags || tags.length === 0) return;
|
|
782
|
+
(carouselData || []).forEach((card, idx) => {
|
|
783
|
+
['title', 'description'].forEach((field) => {
|
|
784
|
+
const templateStr = card?.[field] || '';
|
|
785
|
+
if (!templateStr) return;
|
|
786
|
+
const resolved = resolveCarouselTemplateWithMap(templateStr, idx, field === 'description');
|
|
787
|
+
if (!resolved) {
|
|
788
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
let contentForValidation = resolved;
|
|
792
|
+
const placeholderTokens = templateStr.match(rcsVarRegex) || [];
|
|
793
|
+
placeholderTokens.forEach((token) => {
|
|
794
|
+
const escaped = token.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
|
|
795
|
+
contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
|
|
796
|
+
});
|
|
797
|
+
if (!contentForValidation.trim()) {
|
|
798
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const validationResponse = validateTags({
|
|
802
|
+
content: contentForValidation,
|
|
803
|
+
tagsParam: tags,
|
|
804
|
+
injectedTagsParams: injectedTags,
|
|
805
|
+
location,
|
|
806
|
+
tagModule: getDefaultTags,
|
|
807
|
+
eventContextTags,
|
|
808
|
+
isFullMode,
|
|
809
|
+
}) || {};
|
|
810
|
+
const errorMsg =
|
|
811
|
+
(validationResponse?.unsupportedTags?.length > 0 &&
|
|
812
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
813
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
814
|
+
})) ||
|
|
815
|
+
(validationResponse.isBraceError &&
|
|
816
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
817
|
+
false;
|
|
818
|
+
updateCarouselErrors(idx, { [field]: errorMsg });
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
}, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
|
|
822
|
+
|
|
417
823
|
const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
|
|
418
824
|
|
|
419
|
-
const
|
|
825
|
+
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
826
|
+
|
|
827
|
+
/** Same `{{tag}}` in both title and description must not share one semantic map entry. */
|
|
828
|
+
const rcsSpanningSemanticVarNames = useMemo(
|
|
829
|
+
() => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
|
|
830
|
+
[templateTitle, templateDesc],
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
/** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
|
|
834
|
+
const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
|
|
835
|
+
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
836
|
+
const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
|
|
837
|
+
const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
|
|
838
|
+
let varOrdinal = 0;
|
|
839
|
+
for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
|
|
840
|
+
const segmentToken = templateSegments[segmentIndexInField];
|
|
841
|
+
if (rcsVarTestRegex.test(segmentToken)) {
|
|
842
|
+
if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
|
|
843
|
+
return offset + varOrdinal;
|
|
844
|
+
}
|
|
845
|
+
varOrdinal += 1;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
|
|
853
|
+
* multi-field variable resolution.
|
|
854
|
+
*/
|
|
855
|
+
const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
|
|
420
856
|
if (!str) return '';
|
|
421
|
-
const arr =
|
|
857
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
858
|
+
let varOrdinal = 0;
|
|
422
859
|
return arr.map((elem) => {
|
|
423
860
|
if (rcsVarTestRegex.test(elem)) {
|
|
424
861
|
const key = getVarNameFromToken(elem);
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
862
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
863
|
+
varOrdinal += 1;
|
|
864
|
+
const slotValue = resolveCardVarMappedSlotValue(
|
|
865
|
+
cardVarMapped,
|
|
866
|
+
key,
|
|
867
|
+
globalSlot,
|
|
868
|
+
isEditLike,
|
|
869
|
+
rcsSpanningSemanticVarNames.has(key),
|
|
870
|
+
);
|
|
871
|
+
if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
|
|
872
|
+
return String(slotValue);
|
|
428
873
|
}
|
|
429
874
|
return elem;
|
|
430
875
|
}).join('');
|
|
431
876
|
};
|
|
432
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Carousel resolve: each variable slot's key comes from `computeCarouselVarSlotKeys` (title,
|
|
880
|
+
* then description, per card; same name within a card shares one key; different cards never
|
|
881
|
+
* collide). A slot's key never depends on which tag currently fills it.
|
|
882
|
+
*/
|
|
883
|
+
const resolveCarouselTemplateWithMap = (str = '', cardIndex, isDescField = false) => {
|
|
884
|
+
if (!str) return '';
|
|
885
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
886
|
+
const fieldKeys = (isDescField
|
|
887
|
+
? computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex]?.description
|
|
888
|
+
: computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex]?.title) ?? [];
|
|
889
|
+
let varOrdinal = 0;
|
|
890
|
+
return arr.map((elem) => {
|
|
891
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
892
|
+
const slotKey = fieldKeys[varOrdinal];
|
|
893
|
+
varOrdinal += 1;
|
|
894
|
+
const slotValue = cardVarMapped?.[slotKey];
|
|
895
|
+
if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
|
|
896
|
+
return String(slotValue);
|
|
897
|
+
}
|
|
898
|
+
return elem;
|
|
899
|
+
}).join('');
|
|
900
|
+
};
|
|
433
901
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const tokens = [
|
|
437
|
-
...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
|
|
438
|
-
...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
|
|
439
|
-
];
|
|
440
|
-
if (!tokens.length) return;
|
|
441
|
-
setCardVarMapped((prev) => {
|
|
442
|
-
const next = { ...(prev || {}) };
|
|
443
|
-
let changed = false;
|
|
444
|
-
tokens.forEach((t) => {
|
|
445
|
-
const name = getVarNameFromToken(t);
|
|
446
|
-
if (name && !Object.prototype.hasOwnProperty.call(next, name)) {
|
|
447
|
-
next[name] = '';
|
|
448
|
-
changed = true;
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
return changed ? next : prev;
|
|
452
|
-
});
|
|
453
|
-
}, [isFullMode, templateTitle, templateDesc]);
|
|
902
|
+
const buildCarouselCardsForPreview = (cards = []) =>
|
|
903
|
+
buildCarouselCardsForPreviewUtil(cards, { isFullMode, rcsVarRegex, resolveTemplateWithMap });
|
|
454
904
|
|
|
905
|
+
/**
|
|
906
|
+
* Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
|
|
907
|
+
* (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
|
|
908
|
+
* TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
|
|
909
|
+
*/
|
|
910
|
+
const getTemplateContent = useCallback(() => {
|
|
911
|
+
if (templateType === contentType.carousel) {
|
|
912
|
+
const carouselDimKey = getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth);
|
|
913
|
+
const carouselImgDims =
|
|
914
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
915
|
+
const carouselVidDims =
|
|
916
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
917
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
918
|
+
return {
|
|
919
|
+
carouselData: buildCarouselCardsForPreview(carouselData),
|
|
920
|
+
carouselPreviewDimensions: {
|
|
921
|
+
imageWidth: carouselImgDims.width,
|
|
922
|
+
imageHeight: carouselImgDims.height,
|
|
923
|
+
videoThumbWidth: carouselVidDims.width,
|
|
924
|
+
videoThumbHeight: carouselVidDims.height,
|
|
925
|
+
},
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
929
|
+
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
930
|
+
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
931
|
+
|
|
932
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
933
|
+
const titleVarCountForResolve = isMediaTypeText
|
|
934
|
+
? 0
|
|
935
|
+
: ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
|
|
936
|
+
const resolvedTitle = isMediaTypeText
|
|
937
|
+
? ''
|
|
938
|
+
: (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
|
|
939
|
+
const resolvedDesc = isSlotMappingMode
|
|
940
|
+
? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
|
|
941
|
+
: templateDesc;
|
|
942
|
+
|
|
943
|
+
const mediaPreview = {};
|
|
944
|
+
if (isMediaTypeImage && rcsImageSrc) {
|
|
945
|
+
mediaPreview.rcsImageSrc = rcsImageSrc;
|
|
946
|
+
}
|
|
947
|
+
if (isMediaTypeVideo && !isMediaTypeText) {
|
|
948
|
+
if (rcsThumbnailSrc) {
|
|
949
|
+
mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
|
|
950
|
+
} else if (rcsVideoSrc?.videoSrc) {
|
|
951
|
+
mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
|
|
952
|
+
}
|
|
953
|
+
if (rcsThumbnailSrc) {
|
|
954
|
+
mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const contentObj = {
|
|
959
|
+
templateHeader: resolvedTitle,
|
|
960
|
+
templateMessage: resolvedDesc,
|
|
961
|
+
...mediaPreview,
|
|
962
|
+
...(suggestions.length > 0 && {
|
|
963
|
+
suggestions: suggestions,
|
|
964
|
+
}),
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
return contentObj;
|
|
968
|
+
}, [
|
|
969
|
+
templateMediaType,
|
|
970
|
+
templateTitle,
|
|
971
|
+
templateDesc,
|
|
972
|
+
rcsImageSrc,
|
|
973
|
+
rcsVideoSrc,
|
|
974
|
+
rcsThumbnailSrc,
|
|
975
|
+
suggestions,
|
|
976
|
+
selectedDimension,
|
|
977
|
+
isFullMode,
|
|
978
|
+
isEditFlow,
|
|
979
|
+
cardVarMapped,
|
|
980
|
+
rcsSpanningSemanticVarNames,
|
|
981
|
+
carouselData,
|
|
982
|
+
selectedCarouselHeight,
|
|
983
|
+
selectedCarouselWidth,
|
|
984
|
+
]);
|
|
985
|
+
|
|
986
|
+
const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
|
|
455
987
|
|
|
456
|
-
const RcsLabel = styled.div`
|
|
457
|
-
display: flex;
|
|
458
|
-
margin-top: 20px;
|
|
459
|
-
`;
|
|
460
988
|
const paramObj = params || {};
|
|
461
989
|
useEffect(() => {
|
|
462
990
|
const { id } = paramObj;
|
|
463
991
|
if (id && isFullMode) {
|
|
464
992
|
setSpin(true);
|
|
465
993
|
actions.getTemplateDetails(id, setSpin);
|
|
994
|
+
} else if (!id && isFullMode) {
|
|
995
|
+
// Create New: clear standalone media and ALL possible carousel card Redux slots.
|
|
996
|
+
// Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
|
|
997
|
+
updateRcsImageSrc('');
|
|
998
|
+
setRcsVideoSrc({});
|
|
999
|
+
setRcsThumbnailSrc('');
|
|
1000
|
+
setAssetList({});
|
|
1001
|
+
setEditFlow(false);
|
|
1002
|
+
actions.clearRcsMediaAsset(0);
|
|
1003
|
+
actions.clearRcsMediaAsset(1);
|
|
1004
|
+
for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
|
|
1005
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
|
|
1006
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
|
|
1007
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
|
|
1008
|
+
}
|
|
1009
|
+
setCarouselData([]);
|
|
1010
|
+
setCarouselErrors([]);
|
|
466
1011
|
}
|
|
467
1012
|
return () => {
|
|
468
1013
|
actions.clearEditResponse();
|
|
@@ -470,59 +1015,68 @@ export const Rcs = (props) => {
|
|
|
470
1015
|
}, [paramObj.id]);
|
|
471
1016
|
|
|
472
1017
|
useEffect(() => {
|
|
473
|
-
|
|
1018
|
+
if (!(isEditFlow || !isFullMode)) return;
|
|
474
1019
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1020
|
+
const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
|
|
1021
|
+
|
|
1022
|
+
const initField = (targetString, setVarMap, slotOffset) => {
|
|
1023
|
+
const arr = splitTemplateVarStringRcs(targetString);
|
|
1024
|
+
if (!arr?.length) {
|
|
1025
|
+
setVarMap({});
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const nextVarMap = {};
|
|
1029
|
+
let varOrdinal = 0;
|
|
1030
|
+
arr.forEach((elem, idx) => {
|
|
1031
|
+
// Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
|
|
1032
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
1033
|
+
const id = `${elem}_${idx}`;
|
|
1034
|
+
const varName = getVarNameFromToken(elem);
|
|
1035
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
1036
|
+
varOrdinal += 1;
|
|
1037
|
+
const mappedValue = resolveCardVarMappedSlotValue(
|
|
1038
|
+
cardVarMapped,
|
|
1039
|
+
varName,
|
|
1040
|
+
globalSlot,
|
|
1041
|
+
isEditLike,
|
|
1042
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
1043
|
+
);
|
|
1044
|
+
nextVarMap[id] = mappedValue;
|
|
481
1045
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
// RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
|
|
486
|
-
if (rcsVarTestRegex.test(elem)) {
|
|
487
|
-
const id = `${elem}_${idx}`;
|
|
488
|
-
const varName = getVarNameFromToken(elem);
|
|
489
|
-
const mappedValue = (cardVarMapped?.[varName] ?? '').toString();
|
|
490
|
-
nextVarMap[id] = mappedValue;
|
|
491
|
-
if (mappedValue !== '') {
|
|
492
|
-
nextUpdated[idx] = mappedValue;
|
|
493
|
-
} else {
|
|
494
|
-
nextUpdated[idx] = elem;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
setVarMap(nextVarMap);
|
|
499
|
-
setUpdated(nextUpdated);
|
|
500
|
-
};
|
|
1046
|
+
});
|
|
1047
|
+
setVarMap(nextVarMap);
|
|
1048
|
+
};
|
|
501
1049
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1050
|
+
initField(templateTitle, setTitleVarMappedData, 0);
|
|
1051
|
+
initField(templateDesc, setDescVarMappedData, titleTokenCount);
|
|
1052
|
+
}, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
|
|
1053
|
+
|
|
1054
|
+
useEffect(() => {
|
|
1055
|
+
if (!isEditFlow && isFullMode) {
|
|
508
1056
|
setRcsVideoSrc({});
|
|
509
1057
|
updateRcsImageSrc('');
|
|
510
|
-
|
|
511
|
-
updateRcsThumbnailSrc('');
|
|
1058
|
+
setRcsThumbnailSrc('');
|
|
512
1059
|
setAssetList({});
|
|
513
|
-
|
|
514
|
-
|
|
1060
|
+
}
|
|
1061
|
+
}, [templateMediaType]);
|
|
515
1062
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1063
|
+
/** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
|
|
1064
|
+
const templateStatusHelper = (cardContentFirst) => {
|
|
1065
|
+
const raw =
|
|
1066
|
+
cardContentFirst?.Status
|
|
1067
|
+
?? cardContentFirst?.status
|
|
1068
|
+
?? cardContentFirst?.approvalStatus
|
|
1069
|
+
?? '';
|
|
1070
|
+
const status = typeof raw === 'string' ? raw.trim() : String(raw);
|
|
1071
|
+
const normalizedStatus = status.toLowerCase();
|
|
1072
|
+
switch (normalizedStatus) {
|
|
519
1073
|
case RCS_STATUSES.approved:
|
|
520
1074
|
setTemplateStatus(RCS_STATUSES.approved);
|
|
521
1075
|
break;
|
|
522
1076
|
case RCS_STATUSES.pending:
|
|
523
1077
|
setTemplateStatus(RCS_STATUSES.pending);
|
|
524
1078
|
break;
|
|
525
|
-
case RCS_STATUSES.awaitingApproval:
|
|
1079
|
+
case RCS_STATUSES.awaitingApproval.toLowerCase():
|
|
526
1080
|
setTemplateStatus(RCS_STATUSES.awaitingApproval);
|
|
527
1081
|
break;
|
|
528
1082
|
case RCS_STATUSES.unavailable:
|
|
@@ -531,6 +1085,9 @@ export const Rcs = (props) => {
|
|
|
531
1085
|
case RCS_STATUSES.rejected:
|
|
532
1086
|
setTemplateStatus(RCS_STATUSES.rejected);
|
|
533
1087
|
break;
|
|
1088
|
+
case RCS_STATUSES.created:
|
|
1089
|
+
setTemplateStatus(RCS_STATUSES.created);
|
|
1090
|
+
break;
|
|
534
1091
|
default:
|
|
535
1092
|
setTemplateStatus(status);
|
|
536
1093
|
break;
|
|
@@ -584,48 +1141,261 @@ export const Rcs = (props) => {
|
|
|
584
1141
|
};
|
|
585
1142
|
|
|
586
1143
|
useEffect(() => {
|
|
587
|
-
const details =
|
|
1144
|
+
const details = rcsHydrationDetails;
|
|
588
1145
|
if (details && Object.keys(details).length > 0) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1146
|
+
// Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
|
|
1147
|
+
const cardFromVersions = get(
|
|
1148
|
+
details,
|
|
1149
|
+
'versions.base.content.RCS.rcsContent.cardContent[0]',
|
|
1150
|
+
);
|
|
1151
|
+
const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
|
|
1152
|
+
const cardType = (rcsContent?.cardType || '').toString();
|
|
1153
|
+
|
|
1154
|
+
setEditFlow(true);
|
|
1155
|
+
setTemplateName(details?.name || details?.creativeName || '');
|
|
1156
|
+
|
|
1157
|
+
const cardFromTop = get(details, 'rcsContent.cardContent[0]');
|
|
1158
|
+
const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
|
|
1159
|
+
const cardVarMappedFromCardContent =
|
|
1160
|
+
card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
|
|
1161
|
+
? card0.cardVarMapped
|
|
1162
|
+
: {};
|
|
1163
|
+
const cardVarMappedFromRootMirror =
|
|
1164
|
+
details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
|
|
1165
|
+
? details.rcsCardVarMapped
|
|
1166
|
+
: {};
|
|
1167
|
+
// Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
|
|
1168
|
+
// nested versions.cardContent[0].cardVarMapped is dropped on reload.
|
|
1169
|
+
const mergedCardVarMappedFromPayload = {
|
|
1170
|
+
...cardVarMappedFromRootMirror,
|
|
1171
|
+
...cardVarMappedFromCardContent,
|
|
1172
|
+
};
|
|
1173
|
+
const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
|
|
1174
|
+
const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
|
|
1175
|
+
const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
|
|
1176
|
+
Object.keys(mergedCardVarMappedFromPayload)
|
|
1177
|
+
.sort()
|
|
1178
|
+
.reduce((accumulator, mapKey) => {
|
|
1179
|
+
accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
|
|
1180
|
+
return accumulator;
|
|
1181
|
+
}, {}),
|
|
1182
|
+
)}`;
|
|
1183
|
+
if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
|
|
1184
|
+
lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
|
|
1185
|
+
setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
|
|
1186
|
+
}
|
|
1187
|
+
const tokenListForMap = [
|
|
1188
|
+
...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
|
|
1189
|
+
...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
|
|
1190
|
+
];
|
|
1191
|
+
const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
|
|
1192
|
+
// Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
|
|
1193
|
+
// getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
|
|
1194
|
+
const cardVarMappedBeforeCoalesce = isFullMode
|
|
1195
|
+
? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
|
|
1196
|
+
: { ...mergedCardVarMappedFromPayload };
|
|
1197
|
+
const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
|
|
1198
|
+
cardVarMappedBeforeCoalesce,
|
|
1199
|
+
loadedTitleForMap,
|
|
1200
|
+
loadedDescForMap,
|
|
1201
|
+
rcsVarRegex,
|
|
1202
|
+
);
|
|
1203
|
+
const cardVarMappedAfterNumericSlotSync = !isFullMode
|
|
1204
|
+
? syncCardVarMappedSemanticsFromSlots(
|
|
1205
|
+
cardVarMappedAfterCoalesce,
|
|
1206
|
+
loadedTitleForMap,
|
|
1207
|
+
loadedDescForMap,
|
|
1208
|
+
rcsVarRegex,
|
|
1209
|
+
)
|
|
1210
|
+
: cardVarMappedAfterCoalesce;
|
|
1211
|
+
const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
|
|
1212
|
+
// Pre-populate variable/tag mappings while opening an existing template in edit flows
|
|
1213
|
+
setCardVarMapped((previousVarMapState) => {
|
|
1214
|
+
const previousVarMap = previousVarMapState ?? {};
|
|
1215
|
+
if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
|
|
1216
|
+
const previousVarMapKeys = Object.keys(previousVarMap);
|
|
1217
|
+
const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
|
|
1218
|
+
if (previousVarMapKeys.length === nextVarMapKeys.length) {
|
|
1219
|
+
const allSlotValuesMatchPrevious = previousVarMapKeys.every(
|
|
1220
|
+
(key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
|
|
1221
|
+
);
|
|
1222
|
+
if (allSlotValuesMatchPrevious) return previousVarMapState;
|
|
1223
|
+
}
|
|
1224
|
+
return hydratedCardVarMappedResult;
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
if (cardType.toUpperCase() === contentType.carousel) {
|
|
1228
|
+
|
|
1229
|
+
setTemplateType(contentType.carousel);
|
|
1230
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1231
|
+
// Library/campaign payloads can store cardSettings/cardContent at the flat top-level
|
|
1232
|
+
// `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
|
|
1233
|
+
// used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
|
|
1234
|
+
// silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
|
|
1235
|
+
// actually uploaded at and causing object-fit:cover to crop the preview image.
|
|
1236
|
+
const cardSettings = rcsContent?.cardSettings
|
|
1237
|
+
|| get(details, 'rcsContent.cardSettings', {});
|
|
1238
|
+
const cardWidth = cardSettings?.cardWidth || SMALL;
|
|
1239
|
+
setSelectedCarouselWidth(cardWidth);
|
|
1240
|
+
|
|
1241
|
+
const cards = Array.isArray(rcsContent?.cardContent)
|
|
1242
|
+
? rcsContent.cardContent
|
|
1243
|
+
: get(details, 'rcsContent.cardContent', []);
|
|
1244
|
+
const firstHeight = cards?.[0]?.media?.height || MEDIUM;
|
|
1245
|
+
setSelectedCarouselHeight(firstHeight);
|
|
1246
|
+
setSelectedCarousel(`${firstHeight}_${cardWidth}`);
|
|
1247
|
+
setActiveCarouselIndex('0');
|
|
1248
|
+
|
|
1249
|
+
if (cards.length > 1) {
|
|
1250
|
+
// Each card's own `cardVarMapped` is already keyed by global var position (see
|
|
1251
|
+
// computeCarouselVarSlotKeys) — a slot's key is assigned once, from where its token
|
|
1252
|
+
// sits in the template text, and never depends on which tag fills it. So merging every
|
|
1253
|
+
// card's own map directly is correct and collision-free: no per-card coalescing,
|
|
1254
|
+
// normalizing, or re-scoping needed (unlike the old name-based scoped-key scheme).
|
|
1255
|
+
setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}) => {
|
|
1256
|
+
const rawCardMap = c?.cardVarMapped;
|
|
1257
|
+
if (rawCardMap == null || typeof rawCardMap !== 'object') return merged;
|
|
1258
|
+
return { ...merged, ...rawCardMap };
|
|
1259
|
+
}, { ...(previousVarMap || {}) }));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const hydratedCards = cards.map((c = {}, idx) => {
|
|
1263
|
+
const mediaType = c.mediaType;
|
|
1264
|
+
const media = c.media || {};
|
|
1265
|
+
const mediaUrl = media.mediaUrl || '';
|
|
1266
|
+
const thumbUrl = media.thumbnailUrl || '';
|
|
1267
|
+
const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
|
|
1268
|
+
const suggestions = idx === 0 && rawSuggestions.length === 0
|
|
1269
|
+
? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
|
|
1270
|
+
: rawSuggestions;
|
|
1271
|
+
return {
|
|
1272
|
+
title: c.title || '',
|
|
1273
|
+
description: c.description || '',
|
|
1274
|
+
mediaType,
|
|
1275
|
+
imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
|
|
1276
|
+
videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
|
|
1277
|
+
videoSrc: mediaUrl,
|
|
1278
|
+
previewUrl: thumbUrl,
|
|
1279
|
+
videoThumbnail: thumbUrl,
|
|
1280
|
+
videoName: c?.media?.videoName || '',
|
|
1281
|
+
} : {},
|
|
1282
|
+
thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
|
|
1283
|
+
suggestions,
|
|
1284
|
+
};
|
|
1285
|
+
});
|
|
1286
|
+
setCarouselData(
|
|
1287
|
+
hydratedCards.length > 0
|
|
1288
|
+
? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
|
|
1289
|
+
: [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
|
|
1290
|
+
);
|
|
1291
|
+
setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
|
|
1292
|
+
|
|
1293
|
+
// Status bar uses first card's Status, keep existing behavior.
|
|
1294
|
+
if (isHostInfoBip) {
|
|
1295
|
+
setTemplateStatus('');
|
|
1296
|
+
} else {
|
|
1297
|
+
const firstCard = cards?.[0] || {};
|
|
1298
|
+
const cardForCarouselStatus = {
|
|
1299
|
+
...firstCard,
|
|
1300
|
+
Status:
|
|
1301
|
+
firstCard.Status
|
|
1302
|
+
?? firstCard.status
|
|
1303
|
+
?? firstCard.approvalStatus
|
|
1304
|
+
?? get(details, 'templateStatus')
|
|
1305
|
+
?? get(details, 'approvalStatus')
|
|
1306
|
+
?? get(details, 'creativeStatus')
|
|
1307
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1308
|
+
?? '',
|
|
1309
|
+
};
|
|
1310
|
+
templateStatusHelper(cardForCarouselStatus);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Hydrate SMS fallback for carousel — same logic as for non-carousel types below.
|
|
1314
|
+
// Without this the early `return` skips fallback hydration entirely, so reopening
|
|
1315
|
+
// a carousel template never restores the saved SMS fallback data.
|
|
1316
|
+
applySmsFallbackHydration({
|
|
1317
|
+
details,
|
|
1318
|
+
isFullMode,
|
|
1319
|
+
lastSmsFallbackHydrationKeyRef,
|
|
1320
|
+
setSmsFallbackData,
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
return;
|
|
592
1324
|
}
|
|
593
|
-
|
|
594
|
-
|
|
1325
|
+
|
|
1326
|
+
const mediaType =
|
|
1327
|
+
card0.mediaType
|
|
1328
|
+
|| get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
|
|
1329
|
+
if (cardType.toUpperCase() !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
|
|
595
1330
|
setTemplateType(contentType.text_message);
|
|
596
|
-
} else {
|
|
1331
|
+
} else if (cardType.toUpperCase() !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
|
|
597
1332
|
setTemplateType(contentType.rich_card);
|
|
598
1333
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1334
|
+
|
|
1335
|
+
const loadedTitle = loadedTitleForMap;
|
|
1336
|
+
const loadedDesc = loadedDescForMap;
|
|
1337
|
+
const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
|
|
1338
|
+
loadedTitle,
|
|
1339
|
+
loadedDesc,
|
|
1340
|
+
isFullMode,
|
|
1341
|
+
cardVarMappedAfterHydration: hydratedCardVarMappedResult,
|
|
1342
|
+
rcsVarRegex,
|
|
1343
|
+
});
|
|
606
1344
|
setTemplateTitle(normalizedTitle);
|
|
607
1345
|
setTemplateDesc(normalizedDesc);
|
|
608
|
-
setSuggestions(
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1346
|
+
setSuggestions(
|
|
1347
|
+
Array.isArray(card0.suggestions)
|
|
1348
|
+
? card0.suggestions
|
|
1349
|
+
: get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
|
|
1350
|
+
);
|
|
1351
|
+
const cardForStatus = {
|
|
1352
|
+
...card0,
|
|
1353
|
+
Status:
|
|
1354
|
+
card0.Status
|
|
1355
|
+
?? card0.status
|
|
1356
|
+
?? card0.approvalStatus
|
|
1357
|
+
?? get(details, 'templateStatus')
|
|
1358
|
+
?? get(details, 'approvalStatus')
|
|
1359
|
+
?? get(details, 'creativeStatus')
|
|
1360
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1361
|
+
?? '',
|
|
1362
|
+
};
|
|
1363
|
+
if (isHostInfoBip) {
|
|
1364
|
+
setTemplateStatus('');
|
|
1365
|
+
} else {
|
|
1366
|
+
templateStatusHelper(cardForStatus);
|
|
1367
|
+
}
|
|
1368
|
+
const mediaData =
|
|
1369
|
+
card0.media != null && card0.media !== ''
|
|
1370
|
+
? card0.media
|
|
1371
|
+
: get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
|
|
1372
|
+
const cardSettings =
|
|
1373
|
+
get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
|
|
1374
|
+
|| get(details, 'rcsContent.cardSettings', '');
|
|
612
1375
|
setMediaData(mediaData, mediaType, cardSettings);
|
|
613
1376
|
if (details?.edit) {
|
|
614
1377
|
const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
|
|
615
1378
|
setRcsAccount(rcsAccountId);
|
|
616
1379
|
}
|
|
1380
|
+
|
|
1381
|
+
applySmsFallbackHydration({
|
|
1382
|
+
details,
|
|
1383
|
+
isFullMode,
|
|
1384
|
+
lastSmsFallbackHydrationKeyRef,
|
|
1385
|
+
setSmsFallbackData,
|
|
1386
|
+
});
|
|
617
1387
|
}
|
|
618
|
-
}, [
|
|
619
|
-
|
|
1388
|
+
}, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
|
|
620
1389
|
|
|
621
1390
|
useEffect(() => {
|
|
622
1391
|
if (templateType === contentType.text_message) {
|
|
623
1392
|
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
624
|
-
|
|
625
|
-
|
|
1393
|
+
// Full-mode create only: switching to plain text clears draft title/media. Never clear when
|
|
1394
|
+
// hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
|
|
626
1395
|
if (!isEditFlow && isFullMode) {
|
|
1396
|
+
setTemplateTitle('');
|
|
1397
|
+
setTemplateTitleError('');
|
|
627
1398
|
setUpdateRcsImageSrc('');
|
|
628
|
-
setUpdateRcsVideoSrc({});
|
|
629
1399
|
setRcsVideoSrc({});
|
|
630
1400
|
setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
|
|
631
1401
|
}
|
|
@@ -649,23 +1419,22 @@ export const Rcs = (props) => {
|
|
|
649
1419
|
}, [templateType, isEditFlow, isFullMode]);
|
|
650
1420
|
|
|
651
1421
|
useEffect(() => {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
globalActions.fetchSchemaForEntity(query);
|
|
1422
|
+
const { type, module } = location.query || {};
|
|
1423
|
+
const isEmbedded = type === EMBEDDED;
|
|
1424
|
+
// Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
|
|
1425
|
+
const context = isEmbedded ? module : 'outbound';
|
|
1426
|
+
const embedded = isEmbedded ? type : FULL;
|
|
1427
|
+
const query = {
|
|
1428
|
+
layout: SMS,
|
|
1429
|
+
type: TAG,
|
|
1430
|
+
context,
|
|
1431
|
+
embedded,
|
|
1432
|
+
};
|
|
1433
|
+
if (getDefaultTags) {
|
|
1434
|
+
query.context = getDefaultTags;
|
|
667
1435
|
}
|
|
668
|
-
|
|
1436
|
+
fetchTagSchemaIfNewQuery(query);
|
|
1437
|
+
}, [fetchTagSchemaIfNewQuery]);
|
|
669
1438
|
|
|
670
1439
|
useEffect(() => {
|
|
671
1440
|
let tag = get(metaEntities, `tags.standard`, []);
|
|
@@ -688,39 +1457,167 @@ export const Rcs = (props) => {
|
|
|
688
1457
|
context,
|
|
689
1458
|
embedded,
|
|
690
1459
|
};
|
|
1460
|
+
if (getDefaultTags) {
|
|
1461
|
+
query.context = getDefaultTags;
|
|
1462
|
+
}
|
|
1463
|
+
fetchTagSchemaIfNewQuery(query);
|
|
691
1464
|
globalActions.fetchSchemaForEntity(query);
|
|
692
1465
|
};
|
|
693
1466
|
|
|
694
|
-
const
|
|
695
|
-
if (!
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1467
|
+
const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
|
|
1468
|
+
if (!templateStr || !numericVarName || !tagName) return templateStr;
|
|
1469
|
+
const placeholderRegex = buildRcsNumericMustachePlaceholderRegex(numericVarName);
|
|
1470
|
+
return templateStr.replace(placeholderRegex, `{{${tagName}}}`);
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
|
|
1474
|
+
if (!varSegmentCompositeDomId) return;
|
|
1475
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
1476
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
1477
|
+
const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
|
|
1478
|
+
if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
|
|
1479
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
1480
|
+
const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
1481
|
+
if (!semanticOrNumericVarName) return;
|
|
1482
|
+
const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
|
|
1483
|
+
const templateStringForField =
|
|
1484
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
|
|
1485
|
+
const titleOrMessageFieldType =
|
|
1486
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
|
|
1487
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
1488
|
+
varSegmentCompositeDomId,
|
|
1489
|
+
templateStringForField,
|
|
1490
|
+
titleOrMessageFieldType,
|
|
1491
|
+
);
|
|
1492
|
+
const cardVarMappedNumericSlotKey =
|
|
1493
|
+
globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
|
|
1494
|
+
? String(globalVarSlotIndexZeroBased + 1)
|
|
1495
|
+
: null;
|
|
1496
|
+
|
|
1497
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
1498
|
+
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
1499
|
+
if (isNumericPlaceholderSlot) {
|
|
1500
|
+
const existingValueBeforeAppend = (
|
|
1501
|
+
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
1502
|
+
).toString();
|
|
1503
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1504
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
1505
|
+
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
1506
|
+
} else {
|
|
1507
|
+
// Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
|
|
1508
|
+
// "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
|
|
1509
|
+
// read/write the global numeric slot only and drop the shared semantic key.
|
|
1510
|
+
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
1511
|
+
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
1512
|
+
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
1513
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1514
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
1515
|
+
if (cardVarMappedNumericSlotKey) {
|
|
1516
|
+
updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
|
|
1517
|
+
// Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
|
|
1518
|
+
const fieldOffset = titleOrMessageFieldType === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
1519
|
+
const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
|
|
1520
|
+
let varOrdinal = 0;
|
|
1521
|
+
for (let i = 0; i < fieldSegments.length; i++) {
|
|
1522
|
+
if (rcsVarTestRegex.test(fieldSegments[i])) {
|
|
1523
|
+
const slotVarName = getVarNameFromToken(fieldSegments[i]);
|
|
1524
|
+
const globalSlot = fieldOffset + varOrdinal;
|
|
1525
|
+
varOrdinal += 1;
|
|
1526
|
+
if (slotVarName === semanticOrNumericVarName && globalSlot !== globalVarSlotIndexZeroBased) {
|
|
1527
|
+
updatedCardVarMapped[String(globalSlot + 1)] = mappedValueAfterAppendingTag;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
} else {
|
|
1532
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return updatedCardVarMapped;
|
|
710
1536
|
});
|
|
1537
|
+
|
|
1538
|
+
if (
|
|
1539
|
+
isNumericPlaceholderSlot
|
|
1540
|
+
&& (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
|
|
1541
|
+
) {
|
|
1542
|
+
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
1543
|
+
setTemplateTitle((previousTitle) => {
|
|
1544
|
+
const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
1545
|
+
previousTitle || '',
|
|
1546
|
+
semanticOrNumericVarName,
|
|
1547
|
+
selectedTagNameFromPicker,
|
|
1548
|
+
);
|
|
1549
|
+
if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
|
|
1550
|
+
setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
|
|
1551
|
+
// Remount segment editor: tag insert replaces {{n}} with e.g. {{tag.FORMAT_1}} — slot ids change; avoids stale UI vs manual typing in full-mode TextArea
|
|
1552
|
+
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1553
|
+
return titleAfterReplacingNumericPlaceholder;
|
|
1554
|
+
});
|
|
1555
|
+
} else {
|
|
1556
|
+
setTemplateDesc((previousDescription) => {
|
|
1557
|
+
const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
1558
|
+
previousDescription || '',
|
|
1559
|
+
semanticOrNumericVarName,
|
|
1560
|
+
selectedTagNameFromPicker,
|
|
1561
|
+
);
|
|
1562
|
+
if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
|
|
1563
|
+
return previousDescription;
|
|
1564
|
+
}
|
|
1565
|
+
setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
|
|
1566
|
+
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1567
|
+
return descriptionAfterReplacingNumericPlaceholder;
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
711
1571
|
};
|
|
712
1572
|
|
|
713
|
-
const onTitleTagSelect = (
|
|
1573
|
+
const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
|
|
1574
|
+
|
|
1575
|
+
const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
|
|
714
1576
|
|
|
715
|
-
|
|
1577
|
+
/** Field-local var ordinal (0-based, vars only) for a `{{tok}}_segIdx` composite id — scoped to
|
|
1578
|
+
* a single field's own template string (title and description are tracked independently). */
|
|
1579
|
+
const getCarouselFieldLocalVarOrdinal = (compositeId, fieldTemplateStr) => {
|
|
1580
|
+
const segments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
|
|
1581
|
+
let varOrdinal = 0;
|
|
1582
|
+
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
|
|
1583
|
+
if (rcsVarTestRegex.test(segments[segmentIndex])) {
|
|
1584
|
+
if (`${segments[segmentIndex]}_${segmentIndex}` === compositeId) return varOrdinal;
|
|
1585
|
+
varOrdinal += 1;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return null;
|
|
1589
|
+
};
|
|
716
1590
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1591
|
+
// The card's placeholder (numeric {{N}} or a bare semantic token like {{first_name}}) is locked
|
|
1592
|
+
// here — picking a tag only supplies/replaces the *value* behind that slot in cardVarMapped, it
|
|
1593
|
+
// never rewrites the card's title/description text. `focusedVarId` is passed in by the caller
|
|
1594
|
+
// (title vs description each track their own last-focused slot, like titleTextAreaId/
|
|
1595
|
+
// descTextAreaId) so a click on one field's "Add Labels" can never apply to the other field's
|
|
1596
|
+
// last-focused slot. The slot's key is its global position across the whole carousel — assigned
|
|
1597
|
+
// once from where its token sits in the template text, never from whatever tag currently fills
|
|
1598
|
+
// it — so swapping tags never changes the key and never collides with another card's slot.
|
|
1599
|
+
const onCarouselTagSelect = (data, focusedVarId, isDescField) => {
|
|
1600
|
+
if (!focusedVarId) return;
|
|
1601
|
+
const activeIdx = parseInt(activeCarouselIndex, 10);
|
|
1602
|
+
const cardIndex = isNaN(activeIdx) ? 0 : activeIdx;
|
|
1603
|
+
const fieldTemplateStr = (isDescField
|
|
1604
|
+
? carouselData?.[cardIndex]?.description
|
|
1605
|
+
: carouselData?.[cardIndex]?.title) || '';
|
|
1606
|
+
const localVarOrdinal = getCarouselFieldLocalVarOrdinal(focusedVarId, fieldTemplateStr);
|
|
1607
|
+
if (localVarOrdinal == null) return;
|
|
1608
|
+
const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
|
|
1609
|
+
const slotKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
|
|
1610
|
+
if (!slotKey) return;
|
|
1611
|
+
|
|
1612
|
+
setCardVarMapped((prev) => ({
|
|
1613
|
+
...(prev || {}),
|
|
1614
|
+
[slotKey]: `{{${data}}}`,
|
|
1615
|
+
}));
|
|
722
1616
|
};
|
|
723
1617
|
|
|
1618
|
+
const onCarouselTitleTagSelect = (data) => onCarouselTagSelect(data, carouselTitleFocusedVarId, false);
|
|
1619
|
+
|
|
1620
|
+
const onCarouselDescTagSelect = (data) => onCarouselTagSelect(data, carouselDescFocusedVarId, true);
|
|
724
1621
|
|
|
725
1622
|
//removing optout tag for rcs
|
|
726
1623
|
const getRcsTags = () => {
|
|
@@ -733,15 +1630,14 @@ export const Rcs = (props) => {
|
|
|
733
1630
|
};
|
|
734
1631
|
// tag Code end
|
|
735
1632
|
|
|
736
|
-
const renderLabel = (value,
|
|
737
|
-
const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
|
|
1633
|
+
const renderLabel = (value, desc) => {
|
|
738
1634
|
return (
|
|
739
1635
|
<>
|
|
740
|
-
<
|
|
1636
|
+
<div className="rcs-form-section-heading">
|
|
741
1637
|
<CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
|
|
742
|
-
</
|
|
1638
|
+
</div>
|
|
743
1639
|
{desc && (
|
|
744
|
-
<CapLabel type="label3"
|
|
1640
|
+
<CapLabel type="label3" className="rcs-form-field-caption">
|
|
745
1641
|
{formatMessage(messages[desc])}
|
|
746
1642
|
</CapLabel>
|
|
747
1643
|
)}
|
|
@@ -760,13 +1656,8 @@ export const Rcs = (props) => {
|
|
|
760
1656
|
},
|
|
761
1657
|
{
|
|
762
1658
|
value: contentType.carousel,
|
|
763
|
-
label: (
|
|
764
|
-
|
|
765
|
-
{formatMessage(messages.carousel)}
|
|
766
|
-
</CapTooltip>
|
|
767
|
-
),
|
|
768
|
-
disabled: true,
|
|
769
|
-
},
|
|
1659
|
+
label: formatMessage(messages.carousel),
|
|
1660
|
+
}
|
|
770
1661
|
];
|
|
771
1662
|
|
|
772
1663
|
const onTemplateNameChange = ({ target: { value } }) => {
|
|
@@ -777,6 +1668,10 @@ export const Rcs = (props) => {
|
|
|
777
1668
|
|
|
778
1669
|
const onTemplateTypeChange = ({ target: { value } }) => {
|
|
779
1670
|
setTemplateType(value);
|
|
1671
|
+
// Carousel has per-card media; keep template-level media type neutral.
|
|
1672
|
+
if (value === contentType.carousel) {
|
|
1673
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1674
|
+
}
|
|
780
1675
|
};
|
|
781
1676
|
|
|
782
1677
|
|
|
@@ -797,8 +1692,39 @@ export const Rcs = (props) => {
|
|
|
797
1692
|
const onTemplateMediaTypeChange = ({ target: { value } }) => {
|
|
798
1693
|
setTemplateMediaType(value);
|
|
799
1694
|
};
|
|
800
|
-
|
|
801
|
-
|
|
1695
|
+
const renderedRCSEditMessage = (descArray, type) => {
|
|
1696
|
+
const renderArray = [];
|
|
1697
|
+
if (descArray?.length) {
|
|
1698
|
+
descArray.forEach((elem, index) => {
|
|
1699
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
1700
|
+
// Variable input
|
|
1701
|
+
renderArray.push(
|
|
1702
|
+
<TextArea
|
|
1703
|
+
id={`${elem}_${index}`}
|
|
1704
|
+
key={`${elem}_${index}`}
|
|
1705
|
+
placeholder={`enter the value for ${elem}`}
|
|
1706
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1707
|
+
onChange={e => textAreaValueChange(e)}
|
|
1708
|
+
value={textAreaValue(index, type)}
|
|
1709
|
+
onFocus={(e) => setTextAreaId(e, type)}
|
|
1710
|
+
/>
|
|
1711
|
+
);
|
|
1712
|
+
} else if (elem) {
|
|
1713
|
+
// Static text
|
|
1714
|
+
renderArray.push(
|
|
1715
|
+
<TextArea
|
|
1716
|
+
key={`static_${index}`}
|
|
1717
|
+
value={elem}
|
|
1718
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1719
|
+
disabled
|
|
1720
|
+
className="rcs-edit-template-message-static-textarea"
|
|
1721
|
+
/>
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
return renderArray;
|
|
1727
|
+
};
|
|
802
1728
|
const onTemplateTitleChange = ({ target: { value } }) => {
|
|
803
1729
|
let errorMessage = false;
|
|
804
1730
|
if (templateType === contentType.rich_card && !value.trim()) {
|
|
@@ -814,7 +1740,7 @@ export const Rcs = (props) => {
|
|
|
814
1740
|
|
|
815
1741
|
const onTemplateDescChange = ({ target: { value } }) => {
|
|
816
1742
|
let errorMessage = false;
|
|
817
|
-
if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
|
|
1743
|
+
if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
|
|
818
1744
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
819
1745
|
} else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
|
|
820
1746
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
@@ -830,16 +1756,16 @@ export const Rcs = (props) => {
|
|
|
830
1756
|
|
|
831
1757
|
const templateDescErrorHandler = (value) => {
|
|
832
1758
|
let errorMessage = false;
|
|
833
|
-
const { isBraceError } = validateTags({
|
|
1759
|
+
const { unsupportedTags, isBraceError } = validateTags({
|
|
834
1760
|
content: value,
|
|
835
1761
|
tagsParam: tags,
|
|
1762
|
+
injectedTagsParams: injectedTags,
|
|
836
1763
|
location,
|
|
837
1764
|
tagModule: getDefaultTags,
|
|
838
|
-
isFullMode,
|
|
839
1765
|
}) || {};
|
|
840
1766
|
|
|
841
1767
|
const maxLength = templateType === contentType.text_message
|
|
842
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH
|
|
1768
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
|
|
843
1769
|
: RCS_RICH_CARD_MAX_LENGTH;
|
|
844
1770
|
|
|
845
1771
|
if (value === '' && isMediaTypeText) {
|
|
@@ -855,19 +1781,6 @@ export const Rcs = (props) => {
|
|
|
855
1781
|
};
|
|
856
1782
|
|
|
857
1783
|
|
|
858
|
-
const onFallbackMessageChange = ({ target: { value } }) => {
|
|
859
|
-
const error = fallbackMessageErrorHandler(value);
|
|
860
|
-
setFallbackMessage(value);
|
|
861
|
-
setFallbackMessageError(error);
|
|
862
|
-
};
|
|
863
|
-
|
|
864
|
-
const fallbackMessageErrorHandler = (value) => {
|
|
865
|
-
if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
|
|
866
|
-
return formatMessage(messages.fallbackMsgLenError);
|
|
867
|
-
}
|
|
868
|
-
return false;
|
|
869
|
-
};
|
|
870
|
-
|
|
871
1784
|
// Check for forbidden characters: square brackets [] and single curly braces {}
|
|
872
1785
|
const forbiddenCharactersValidation = (value) => {
|
|
873
1786
|
if (!value) return false;
|
|
@@ -912,53 +1825,43 @@ export const Rcs = (props) => {
|
|
|
912
1825
|
if(!isFullMode){
|
|
913
1826
|
return false;
|
|
914
1827
|
}
|
|
915
|
-
|
|
1828
|
+
// Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
|
|
1829
|
+
if (!/^[\w.]+$/.test(paramName)) {
|
|
916
1830
|
return formatMessage(messages.unknownCharactersError);
|
|
917
1831
|
}
|
|
918
1832
|
}
|
|
919
1833
|
return false;
|
|
920
1834
|
};
|
|
921
|
-
|
|
922
|
-
const templateHeaderErrorHandler = (value) => {
|
|
923
|
-
let errorMessage = false;
|
|
924
|
-
if (value?.length > TEMPLATE_HEADER_MAX_LENGTH) {
|
|
925
|
-
errorMessage = formatMessage(messages.templateHeaderLengthError);
|
|
926
|
-
} else {
|
|
927
|
-
errorMessage = variableErrorHandling(value);
|
|
928
|
-
}
|
|
929
|
-
return errorMessage;
|
|
930
|
-
};
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const templateMessageErrorHandler = (value) => {
|
|
934
|
-
let errorMessage = false;
|
|
935
|
-
if (value === '') {
|
|
936
|
-
errorMessage = formatMessage(messages.emptyTemplateMessageErrorMessage);
|
|
937
|
-
} else if (
|
|
938
|
-
value?.length
|
|
939
|
-
> TEMPLATE_MESSAGE_MAX_LENGTH
|
|
940
|
-
) {
|
|
941
|
-
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
942
|
-
} else {
|
|
943
|
-
errorMessage = variableErrorHandling(value);
|
|
944
|
-
}
|
|
945
|
-
return errorMessage;
|
|
946
|
-
};
|
|
947
|
-
|
|
948
1835
|
|
|
949
1836
|
const onMessageAddVar = () => {
|
|
950
|
-
onAddVar(
|
|
1837
|
+
onAddVar(templateDesc);
|
|
951
1838
|
};
|
|
952
1839
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1840
|
+
/**
|
|
1841
|
+
* Returns the smallest positive integer not already used as a `{{N}}` variable
|
|
1842
|
+
* in either the title or description fields, or null if the limit (19) is reached.
|
|
1843
|
+
* Scans both fields so title and description vars never share the same number
|
|
1844
|
+
* (duplicate numbers would share a cardVarMapped key and bleed values across fields).
|
|
1845
|
+
*/
|
|
1846
|
+
const getNextRcsNumericVarNumber = (titleStr, descStr) => {
|
|
1847
|
+
const allExistingVars = [
|
|
1848
|
+
...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
|
|
1849
|
+
...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
|
|
1850
|
+
];
|
|
1851
|
+
const existingNumbers = allExistingVars.flatMap(varToken => {
|
|
1852
|
+
const numericMatch = varToken.match(/\d+/);
|
|
1853
|
+
return numericMatch ? [parseInt(numericMatch[0], 10)] : [];
|
|
1854
|
+
});
|
|
957
1855
|
let nextNumber = 1;
|
|
958
1856
|
while (existingNumbers.includes(nextNumber)) {
|
|
959
1857
|
nextNumber++;
|
|
960
1858
|
}
|
|
961
|
-
|
|
1859
|
+
return nextNumber > 19 ? null : nextNumber;
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
const onAddVar = (messageContent) => {
|
|
1863
|
+
const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
|
|
1864
|
+
if (nextNumber === null) {
|
|
962
1865
|
return;
|
|
963
1866
|
}
|
|
964
1867
|
const nextVar = `{{${nextNumber}}}`;
|
|
@@ -970,15 +1873,13 @@ const onAddVar = (type, messageContent, regex) => {
|
|
|
970
1873
|
};
|
|
971
1874
|
|
|
972
1875
|
const onTitleAddVar = () => {
|
|
973
|
-
//
|
|
1876
|
+
// Scan both title AND description so the new title var number doesn't
|
|
1877
|
+
// duplicate a number already used in the description. Duplicate numeric
|
|
1878
|
+
// names would share the same cardVarMapped semantic key, causing the
|
|
1879
|
+
// description slot to reflect the title slot value and vice-versa.
|
|
974
1880
|
const messageContent = templateTitle;
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
let nextNumber = 1;
|
|
978
|
-
while (existingNumbers.includes(nextNumber)) {
|
|
979
|
-
nextNumber++;
|
|
980
|
-
}
|
|
981
|
-
if (nextNumber > 19) {
|
|
1881
|
+
const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
|
|
1882
|
+
if (nextNumber === null) {
|
|
982
1883
|
return;
|
|
983
1884
|
}
|
|
984
1885
|
const nextVar = `{{${nextNumber}}}`;
|
|
@@ -990,26 +1891,17 @@ const onTitleAddVar = () => {
|
|
|
990
1891
|
setTemplateTitleError(error);
|
|
991
1892
|
};
|
|
992
1893
|
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
content = content.substring(index + validVarArr?.[0]?.length, content?.length);
|
|
1005
|
-
validVarArr?.shift();
|
|
1006
|
-
} else {
|
|
1007
|
-
templateVarArray.push(content);
|
|
1008
|
-
break;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return templateVarArray.filter(Boolean);
|
|
1012
|
-
};
|
|
1894
|
+
const appendVarToCarouselField = (cardIndex, fieldName) => {
|
|
1895
|
+
const token = getNextCarouselVarToken(carouselData);
|
|
1896
|
+
if (!token) return;
|
|
1897
|
+
setCarouselData((prev = []) => {
|
|
1898
|
+
const updated = cloneDeep(prev);
|
|
1899
|
+
if (!updated[cardIndex]) return prev;
|
|
1900
|
+
const current = (updated[cardIndex][fieldName] || '').toString();
|
|
1901
|
+
updated[cardIndex][fieldName] = `${current}${token}`;
|
|
1902
|
+
return updated;
|
|
1903
|
+
});
|
|
1904
|
+
};
|
|
1013
1905
|
|
|
1014
1906
|
const textAreaValue = (idValue, type) => {
|
|
1015
1907
|
if (idValue >= 0) {
|
|
@@ -1025,7 +1917,60 @@ const splitTemplateVarString = (str) => {
|
|
|
1025
1917
|
return "";
|
|
1026
1918
|
};
|
|
1027
1919
|
|
|
1028
|
-
|
|
1920
|
+
// Carousel: render variable-value editor for a given template string (title/description) belonging
|
|
1921
|
+
// to `cardIndex`. This matches rich-card/text edit behavior: static pieces are read-only, variable
|
|
1922
|
+
// tokens are editable. Each slot's key comes from `computeCarouselVarSlotKeys` — stable regardless
|
|
1923
|
+
// of which tag fills it, and independent even from another occurrence of the same tag name.
|
|
1924
|
+
const renderCarouselEditMessage = (templateStr, cardIndex, fieldType) => {
|
|
1925
|
+
const renderArray = [];
|
|
1926
|
+
const templateArr = splitTemplateVarString(templateStr);
|
|
1927
|
+
const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
|
|
1928
|
+
const slotKeysForCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
|
|
1929
|
+
const fieldKeys = (isDescField ? slotKeysForCard?.description : slotKeysForCard?.title) ?? [];
|
|
1930
|
+
let varOrdinal = 0;
|
|
1931
|
+
if (templateArr?.length) {
|
|
1932
|
+
templateArr.forEach((elem, index) => {
|
|
1933
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
1934
|
+
const varName = getVarNameFromToken(elem);
|
|
1935
|
+
const mapKey = fieldKeys[varOrdinal];
|
|
1936
|
+
varOrdinal += 1;
|
|
1937
|
+
const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
|
|
1938
|
+
renderArray.push(
|
|
1939
|
+
<div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
|
|
1940
|
+
<TextArea
|
|
1941
|
+
id={`${elem}_${index}`}
|
|
1942
|
+
placeholder={`enter the value for ${elem}`}
|
|
1943
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1944
|
+
onChange={(e) => carouselTextAreaValueChange(e, cardIndex, fieldType)}
|
|
1945
|
+
value={computedValue}
|
|
1946
|
+
onFocus={(e) => {
|
|
1947
|
+
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1948
|
+
if (fieldType === RCS_TAG_AREA_FIELD_DESC) {
|
|
1949
|
+
setCarouselDescFocusedVarId(id);
|
|
1950
|
+
} else {
|
|
1951
|
+
setCarouselTitleFocusedVarId(id);
|
|
1952
|
+
}
|
|
1953
|
+
}}
|
|
1954
|
+
/>
|
|
1955
|
+
</div>
|
|
1956
|
+
);
|
|
1957
|
+
} else if (elem) {
|
|
1958
|
+
renderArray.push(
|
|
1959
|
+
<CapHeading
|
|
1960
|
+
key={`static_${index}`}
|
|
1961
|
+
type="h4"
|
|
1962
|
+
className="rcs-edit-template-message-split"
|
|
1963
|
+
>
|
|
1964
|
+
{elem}
|
|
1965
|
+
</CapHeading>
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
const textAreaValueChange = (e) => {
|
|
1029
1974
|
const value = e?.target?.value ?? '';
|
|
1030
1975
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1031
1976
|
if (!id) return;
|
|
@@ -1036,16 +1981,43 @@ const splitTemplateVarString = (str) => {
|
|
|
1036
1981
|
const variableName = getVarNameFromToken(token);
|
|
1037
1982
|
|
|
1038
1983
|
if (variableName) {
|
|
1984
|
+
const nextVal = isInvalidValue ? "" : value;
|
|
1039
1985
|
setCardVarMapped((prev) => ({
|
|
1040
1986
|
...prev,
|
|
1041
|
-
[variableName]:
|
|
1987
|
+
[variableName]: nextVal,
|
|
1042
1988
|
}));
|
|
1043
1989
|
}
|
|
1044
1990
|
};
|
|
1045
1991
|
|
|
1046
|
-
|
|
1992
|
+
// Carousel: typing directly into a slot's value box (as opposed to picking a tag) — same global
|
|
1993
|
+
// positional key as onCarouselTagSelect/renderCarouselEditMessage, so both ways of filling a
|
|
1994
|
+
// slot land on the identical key.
|
|
1995
|
+
const carouselTextAreaValueChange = (e, cardIndex, fieldType) => {
|
|
1996
|
+
const value = e?.target?.value ?? '';
|
|
1047
1997
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1048
1998
|
if (!id) return;
|
|
1999
|
+
const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
|
|
2000
|
+
const fieldTemplateStr = (isDescField
|
|
2001
|
+
? carouselData?.[cardIndex]?.description
|
|
2002
|
+
: carouselData?.[cardIndex]?.title) || '';
|
|
2003
|
+
const localVarOrdinal = getCarouselFieldLocalVarOrdinal(id, fieldTemplateStr);
|
|
2004
|
+
if (localVarOrdinal == null) return;
|
|
2005
|
+
const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
|
|
2006
|
+
const mapKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
|
|
2007
|
+
if (!mapKey) return;
|
|
2008
|
+
const isInvalidValue = value?.trim() === "";
|
|
2009
|
+
const nextVal = isInvalidValue ? "" : value;
|
|
2010
|
+
setCardVarMapped((prev) => ({
|
|
2011
|
+
...prev,
|
|
2012
|
+
[mapKey]: nextVal,
|
|
2013
|
+
}));
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
const setTextAreaId = (e, type) => {
|
|
2017
|
+
// VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
|
|
2018
|
+
// have an `.target.id` shape. Support both.
|
|
2019
|
+
const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
|
|
2020
|
+
if (!id) return;
|
|
1049
2021
|
if (type === TITLE_TEXT) setTitleTextAreaId(id);
|
|
1050
2022
|
else setDescTextAreaId(id);
|
|
1051
2023
|
};
|
|
@@ -1063,10 +2035,12 @@ const splitTemplateVarString = (str) => {
|
|
|
1063
2035
|
</CapRow>
|
|
1064
2036
|
}
|
|
1065
2037
|
description={
|
|
1066
|
-
|
|
2038
|
+
!isEditFlow && (
|
|
2039
|
+
<CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
|
|
2040
|
+
)
|
|
1067
2041
|
}
|
|
1068
2042
|
/>
|
|
1069
|
-
|
|
2043
|
+
|
|
1070
2044
|
<CapActionButton
|
|
1071
2045
|
buttonType={buttonType}
|
|
1072
2046
|
updateButtonChange={updateButtonChange}
|
|
@@ -1074,45 +2048,86 @@ const splitTemplateVarString = (str) => {
|
|
|
1074
2048
|
suggestions={suggestions}
|
|
1075
2049
|
isEditFlow={isEditFlow}
|
|
1076
2050
|
isFullMode={isFullMode}
|
|
1077
|
-
maxButtons={MAX_BUTTONS}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
2051
|
+
maxButtons={MAX_BUTTONS}
|
|
2052
|
+
host={hostName}
|
|
2053
|
+
/>
|
|
2054
|
+
</>
|
|
2055
|
+
);
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
const getRcsValueMap = (fieldTemplateString, fieldType) => {
|
|
2059
|
+
if (!fieldTemplateString) return {};
|
|
2060
|
+
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
2061
|
+
const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
|
|
2062
|
+
const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
|
|
2063
|
+
const segmentIdToResolvedValue = {};
|
|
2064
|
+
let varOrdinal = 0;
|
|
2065
|
+
templateSegments.forEach((segmentToken, segmentIndexInField) => {
|
|
2066
|
+
if (rcsVarTestRegex.test(segmentToken)) {
|
|
2067
|
+
const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
|
|
2068
|
+
const varName = getVarNameFromToken(segmentToken);
|
|
2069
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
2070
|
+
varOrdinal += 1;
|
|
2071
|
+
segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
|
|
2072
|
+
cardVarMapped,
|
|
2073
|
+
varName,
|
|
2074
|
+
globalSlot,
|
|
2075
|
+
isEditLike,
|
|
2076
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
return segmentIdToResolvedValue;
|
|
2081
|
+
};
|
|
2082
|
+
|
|
2083
|
+
const titleVarSegmentValueMapById = useMemo(
|
|
2084
|
+
() => getRcsValueMap(templateTitle, TITLE_TEXT),
|
|
2085
|
+
[templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
2086
|
+
);
|
|
2087
|
+
const descriptionVarSegmentValueMapById = useMemo(
|
|
2088
|
+
() => getRcsValueMap(templateDesc, MESSAGE_TEXT),
|
|
2089
|
+
[templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
2090
|
+
);
|
|
2091
|
+
|
|
2092
|
+
const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
|
|
2093
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
2094
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
2095
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
2096
|
+
const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
2097
|
+
if (variableName === undefined || variableName === null || variableName === '') return;
|
|
2098
|
+
const isInvalidValue = value?.trim() === '';
|
|
2099
|
+
const coercedSlotValue = isInvalidValue ? '' : value;
|
|
2100
|
+
const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
2101
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
2102
|
+
varSegmentCompositeDomId,
|
|
2103
|
+
templateStringForField,
|
|
2104
|
+
type,
|
|
2105
|
+
);
|
|
2106
|
+
const fieldOffset = type === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
2107
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
2108
|
+
const updatedCardVarMapped = { ...previousCardVarMapped };
|
|
2109
|
+
// Remove stale semantic key: keeping it causes every other slot sharing the same
|
|
2110
|
+
// variable name (e.g. {{adv}} in both title and description) to read the same value
|
|
2111
|
+
// via the semantic-key fallback in resolveCardVarMappedSlotValue.
|
|
2112
|
+
delete updatedCardVarMapped[variableName];
|
|
2113
|
+
if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
|
|
2114
|
+
updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
|
|
2115
|
+
// Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
|
|
2116
|
+
const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
|
|
2117
|
+
let varOrdinal = 0;
|
|
2118
|
+
for (let i = 0; i < fieldSegments.length; i++) {
|
|
2119
|
+
if (rcsVarTestRegex.test(fieldSegments[i])) {
|
|
2120
|
+
const slotVarName = getVarNameFromToken(fieldSegments[i]);
|
|
2121
|
+
const globalSlot = fieldOffset + varOrdinal;
|
|
2122
|
+
varOrdinal += 1;
|
|
2123
|
+
if (slotVarName === variableName && globalSlot !== globalVarSlotIndexZeroBased) {
|
|
2124
|
+
updatedCardVarMapped[String(globalSlot + 1)] = coercedSlotValue;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
1112
2127
|
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
|
|
2128
|
+
}
|
|
2129
|
+
return updatedCardVarMapped;
|
|
2130
|
+
});
|
|
1116
2131
|
};
|
|
1117
2132
|
|
|
1118
2133
|
const renderTextComponent = () => {
|
|
@@ -1132,6 +2147,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1132
2147
|
}
|
|
1133
2148
|
suffix={
|
|
1134
2149
|
<>
|
|
2150
|
+
|
|
1135
2151
|
{(isEditFlow || !isFullMode) ? (
|
|
1136
2152
|
<TagList
|
|
1137
2153
|
label={formatMessage(globalMessages.addLabels)}
|
|
@@ -1148,18 +2164,28 @@ const splitTemplateVarString = (str) => {
|
|
|
1148
2164
|
type="flat"
|
|
1149
2165
|
isAddBtn
|
|
1150
2166
|
onClick={onTitleAddVar}
|
|
1151
|
-
disabled={
|
|
2167
|
+
disabled={templateTitleError}
|
|
1152
2168
|
>
|
|
1153
2169
|
{formatMessage(messages.addVar)}
|
|
1154
2170
|
</CapButton>
|
|
1155
|
-
|
|
2171
|
+
)}
|
|
1156
2172
|
</>
|
|
1157
|
-
}
|
|
2173
|
+
}
|
|
2174
|
+
/>
|
|
2175
|
+
|
|
2176
|
+
{(isEditFlow || !isFullMode) ? (
|
|
2177
|
+
<VarSegmentMessageEditor
|
|
2178
|
+
key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
2179
|
+
templateString={templateTitle}
|
|
2180
|
+
valueMap={titleVarSegmentValueMapById}
|
|
2181
|
+
onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
|
|
2182
|
+
onFocus={(id) => setTitleTextAreaId(id)}
|
|
2183
|
+
varRegex={rcsVarRegex}
|
|
2184
|
+
placeholderPrefix=""
|
|
2185
|
+
getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
|
|
1158
2186
|
/>
|
|
1159
|
-
<div className="rcs_text_area_wrapper">
|
|
1160
|
-
{(isEditFlow || !isFullMode) ? (
|
|
1161
|
-
renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
|
|
1162
2187
|
) : (
|
|
2188
|
+
<div className="rcs_text_area_wrapper">
|
|
1163
2189
|
<CapInput
|
|
1164
2190
|
className={`rcs-template-title-input ${
|
|
1165
2191
|
!isTemplateApproved ? "rcs-edit-disabled" : ""
|
|
@@ -1172,8 +2198,8 @@ const splitTemplateVarString = (str) => {
|
|
|
1172
2198
|
errorMessage={templateTitleError}
|
|
1173
2199
|
disabled={isEditFlow || !isFullMode}
|
|
1174
2200
|
/>
|
|
2201
|
+
</div>
|
|
1175
2202
|
)}
|
|
1176
|
-
</div>
|
|
1177
2203
|
{(isEditFlow || !isFullMode) && templateTitleError && (
|
|
1178
2204
|
<CapError className="rcs-template-title-error">
|
|
1179
2205
|
{templateTitleError}
|
|
@@ -1181,7 +2207,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1181
2207
|
)}
|
|
1182
2208
|
{!isEditFlow && isFullMode && renderTitleCharacterCount()}
|
|
1183
2209
|
</>
|
|
1184
|
-
|
|
2210
|
+
)}
|
|
1185
2211
|
|
|
1186
2212
|
{/* Template Message */}
|
|
1187
2213
|
<CapRow useLegacy id="rcs-template-message-label">
|
|
@@ -1219,9 +2245,21 @@ const splitTemplateVarString = (str) => {
|
|
|
1219
2245
|
/>
|
|
1220
2246
|
</CapRow>
|
|
1221
2247
|
<CapRow useLegacy className="rcs-create-template-message-input">
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
2248
|
+
{/* Edit/library: segmented inputs (split on {{…}}). Full-mode create: single TextArea below — manual entry there never hits segment split. TagList replaces {{n}} in template string here. */}
|
|
2249
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
2250
|
+
{(isEditFlow || !isFullMode)?
|
|
2251
|
+
(
|
|
2252
|
+
<VarSegmentMessageEditor
|
|
2253
|
+
key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
2254
|
+
templateString={templateDesc}
|
|
2255
|
+
valueMap={descriptionVarSegmentValueMapById}
|
|
2256
|
+
onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
|
|
2257
|
+
onFocus={(id) => setDescTextAreaId(id)}
|
|
2258
|
+
varRegex={rcsVarRegex}
|
|
2259
|
+
placeholderPrefix=""
|
|
2260
|
+
getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
|
|
2261
|
+
/>
|
|
2262
|
+
)
|
|
1225
2263
|
: (
|
|
1226
2264
|
<>
|
|
1227
2265
|
<TextArea
|
|
@@ -1259,13 +2297,15 @@ const splitTemplateVarString = (str) => {
|
|
|
1259
2297
|
</>
|
|
1260
2298
|
)
|
|
1261
2299
|
}
|
|
1262
|
-
</
|
|
2300
|
+
</CapRow>
|
|
1263
2301
|
{(isEditFlow || !isFullMode) && templateDescError && (
|
|
1264
2302
|
<CapError className="rcs-template-message-error">
|
|
1265
2303
|
{templateDescError}
|
|
1266
2304
|
</CapError>
|
|
1267
2305
|
)}
|
|
1268
|
-
{
|
|
2306
|
+
{(isEditFlow || !isFullMode)
|
|
2307
|
+
? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
|
|
2308
|
+
: (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
|
|
1269
2309
|
{!isFullMode && hasTag() && (
|
|
1270
2310
|
<CapAlert
|
|
1271
2311
|
message={
|
|
@@ -1279,24 +2319,13 @@ const splitTemplateVarString = (str) => {
|
|
|
1279
2319
|
/>
|
|
1280
2320
|
)}
|
|
1281
2321
|
</CapRow>
|
|
1282
|
-
{
|
|
2322
|
+
{((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
|
|
2323
|
+
renderButtonComponent()}
|
|
1283
2324
|
</>
|
|
1284
2325
|
|
|
1285
2326
|
);
|
|
1286
2327
|
};
|
|
1287
2328
|
|
|
1288
|
-
|
|
1289
|
-
const fallbackSmsLength = () => (
|
|
1290
|
-
<CapLabel type="label1" className="fallback-sms-length">
|
|
1291
|
-
{formatMessage(messages.totalCharacters, {
|
|
1292
|
-
smsCount: Math.ceil(
|
|
1293
|
-
fallbackMessage?.length / FALLBACK_MESSAGE_MAX_LENGTH,
|
|
1294
|
-
),
|
|
1295
|
-
number: fallbackMessage?.length,
|
|
1296
|
-
})}
|
|
1297
|
-
</CapLabel>
|
|
1298
|
-
);
|
|
1299
|
-
|
|
1300
2329
|
// Get character count for title (rich card only)
|
|
1301
2330
|
const getTitleCharacterCount = () => {
|
|
1302
2331
|
if (templateType === contentType.text_message) return 0;
|
|
@@ -1311,7 +2340,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1311
2340
|
// Get max length for description based on template type
|
|
1312
2341
|
const getDescriptionMaxLength = () => {
|
|
1313
2342
|
return templateType === contentType.text_message
|
|
1314
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
|
|
2343
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
|
|
1315
2344
|
: RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
|
|
1316
2345
|
};
|
|
1317
2346
|
|
|
@@ -1374,287 +2403,34 @@ const splitTemplateVarString = (str) => {
|
|
|
1374
2403
|
return false;
|
|
1375
2404
|
};
|
|
1376
2405
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
base: data?.value?.base || {},
|
|
1402
|
-
},
|
|
1403
|
-
};
|
|
1404
|
-
const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
|
|
1405
|
-
'',
|
|
1406
|
-
);
|
|
1407
|
-
closeDltContainerHandler();
|
|
1408
|
-
setDltEditData(tempData);
|
|
1409
|
-
setFallbackMessage(fallMsg);
|
|
1410
|
-
setFallbackMessageError(fallMsg?.length > FALLBACK_MESSAGE_MAX_LENGTH);
|
|
1411
|
-
setShowDltCard(true);
|
|
1412
|
-
};
|
|
1413
|
-
|
|
1414
|
-
const rcsDltCardDeleteHandler = () => {
|
|
1415
|
-
closeDltContainerHandler();
|
|
1416
|
-
setDltEditData({});
|
|
1417
|
-
setFallbackMessage('');
|
|
1418
|
-
setFallbackMessageError(false);
|
|
1419
|
-
setShowDltCard(false);
|
|
1420
|
-
};
|
|
1421
|
-
|
|
1422
|
-
const dltFallbackListingPreviewhandler = (data) => {
|
|
1423
|
-
const {
|
|
1424
|
-
'updated-sms-editor': updatedSmsEditor = [],
|
|
1425
|
-
'sms-editor': smsEditor = '',
|
|
1426
|
-
} = data.versions.base || {};
|
|
1427
|
-
setFallbackPreviewmode(true);
|
|
1428
|
-
setDltPreviewData(
|
|
1429
|
-
updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
|
|
1430
|
-
);
|
|
1431
|
-
};
|
|
1432
|
-
|
|
1433
|
-
const getDltContentCardList = (content, channel) => {
|
|
1434
|
-
const extra = [
|
|
1435
|
-
<CapIcon
|
|
1436
|
-
type="edit"
|
|
1437
|
-
style={{ marginRight: '8px' }}
|
|
1438
|
-
onClick={() => rcsDltEditSelectHandler(dltEditData)}
|
|
1439
|
-
/>,
|
|
1440
|
-
<CapDropdown
|
|
1441
|
-
overlay={(
|
|
1442
|
-
<CapMenu>
|
|
1443
|
-
<>
|
|
1444
|
-
<CapMenu.Item
|
|
1445
|
-
className="ant-dropdown-menu-item"
|
|
1446
|
-
onClick={() => setFallbackPreviewmode(true)}
|
|
1447
|
-
>
|
|
1448
|
-
{formatMessage(globalMessages.preview)}
|
|
1449
|
-
</CapMenu.Item>
|
|
1450
|
-
<CapMenu.Item
|
|
1451
|
-
className="ant-dropdown-menu-item"
|
|
1452
|
-
onClick={rcsDltCardDeleteHandler}
|
|
1453
|
-
>
|
|
1454
|
-
{formatMessage(globalMessages.delete)}
|
|
1455
|
-
</CapMenu.Item>
|
|
1456
|
-
</>
|
|
1457
|
-
</CapMenu>
|
|
1458
|
-
)}
|
|
1459
|
-
>
|
|
1460
|
-
<CapIcon type="more" />
|
|
1461
|
-
</CapDropdown>,
|
|
1462
|
-
];
|
|
1463
|
-
return {
|
|
1464
|
-
title: channel,
|
|
1465
|
-
content,
|
|
1466
|
-
cardType: channel,
|
|
1467
|
-
extra,
|
|
1468
|
-
};
|
|
1469
|
-
};
|
|
1470
|
-
|
|
1471
|
-
const getDltSlideBoxContent = () => {
|
|
1472
|
-
const loc = {
|
|
1473
|
-
pathname: `/sms/edit`,
|
|
1474
|
-
query: { type: EMBEDDED, module: LIBRARY },
|
|
1475
|
-
search: '',
|
|
1476
|
-
};
|
|
1477
|
-
const route = { name: SMS.toLowerCase() };
|
|
1478
|
-
let dltHeader = '';
|
|
1479
|
-
let dltContent = '';
|
|
1480
|
-
if (dltMode === RCS_DLT_MODE.TEMPLATES) {
|
|
1481
|
-
dltHeader = (
|
|
1482
|
-
<CapHeader
|
|
1483
|
-
title={formatMessage(messages.rcsDltTitle)}
|
|
1484
|
-
description={formatMessage(messages.rcsDltDesc)}
|
|
1485
|
-
/>
|
|
1486
|
-
);
|
|
1487
|
-
dltContent = (
|
|
1488
|
-
<Templates
|
|
1489
|
-
location={loc}
|
|
1490
|
-
route={route}
|
|
1491
|
-
smsRegister={smsRegister}
|
|
1492
|
-
isFullMode={isFullMode}
|
|
1493
|
-
isDltFromRcs
|
|
1494
|
-
onSelectTemplate={rcsDltEditSelectHandler}
|
|
1495
|
-
handlePeviewTemplate={dltFallbackListingPreviewhandler}
|
|
1496
|
-
/>
|
|
1497
|
-
);
|
|
1498
|
-
} else if (dltMode === RCS_DLT_MODE.EDIT) {
|
|
1499
|
-
dltHeader = formatMessage(messages.dltEditHeader);
|
|
1500
|
-
dltContent = (
|
|
1501
|
-
<SmsTraiEdit
|
|
1502
|
-
location={loc}
|
|
1503
|
-
route={route}
|
|
1504
|
-
getDefaultTags="outbound"
|
|
1505
|
-
handleClose={closeDltContainerHandler}
|
|
1506
|
-
templateData={dltEditData}
|
|
1507
|
-
getFormSubscriptionData={rcsDltEditCompletionHandler}
|
|
1508
|
-
isFullMode={false}
|
|
1509
|
-
selectedOfferDetails={selectedOfferDetails}
|
|
1510
|
-
/>
|
|
1511
|
-
);
|
|
1512
|
-
}
|
|
1513
|
-
return { dltHeader, dltContent };
|
|
1514
|
-
};
|
|
1515
|
-
|
|
1516
|
-
const renderFallBackSmsComponent = () => {
|
|
1517
|
-
// Completely disable fallback functionality when DLT is disabled
|
|
1518
|
-
return null;
|
|
1519
|
-
|
|
1520
|
-
// const contentArr = [];
|
|
1521
|
-
// const showAddCreativeBtnForDlt = isDltEnabled && !showDltCard;
|
|
1522
|
-
// const showCardForDlt = isDltEnabled && showDltCard;
|
|
1523
|
-
// const showNonDltFallbackComp = !showAddCreativeBtnForDlt && !showCardForDlt;
|
|
1524
|
-
// //pushing common fallback sms headings
|
|
1525
|
-
// contentArr.push(
|
|
1526
|
-
// <CapRow
|
|
1527
|
-
// style={{
|
|
1528
|
-
// marginBottom: isDltEnabled ? '20px' : '10px',
|
|
1529
|
-
// }}
|
|
1530
|
-
// >
|
|
1531
|
-
// <CapHeader
|
|
1532
|
-
// title={(
|
|
1533
|
-
// <CapRow type="flex">
|
|
1534
|
-
// <CapHeading type="h4">
|
|
1535
|
-
// {formatMessage(messages.fallbackLabel)}
|
|
1536
|
-
// </CapHeading>
|
|
1537
|
-
// <CapTooltipWithInfo
|
|
1538
|
-
// placement="right"
|
|
1539
|
-
// infoIconProps={{
|
|
1540
|
-
// style: { marginLeft: '4px', marginTop: '3px' },
|
|
1541
|
-
// }}
|
|
1542
|
-
// title={formatMessage(messages.fallbackToolTip)}
|
|
1543
|
-
// />
|
|
1544
|
-
// </CapRow>
|
|
1545
|
-
// )}
|
|
1546
|
-
// description={formatMessage(messages.fallbackDesc)}
|
|
1547
|
-
// suffix={
|
|
1548
|
-
// isDltEnabled ? null : (
|
|
1549
|
-
// <CapButton
|
|
1550
|
-
// type="flat"
|
|
1551
|
-
// className="fallback-preview-btn"
|
|
1552
|
-
// prefix={<CapIcon type="eye" />}
|
|
1553
|
-
// style={{ color: CAP_SECONDARY.base }}
|
|
1554
|
-
// onClick={() => setFallbackPreviewmode(true)}
|
|
1555
|
-
// disabled={fallbackMessage === '' || fallbackMessageError}
|
|
1556
|
-
// >
|
|
1557
|
-
// {formatMessage(globalMessages.preview)}
|
|
1558
|
-
// </CapButton>
|
|
1559
|
-
// )
|
|
1560
|
-
// }
|
|
1561
|
-
// />
|
|
1562
|
-
// </CapRow>,
|
|
1563
|
-
// );
|
|
1564
|
-
|
|
1565
|
-
//dlt is enabled, and dlt content is not yet added, show button to add dlt creative
|
|
1566
|
-
// showAddCreativeBtnForDlt
|
|
1567
|
-
// && contentArr.push(
|
|
1568
|
-
// <CapCard className="rcs-dlt-fallback-card">
|
|
1569
|
-
// <CapRow type="flex" justify="center" align="middle">
|
|
1570
|
-
// <CapColumn span={10}>
|
|
1571
|
-
// <CapImage src={addCreativesIcon} />
|
|
1572
|
-
// </CapColumn>
|
|
1573
|
-
// <CapColumn span={14}>
|
|
1574
|
-
// <CapButton
|
|
1575
|
-
// className="add-dlt-btn"
|
|
1576
|
-
// type="secondary"
|
|
1577
|
-
// onClick={addDltMsgHandler}
|
|
1578
|
-
// >
|
|
1579
|
-
// {formatMessage(messages.addSmsCreative)}
|
|
1580
|
-
// </CapButton>
|
|
1581
|
-
// </CapColumn>
|
|
1582
|
-
// </CapRow>
|
|
1583
|
-
// </CapCard>,
|
|
1584
|
-
// );
|
|
1585
|
-
|
|
1586
|
-
// //dlt is enabled and dlt content is added, show it in a card
|
|
1587
|
-
// showCardForDlt
|
|
1588
|
-
// && contentArr.push(
|
|
1589
|
-
// <CapCustomCardList
|
|
1590
|
-
// cardList={[getDltContentCardList(fallbackMessage, SMS)]}
|
|
1591
|
-
// className="rcs-dlt-card"
|
|
1592
|
-
// />,
|
|
1593
|
-
// fallbackMessageError && (
|
|
1594
|
-
// <CapError className="rcs-fallback-len-error">
|
|
1595
|
-
// {formatMessage(messages.fallbackMsgLenError)}
|
|
1596
|
-
// </CapError>
|
|
1597
|
-
// ),
|
|
1598
|
-
// );
|
|
1599
|
-
|
|
1600
|
-
// //dlt is not enabled, show non dlt text area
|
|
1601
|
-
// showNonDltFallbackComp
|
|
1602
|
-
// && contentArr.push(
|
|
1603
|
-
// <>
|
|
1604
|
-
// <CapRow>
|
|
1605
|
-
// <CapHeader
|
|
1606
|
-
// title={(
|
|
1607
|
-
// <CapHeading type="h4">
|
|
1608
|
-
// {formatMessage(messages.fallbackTextAreaLabel)}
|
|
1609
|
-
// </CapHeading>
|
|
1610
|
-
// )}
|
|
1611
|
-
// suffix={(
|
|
1612
|
-
// <TagList
|
|
1613
|
-
// label={formatMessage(globalMessages.addLabels)}
|
|
1614
|
-
// onTagSelect={onTagSelectFallback}
|
|
1615
|
-
// location={location}
|
|
1616
|
-
// tags={tags || []}
|
|
1617
|
-
// onContextChange={handleOnTagsContextChange}
|
|
1618
|
-
// injectedTags={injectedTags || {}}
|
|
1619
|
-
// selectedOfferDetails={selectedOfferDetails}
|
|
1620
|
-
// />
|
|
1621
|
-
// )}
|
|
1622
|
-
// />
|
|
1623
|
-
// </CapRow>
|
|
1624
|
-
// <div className="rcs_fallback_msg_textarea_wrapper">
|
|
1625
|
-
// <TextArea
|
|
1626
|
-
// id="rcs_fallback_message_textarea"
|
|
1627
|
-
// autosize={{ minRows: 3, maxRows: 5 }}
|
|
1628
|
-
// placeholder={formatMessage(messages.fallbackMsgPlaceholder)}
|
|
1629
|
-
// onChange={onFallbackMessageChange}
|
|
1630
|
-
// errorMessage={fallbackMessageError}
|
|
1631
|
-
// value={fallbackMessage || ""}
|
|
1632
|
-
// />
|
|
1633
|
-
// {!aiContentBotDisabled && (
|
|
1634
|
-
// <CapAskAira.ContentGenerationBot
|
|
1635
|
-
// text={fallbackMessage || ""}
|
|
1636
|
-
// setText={(text) => {
|
|
1637
|
-
// onFallbackMessageChange({ target: { value: text } });
|
|
1638
|
-
// }}
|
|
1639
|
-
// iconPlacement="float-br"
|
|
1640
|
-
// rootStyle={{
|
|
1641
|
-
// bottom: "0.5rem",
|
|
1642
|
-
// right: "0.5rem",
|
|
1643
|
-
// position: "absolute",
|
|
1644
|
-
// }}
|
|
1645
|
-
// />
|
|
1646
|
-
// )}
|
|
1647
|
-
// </div>
|
|
1648
|
-
// <CapRow>{fallbackSmsLength()}</CapRow>
|
|
1649
|
-
// </>
|
|
1650
|
-
// );
|
|
1651
|
-
|
|
1652
|
-
// return <>{contentArr}</>;
|
|
1653
|
-
};
|
|
2406
|
+
const renderFallBackSmsComponent = () => (
|
|
2407
|
+
<SmsFallback
|
|
2408
|
+
value={smsFallbackData}
|
|
2409
|
+
onChange={setSmsFallbackData}
|
|
2410
|
+
parentLocation={location}
|
|
2411
|
+
smsRegister={smsRegister}
|
|
2412
|
+
isFullMode={isFullMode}
|
|
2413
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
2414
|
+
channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
|
|
2415
|
+
sectionTitle={
|
|
2416
|
+
smsFallbackData
|
|
2417
|
+
? formatMessage(messages.fallbackLabel)
|
|
2418
|
+
: formatMessage(messages.smsFallbackOptional)
|
|
2419
|
+
}
|
|
2420
|
+
templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
|
|
2421
|
+
templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
|
|
2422
|
+
/* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
|
|
2423
|
+
showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
|
|
2424
|
+
disableSelectTemplate={isEditFlow}
|
|
2425
|
+
eventContextTags={eventContextTags}
|
|
2426
|
+
onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
|
|
2427
|
+
isRcsEditFlow={isEditFlow}
|
|
2428
|
+
/>
|
|
2429
|
+
);
|
|
1654
2430
|
|
|
1655
2431
|
const uploadRcsImage = useCallback((file, type, fileParams, index) => {
|
|
1656
2432
|
setImageError(null);
|
|
1657
|
-
const isRcsThumbnail = index
|
|
2433
|
+
const isRcsThumbnail = isThumbnailAssetIndex(index);
|
|
1658
2434
|
actions.uploadRcsAsset(file, type, {
|
|
1659
2435
|
isRcsThumbnail,
|
|
1660
2436
|
...fileParams,
|
|
@@ -1692,21 +2468,15 @@ const splitTemplateVarString = (str) => {
|
|
|
1692
2468
|
const updateOnRcsImageReUpload = useCallback(() => {
|
|
1693
2469
|
setUpdateRcsImageSrc('');
|
|
1694
2470
|
}, []);
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
const uploadRcsVideo = (file, type, fileParams) => {
|
|
1698
|
-
setImageError(null);
|
|
2471
|
+
const uploadRcsVideo = (file, type, fileParams, index) => {
|
|
1699
2472
|
actions.uploadRcsAsset(file, type, {
|
|
1700
2473
|
...fileParams,
|
|
1701
2474
|
type: 'video',
|
|
1702
2475
|
channel: RCS,
|
|
1703
2476
|
isGenerateVideoThumbnail: false,
|
|
1704
|
-
});
|
|
2477
|
+
}, index);
|
|
1705
2478
|
};
|
|
1706
2479
|
|
|
1707
|
-
const updateRcsVideoSrc = (val) => {
|
|
1708
|
-
setRcsVideoSrc(val);
|
|
1709
|
-
};
|
|
1710
2480
|
const setUpdateRcsVideoSrc = useCallback((index, val) => {
|
|
1711
2481
|
setRcsVideoSrc(val);
|
|
1712
2482
|
setAssetList(val);
|
|
@@ -1729,13 +2499,13 @@ const splitTemplateVarString = (str) => {
|
|
|
1729
2499
|
updateRcsThumbnailSrc('');
|
|
1730
2500
|
};
|
|
1731
2501
|
|
|
1732
|
-
|
|
2502
|
+
const renderThumbnailComponent = () => {
|
|
1733
2503
|
const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
1734
2504
|
return !isEditFlow && (
|
|
1735
2505
|
<>
|
|
1736
2506
|
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
|
|
1737
2507
|
<CapImageUpload
|
|
1738
|
-
|
|
2508
|
+
className="rcs-image-upload-with-top-padding"
|
|
1739
2509
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1740
2510
|
imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
|
|
1741
2511
|
imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
|
|
@@ -1753,6 +2523,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1753
2523
|
channel={RCS}
|
|
1754
2524
|
channelSpecificStyle={!isFullMode}
|
|
1755
2525
|
skipDimensionValidation={true}
|
|
2526
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
1756
2527
|
/>
|
|
1757
2528
|
</>
|
|
1758
2529
|
)
|
|
@@ -1778,7 +2549,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1778
2549
|
value: dim.type,
|
|
1779
2550
|
label: `${dim.label}`
|
|
1780
2551
|
}))}
|
|
1781
|
-
|
|
2552
|
+
className="rcs-dimension-select--bottom-spacing"
|
|
1782
2553
|
/>
|
|
1783
2554
|
</>
|
|
1784
2555
|
)}
|
|
@@ -1792,7 +2563,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1792
2563
|
</div>
|
|
1793
2564
|
) : (
|
|
1794
2565
|
<CapImageUpload
|
|
1795
|
-
|
|
2566
|
+
className="rcs-image-upload-with-top-padding"
|
|
1796
2567
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1797
2568
|
imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
|
|
1798
2569
|
imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
|
|
@@ -1803,7 +2574,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1803
2574
|
updateImageSrc={setUpdateRcsImageSrc}
|
|
1804
2575
|
updateOnReUpload={updateOnRcsImageReUpload}
|
|
1805
2576
|
index={0}
|
|
1806
|
-
className="cap-custom-image-upload"
|
|
2577
|
+
className="cap-custom-image-upload rcs-image-upload--top-spacing"
|
|
1807
2578
|
key={`rcs-uploaded-image-${selectedDimension}`}
|
|
1808
2579
|
imageData={rcsData}
|
|
1809
2580
|
channel={RCS}
|
|
@@ -1814,7 +2585,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1814
2585
|
|
|
1815
2586
|
</>
|
|
1816
2587
|
);
|
|
1817
|
-
|
|
2588
|
+
}
|
|
1818
2589
|
|
|
1819
2590
|
const renderVideoComponent = () => {
|
|
1820
2591
|
const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
@@ -1833,7 +2604,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1833
2604
|
value: dim.type,
|
|
1834
2605
|
label: `${dim.label}`
|
|
1835
2606
|
}))}
|
|
1836
|
-
|
|
2607
|
+
className="rcs-dimension-select--bottom-spacing"
|
|
1837
2608
|
/>
|
|
1838
2609
|
)}
|
|
1839
2610
|
{(isEditFlow || !isFullMode) ? (
|
|
@@ -1891,10 +2662,48 @@ const splitTemplateVarString = (str) => {
|
|
|
1891
2662
|
};
|
|
1892
2663
|
|
|
1893
2664
|
const getRcsPreview = () => {
|
|
2665
|
+
|
|
2666
|
+
if (templateType === contentType.carousel) {
|
|
2667
|
+
const cardsForPreview = buildCarouselCardsForPreview(carouselData);
|
|
2668
|
+
const carouselDimKey = getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth);
|
|
2669
|
+
const carouselImgDims =
|
|
2670
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
2671
|
+
const carouselVidDims =
|
|
2672
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
2673
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
2674
|
+
// Debug log for embedded/library mode preview payload (carousel)
|
|
2675
|
+
// eslint-disable-next-line no-console
|
|
2676
|
+
return (
|
|
2677
|
+
<UnifiedPreview
|
|
2678
|
+
channel={RCS}
|
|
2679
|
+
content={{
|
|
2680
|
+
carouselData: cardsForPreview,
|
|
2681
|
+
carouselPreviewDimensions: {
|
|
2682
|
+
imageWidth: carouselImgDims.width,
|
|
2683
|
+
imageHeight: carouselImgDims.height,
|
|
2684
|
+
videoThumbWidth: carouselVidDims.width,
|
|
2685
|
+
videoThumbHeight: carouselVidDims.height,
|
|
2686
|
+
},
|
|
2687
|
+
}}
|
|
2688
|
+
device={ANDROID}
|
|
2689
|
+
showDeviceToggle={false}
|
|
2690
|
+
showHeader={false}
|
|
2691
|
+
formatMessage={formatMessage}
|
|
2692
|
+
/>
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
1894
2695
|
|
|
1895
2696
|
const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
|
|
1896
|
-
const
|
|
1897
|
-
const
|
|
2697
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
2698
|
+
const titleVarCountForResolve = isMediaTypeText
|
|
2699
|
+
? 0
|
|
2700
|
+
: ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
|
|
2701
|
+
const resolvedTitle = isMediaTypeText
|
|
2702
|
+
? ''
|
|
2703
|
+
: (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
|
|
2704
|
+
const resolvedDesc = isSlotMappingMode
|
|
2705
|
+
? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
|
|
2706
|
+
: templateDesc;
|
|
1898
2707
|
return (
|
|
1899
2708
|
<UnifiedPreview
|
|
1900
2709
|
channel={RCS}
|
|
@@ -1917,51 +2726,39 @@ const splitTemplateVarString = (str) => {
|
|
|
1917
2726
|
);
|
|
1918
2727
|
};
|
|
1919
2728
|
|
|
1920
|
-
const getUnmappedDesc = (str, mapping) => {
|
|
1921
|
-
if (!str) return '';
|
|
1922
|
-
if (!mapping || Object.keys(mapping).length === 0) return str;
|
|
1923
|
-
let result = str;
|
|
1924
|
-
const replacements = [];
|
|
1925
|
-
Object.entries(mapping).forEach(([key, value]) => {
|
|
1926
|
-
const raw = (value ?? '').toString();
|
|
1927
|
-
if (!raw || raw?.trim?.() === '') return;
|
|
1928
|
-
const braced = /^\{\{[\s\S]*\}\}$/.test(raw) ? raw : `{{${raw}}}`;
|
|
1929
|
-
replacements.push({ key, needle: raw });
|
|
1930
|
-
if (braced !== raw) replacements.push({ key, needle: braced });
|
|
1931
|
-
});
|
|
1932
|
-
const seen = new Set();
|
|
1933
|
-
const uniq = replacements
|
|
1934
|
-
.filter(({ key, needle }) => {
|
|
1935
|
-
const id = `${key}::${needle}`;
|
|
1936
|
-
if (seen.has(id)) return false;
|
|
1937
|
-
seen.add(id);
|
|
1938
|
-
return true;
|
|
1939
|
-
})
|
|
1940
|
-
.sort((a, b) => (b.needle.length - a.needle.length));
|
|
1941
|
-
|
|
1942
|
-
uniq.forEach(({ key, needle }) => {
|
|
1943
|
-
if (!needle) return;
|
|
1944
|
-
const escaped = needle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
1945
|
-
const regex = new RegExp(escaped, 'g');
|
|
1946
|
-
result = result.replace(regex, `{{${key}}}`);
|
|
1947
|
-
});
|
|
1948
|
-
return result;
|
|
1949
|
-
};
|
|
1950
|
-
|
|
1951
2729
|
const createPayload = () => {
|
|
1952
|
-
const
|
|
1953
|
-
const {
|
|
1954
|
-
template_id: templateId = '',
|
|
1955
|
-
template_name = '',
|
|
1956
|
-
'sms-editor': template = '',
|
|
1957
|
-
header: registeredSenderIds = [],
|
|
1958
|
-
} = base;
|
|
1959
|
-
const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
|
|
1960
|
-
const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
|
|
2730
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
1961
2731
|
const alignment = isMediaTypeImage
|
|
1962
2732
|
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
|
|
1963
2733
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
|
|
1964
2734
|
|
|
2735
|
+
const heightTypeForCardWidth = isMediaTypeText
|
|
2736
|
+
? undefined
|
|
2737
|
+
: isMediaTypeImage
|
|
2738
|
+
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
|
|
2739
|
+
: isMediaTypeVideo
|
|
2740
|
+
? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
|
|
2741
|
+
: undefined;
|
|
2742
|
+
const cardWidthFromSelection =
|
|
2743
|
+
heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
|
|
2744
|
+
|
|
2745
|
+
/** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
|
|
2746
|
+
const smsFallbackPayloadContent = buildSmsFallBackContentForPayload({
|
|
2747
|
+
smsFallbackData,
|
|
2748
|
+
templateData,
|
|
2749
|
+
isFullMode,
|
|
2750
|
+
smsRegister,
|
|
2751
|
+
});
|
|
2752
|
+
|
|
2753
|
+
const carouselCardContent = isCarouselType
|
|
2754
|
+
? buildCarouselCardContentForPayload(carouselData, {
|
|
2755
|
+
isSlotMappingMode,
|
|
2756
|
+
cardVarMapped,
|
|
2757
|
+
selectedCarouselHeight,
|
|
2758
|
+
rcsVarRegex,
|
|
2759
|
+
})
|
|
2760
|
+
: null;
|
|
2761
|
+
|
|
1965
2762
|
const payload = {
|
|
1966
2763
|
name: templateName,
|
|
1967
2764
|
versions: {
|
|
@@ -1970,16 +2767,22 @@ const splitTemplateVarString = (str) => {
|
|
|
1970
2767
|
RCS: {
|
|
1971
2768
|
rcsContent: {
|
|
1972
2769
|
...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
|
|
1973
|
-
cardType: STANDALONE,
|
|
1974
|
-
cardSettings:
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2770
|
+
cardType: isCarouselType ? contentType.carousel : STANDALONE,
|
|
2771
|
+
cardSettings: isCarouselType
|
|
2772
|
+
? { cardOrientation: VERTICAL, cardWidth: selectedCarouselWidth || SMALL }
|
|
2773
|
+
: {
|
|
2774
|
+
cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
|
|
2775
|
+
...(alignment && { mediaAlignment: alignment }),
|
|
2776
|
+
cardWidth: SMALL,
|
|
2777
|
+
},
|
|
2778
|
+
cardContent: isCarouselType
|
|
2779
|
+
? carouselCardContent
|
|
2780
|
+
: [
|
|
1980
2781
|
{
|
|
1981
|
-
|
|
1982
|
-
|
|
2782
|
+
// Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
|
|
2783
|
+
// / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
|
|
2784
|
+
title: templateTitle,
|
|
2785
|
+
description: templateDesc,
|
|
1983
2786
|
mediaType: templateMediaType,
|
|
1984
2787
|
...(!isMediaTypeText && {media: {
|
|
1985
2788
|
mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
|
|
@@ -1988,23 +2791,32 @@ const splitTemplateVarString = (str) => {
|
|
|
1988
2791
|
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
|
|
1989
2792
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
|
|
1990
2793
|
}}),
|
|
1991
|
-
...(
|
|
1992
|
-
const
|
|
1993
|
-
...(templateTitle
|
|
1994
|
-
...(templateDesc
|
|
2794
|
+
...(isSlotMappingMode && (() => {
|
|
2795
|
+
const templateVarTokens = [
|
|
2796
|
+
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
2797
|
+
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
1995
2798
|
];
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2799
|
+
const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
|
|
2800
|
+
cardVarMapped,
|
|
2801
|
+
);
|
|
2802
|
+
// Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
|
|
2803
|
+
// semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
|
|
2804
|
+
// and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
|
|
2805
|
+
const persistedSlotVarMap = {};
|
|
2806
|
+
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
2807
|
+
const varName = getVarNameFromToken(token);
|
|
2808
|
+
if (!varName) return;
|
|
2809
|
+
const resolvedRawValue = resolveCardVarMappedSlotValue(
|
|
2810
|
+
cardVarMappedForRcsCardOnly,
|
|
2811
|
+
varName,
|
|
2812
|
+
slotIndexZeroBased,
|
|
2813
|
+
isSlotMappingMode,
|
|
2814
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
2815
|
+
);
|
|
2816
|
+
const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
|
|
2817
|
+
persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
|
|
2006
2818
|
});
|
|
2007
|
-
return { cardVarMapped:
|
|
2819
|
+
return { cardVarMapped: persistedSlotVarMap };
|
|
2008
2820
|
})()),
|
|
2009
2821
|
...(suggestions.length > 0 && { suggestions }),
|
|
2010
2822
|
}
|
|
@@ -2012,17 +2824,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2012
2824
|
contentType: isFullMode ? templateType : RICHCARD,
|
|
2013
2825
|
...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
|
|
2014
2826
|
},
|
|
2015
|
-
|
|
2016
|
-
message: fallbackMessage,
|
|
2017
|
-
...(isDltEnabled && {
|
|
2018
|
-
templateConfigs: {
|
|
2019
|
-
templateId,
|
|
2020
|
-
templateName: template_name,
|
|
2021
|
-
template,
|
|
2022
|
-
registeredSenderIds,
|
|
2023
|
-
},
|
|
2024
|
-
}),
|
|
2025
|
-
},
|
|
2827
|
+
...smsFallbackPayloadContent,
|
|
2026
2828
|
},
|
|
2027
2829
|
},
|
|
2028
2830
|
},
|
|
@@ -2032,6 +2834,126 @@ const splitTemplateVarString = (str) => {
|
|
|
2032
2834
|
return payload;
|
|
2033
2835
|
};
|
|
2034
2836
|
|
|
2837
|
+
/** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
|
|
2838
|
+
const testPreviewFormData = useMemo(() => {
|
|
2839
|
+
const payload = createPayload();
|
|
2840
|
+
const rcs = payload?.versions?.base?.content?.RCS;
|
|
2841
|
+
if (!rcs) return null;
|
|
2842
|
+
// createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
|
|
2843
|
+
const accountIdForCreateMessageMeta =
|
|
2844
|
+
(wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
|
|
2845
|
+
? String(wecrmAccountId)
|
|
2846
|
+
: accountId;
|
|
2847
|
+
const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
|
|
2848
|
+
let rcsForTest = {
|
|
2849
|
+
...rcs,
|
|
2850
|
+
rcsContent: {
|
|
2851
|
+
...rcs.rcsContent,
|
|
2852
|
+
...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
|
|
2853
|
+
},
|
|
2854
|
+
};
|
|
2855
|
+
/** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
|
|
2856
|
+
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
2857
|
+
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
2858
|
+
if (isCarouselType) {
|
|
2859
|
+
// Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
|
|
2860
|
+
// user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
2861
|
+
// - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
|
|
2862
|
+
// - prepareTagExtractionPayload can extract tag metadata from the card content.
|
|
2863
|
+
// Each variable slot's key comes from computeCarouselVarSlotKeys, so two cards' variables
|
|
2864
|
+
// never share a key even when they pick the same tag name for their own slot.
|
|
2865
|
+
rcsForTest = {
|
|
2866
|
+
...rcsForTest,
|
|
2867
|
+
rcsContent: {
|
|
2868
|
+
...rcsForTest.rcsContent,
|
|
2869
|
+
cardContent: cardContent.map((card) => {
|
|
2870
|
+
const rawTitle = card.title || '';
|
|
2871
|
+
const rawDesc = card.description || '';
|
|
2872
|
+
const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex, false);
|
|
2873
|
+
const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex, true);
|
|
2874
|
+
return { ...card, title: resolvedTitle, description: resolvedDesc };
|
|
2875
|
+
}),
|
|
2876
|
+
},
|
|
2877
|
+
};
|
|
2878
|
+
} else if (isSlotMappingModeForPreview) {
|
|
2879
|
+
// Standalone card: coalesce cardVarMapped with the template's slot names so the preview
|
|
2880
|
+
// API receives a correctly-keyed var map for non-carousel templates.
|
|
2881
|
+
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
2882
|
+
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
2883
|
+
templateTitle,
|
|
2884
|
+
templateDesc,
|
|
2885
|
+
rcsVarRegex,
|
|
2886
|
+
);
|
|
2887
|
+
rcsForTest = {
|
|
2888
|
+
...rcsForTest,
|
|
2889
|
+
rcsContent: {
|
|
2890
|
+
...rcsForTest.rcsContent,
|
|
2891
|
+
cardContent: [
|
|
2892
|
+
{ ...cardContent[0], cardVarMapped: fullCardVarMapped },
|
|
2893
|
+
...cardContent.slice(1),
|
|
2894
|
+
],
|
|
2895
|
+
},
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
const out = {
|
|
2900
|
+
versions: {
|
|
2901
|
+
base: {
|
|
2902
|
+
content: {
|
|
2903
|
+
RCS: rcsForTest,
|
|
2904
|
+
},
|
|
2905
|
+
},
|
|
2906
|
+
},
|
|
2907
|
+
};
|
|
2908
|
+
// Use the smsFallbackTemplateConfigs already built by createPayload() / buildSmsFallBackContentForPayload —
|
|
2909
|
+
// it merges the API baseline (templateId, registeredSenderIds, templateName, etc.) with local
|
|
2910
|
+
// smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced smsTemplateId
|
|
2911
|
+
// in campaign mode. This also keeps templateName (and all other fields) in sync with the approval
|
|
2912
|
+
// payload automatically.
|
|
2913
|
+
const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
|
|
2914
|
+
if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
|
|
2915
|
+
out.templateConfigs = {
|
|
2916
|
+
...smsFallbackTcFromPayload,
|
|
2917
|
+
traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
return out;
|
|
2921
|
+
}, [
|
|
2922
|
+
templateName,
|
|
2923
|
+
templateTitle,
|
|
2924
|
+
templateDesc,
|
|
2925
|
+
templateMediaType,
|
|
2926
|
+
cardVarMapped,
|
|
2927
|
+
carouselData,
|
|
2928
|
+
suggestions,
|
|
2929
|
+
rcsImageSrc,
|
|
2930
|
+
rcsVideoSrc,
|
|
2931
|
+
rcsThumbnailSrc,
|
|
2932
|
+
selectedDimension,
|
|
2933
|
+
smsFallbackData,
|
|
2934
|
+
isFullMode,
|
|
2935
|
+
isEditFlow,
|
|
2936
|
+
templateType,
|
|
2937
|
+
accountId,
|
|
2938
|
+
wecrmAccountId,
|
|
2939
|
+
accessToken,
|
|
2940
|
+
accountName,
|
|
2941
|
+
hostName,
|
|
2942
|
+
smsRegister,
|
|
2943
|
+
]);
|
|
2944
|
+
|
|
2945
|
+
/**
|
|
2946
|
+
* Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
|
|
2947
|
+
* with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
|
|
2948
|
+
* miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
|
|
2949
|
+
*/
|
|
2950
|
+
const librarySmsFallbackMergedForValidation = useMemo(
|
|
2951
|
+
() => mergeSmsFallbackForLibrary(templateData, smsFallbackData, isFullMode),
|
|
2952
|
+
[isFullMode, templateData, smsFallbackData],
|
|
2953
|
+
);
|
|
2954
|
+
|
|
2955
|
+
|
|
2956
|
+
|
|
2035
2957
|
const actionCallback = ({ errorMessage, resp }, isEdit) => {
|
|
2036
2958
|
// eslint-disable-next-line no-undef
|
|
2037
2959
|
const error = errorMessage?.message || errorMessage;
|
|
@@ -2061,6 +2983,9 @@ const splitTemplateVarString = (str) => {
|
|
|
2061
2983
|
_id: params?.id,
|
|
2062
2984
|
validity: true,
|
|
2063
2985
|
type: RCS,
|
|
2986
|
+
// CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
|
|
2987
|
+
// the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
|
|
2988
|
+
closeSlideBoxAfterSubmit: !isFullMode,
|
|
2064
2989
|
};
|
|
2065
2990
|
getFormData(formDataParams);
|
|
2066
2991
|
};
|
|
@@ -2074,6 +2999,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2074
2999
|
actionCallback({ resp, errorMessage });
|
|
2075
3000
|
setSpin(false); // Always turn off spinner
|
|
2076
3001
|
if (!errorMessage) {
|
|
3002
|
+
setTemplateStatus(RCS_STATUSES.pending);
|
|
2077
3003
|
onCreateComplete();
|
|
2078
3004
|
}
|
|
2079
3005
|
});
|
|
@@ -2085,6 +3011,72 @@ const splitTemplateVarString = (str) => {
|
|
|
2085
3011
|
}
|
|
2086
3012
|
};
|
|
2087
3013
|
|
|
3014
|
+
const smsFallbackBlocksDone = () => checkSmsFallbackBlocksDone({
|
|
3015
|
+
isFullMode,
|
|
3016
|
+
smsRegister,
|
|
3017
|
+
smsFallbackData,
|
|
3018
|
+
templateData,
|
|
3019
|
+
smsFallbackMergedForValidation: librarySmsFallbackMergedForValidation,
|
|
3020
|
+
});
|
|
3021
|
+
|
|
3022
|
+
/**
|
|
3023
|
+
* Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
|
|
3024
|
+
* semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
|
|
3025
|
+
* / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
|
|
3026
|
+
*/
|
|
3027
|
+
const isLibraryCampaignCardVarMappingIncomplete = () => {
|
|
3028
|
+
if (isFullMode) return false;
|
|
3029
|
+
const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
|
|
3030
|
+
rcsVarTestRegex.test(elem),
|
|
3031
|
+
);
|
|
3032
|
+
const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
|
|
3033
|
+
rcsVarTestRegex.test(elem),
|
|
3034
|
+
);
|
|
3035
|
+
const orderedVarNames = [
|
|
3036
|
+
...titleTokens.map((token) => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
|
|
3037
|
+
...descTokens.map((token) => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
|
|
3038
|
+
];
|
|
3039
|
+
if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
|
|
3040
|
+
return true;
|
|
3041
|
+
}
|
|
3042
|
+
return orderedVarNames.some((name, globalIdx) => {
|
|
3043
|
+
const slotValue = resolveCardVarMappedSlotValue(
|
|
3044
|
+
cardVarMapped,
|
|
3045
|
+
name,
|
|
3046
|
+
globalIdx,
|
|
3047
|
+
true,
|
|
3048
|
+
rcsSpanningSemanticVarNames.has(name),
|
|
3049
|
+
);
|
|
3050
|
+
const stringValue = slotValue == null ? '' : String(slotValue);
|
|
3051
|
+
return stringValue.trim() === '';
|
|
3052
|
+
});
|
|
3053
|
+
};
|
|
3054
|
+
|
|
3055
|
+
const isCarouselLibraryIncomplete = () => {
|
|
3056
|
+
if (!isCarouselType || isFullMode) return false;
|
|
3057
|
+
if ((carouselErrors || []).some((err) => err?.title || err?.description)) return true;
|
|
3058
|
+
// Block if any card's title or description has no content at all (no static text and no variable tokens).
|
|
3059
|
+
const hasEmptyField = (carouselData || []).some((card) =>
|
|
3060
|
+
['title', 'description'].some((field) => !(card?.[field] || '').trim())
|
|
3061
|
+
);
|
|
3062
|
+
if (hasEmptyField) return true;
|
|
3063
|
+
const slotKeysByCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex);
|
|
3064
|
+
const unfilledVar = (carouselData || []).some((card, cardIndex) =>
|
|
3065
|
+
['title', 'description'].some((field) => {
|
|
3066
|
+
const isDescField = field === 'description';
|
|
3067
|
+
const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
|
|
3068
|
+
if (tokens.length === 0) return false;
|
|
3069
|
+
const fieldKeys = (isDescField ? slotKeysByCard[cardIndex]?.description : slotKeysByCard[cardIndex]?.title) ?? [];
|
|
3070
|
+
return tokens.some((token, localVarOrdinal) => {
|
|
3071
|
+
const slotKey = fieldKeys[localVarOrdinal];
|
|
3072
|
+
const slotValue = cardVarMapped?.[slotKey];
|
|
3073
|
+
return slotValue == null || String(slotValue).trim() === '';
|
|
3074
|
+
});
|
|
3075
|
+
})
|
|
3076
|
+
);
|
|
3077
|
+
return unfilledVar;
|
|
3078
|
+
};
|
|
3079
|
+
|
|
2088
3080
|
const isDisableDone = () => {
|
|
2089
3081
|
if(isEditFlow){
|
|
2090
3082
|
return false;
|
|
@@ -2095,46 +3087,26 @@ const splitTemplateVarString = (str) => {
|
|
|
2095
3087
|
}
|
|
2096
3088
|
}
|
|
2097
3089
|
|
|
2098
|
-
if(
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
|
|
2102
|
-
|
|
2103
|
-
if (allVars.length > 0 && isEmpty(cardVarMapped)) {
|
|
2104
|
-
return true;
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
const hasEmptyMapping =
|
|
2108
|
-
cardVarMapped &&
|
|
2109
|
-
Object.keys(cardVarMapped).length > 0 &&
|
|
2110
|
-
Object.entries(cardVarMapped).some(([_, v]) => {
|
|
2111
|
-
if (typeof v !== 'string') return !v; // null/undefined
|
|
2112
|
-
return v.trim() === ''; // empty string
|
|
2113
|
-
});
|
|
2114
|
-
|
|
2115
|
-
if (hasEmptyMapping) {
|
|
2116
|
-
return true;
|
|
2117
|
-
}
|
|
3090
|
+
if (isCarouselLibraryIncomplete()) {
|
|
3091
|
+
return true;
|
|
3092
|
+
}
|
|
2118
3093
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
if (typeof v !== 'string') return !v;
|
|
2122
|
-
return v.trim() === '';
|
|
2123
|
-
});
|
|
2124
|
-
if (anyMissing) {
|
|
2125
|
-
return true;
|
|
2126
|
-
}
|
|
3094
|
+
if (isLibraryCampaignCardVarMappingIncomplete()) {
|
|
3095
|
+
return true;
|
|
2127
3096
|
}
|
|
2128
3097
|
|
|
2129
|
-
|
|
3098
|
+
if (smsFallbackBlocksDone()) {
|
|
2130
3099
|
return true;
|
|
3100
|
+
}
|
|
2131
3101
|
|
|
3102
|
+
if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
|
|
3103
|
+
return true;
|
|
2132
3104
|
}
|
|
2133
|
-
if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
3105
|
+
if (!isCarouselType && isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2134
3106
|
return true;
|
|
2135
3107
|
}
|
|
2136
3108
|
|
|
2137
|
-
if (isMediaTypeVideo && (rcsVideoSrc.videoSrc
|
|
3109
|
+
if (!isCarouselType && isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2138
3110
|
return true;
|
|
2139
3111
|
}
|
|
2140
3112
|
if (buttonType.includes(CTA)) {
|
|
@@ -2146,72 +3118,54 @@ const splitTemplateVarString = (str) => {
|
|
|
2146
3118
|
return true;
|
|
2147
3119
|
}
|
|
2148
3120
|
}
|
|
2149
|
-
if (templateDescError || templateTitleError
|
|
3121
|
+
if (templateDescError || templateTitleError) {
|
|
3122
|
+
return true;
|
|
3123
|
+
}
|
|
3124
|
+
if (isSmsFallbackContentTooLong(smsFallbackData, FALLBACK_MESSAGE_MAX_LENGTH)) {
|
|
2150
3125
|
return true;
|
|
2151
3126
|
}
|
|
2152
3127
|
return false;
|
|
2153
3128
|
};
|
|
2154
3129
|
|
|
2155
3130
|
const isEditDisableDone = () => {
|
|
2156
|
-
|
|
2157
|
-
if (templateStatus !== RCS_STATUSES.approved) {
|
|
3131
|
+
if (isFullMode && !isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
|
|
2158
3132
|
return true;
|
|
2159
3133
|
}
|
|
2160
3134
|
|
|
2161
|
-
if (
|
|
2162
|
-
|
|
2163
|
-
return true;
|
|
2164
|
-
}
|
|
3135
|
+
if (isCarouselLibraryIncomplete()) {
|
|
3136
|
+
return true;
|
|
2165
3137
|
}
|
|
2166
|
-
if(!isFullMode){
|
|
2167
|
-
const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
|
|
2168
|
-
const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
|
|
2169
|
-
const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
|
|
2170
|
-
|
|
2171
|
-
if (allVars.length > 0 && isEmpty(cardVarMapped)) {
|
|
2172
|
-
return true;
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
const hasEmptyMapping =
|
|
2176
|
-
cardVarMapped &&
|
|
2177
|
-
Object.keys(cardVarMapped).length > 0 &&
|
|
2178
|
-
Object.entries(cardVarMapped).some(([_, v]) => {
|
|
2179
|
-
if (typeof v !== 'string') return !v; // null/undefined
|
|
2180
|
-
return v.trim() === ''; // empty string
|
|
2181
|
-
});
|
|
2182
3138
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
3139
|
+
if (isLibraryCampaignCardVarMappingIncomplete()) {
|
|
3140
|
+
return true;
|
|
3141
|
+
}
|
|
2186
3142
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
if (typeof v !== 'string') return !v;
|
|
2190
|
-
return v.trim() === '';
|
|
2191
|
-
});
|
|
2192
|
-
if (anyMissing) {
|
|
2193
|
-
return true;
|
|
2194
|
-
}
|
|
3143
|
+
if (smsFallbackBlocksDone()) {
|
|
3144
|
+
return true;
|
|
2195
3145
|
}
|
|
2196
|
-
|
|
3146
|
+
|
|
3147
|
+
if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
|
|
2197
3148
|
return true;
|
|
2198
3149
|
}
|
|
2199
|
-
if (isMediaTypeImage && rcsImageSrc === '') {
|
|
3150
|
+
if (!isCarouselType && isMediaTypeImage && rcsImageSrc === '') {
|
|
2200
3151
|
return true;
|
|
2201
3152
|
}
|
|
2202
|
-
if(isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
|
|
3153
|
+
if (!isCarouselType && isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
|
|
2203
3154
|
return true;
|
|
2204
3155
|
}
|
|
2205
3156
|
|
|
2206
3157
|
if (buttonType.includes(CTA)) {
|
|
2207
|
-
const hasValidButtons = suggestions.every(suggestion =>
|
|
3158
|
+
const hasValidButtons = suggestions.every(suggestion =>
|
|
2208
3159
|
suggestion.text && suggestion.url && !suggestionError && !forbiddenCharactersValidation(suggestion.text)
|
|
2209
3160
|
);
|
|
2210
3161
|
if (!hasValidButtons) {
|
|
2211
3162
|
return true;
|
|
2212
3163
|
}
|
|
2213
3164
|
}
|
|
2214
|
-
if (templateTitleError || templateDescError
|
|
3165
|
+
if (templateTitleError || templateDescError) {
|
|
3166
|
+
return true;
|
|
3167
|
+
}
|
|
3168
|
+
if (isSmsFallbackContentTooLong(smsFallbackData, FALLBACK_MESSAGE_MAX_LENGTH)) {
|
|
2215
3169
|
return true;
|
|
2216
3170
|
}
|
|
2217
3171
|
return false;
|
|
@@ -2261,85 +3215,108 @@ const splitTemplateVarString = (str) => {
|
|
|
2261
3215
|
};
|
|
2262
3216
|
|
|
2263
3217
|
const getMainContent = () => {
|
|
2264
|
-
if (showDltContainer && !fallbackPreviewmode) {
|
|
2265
|
-
const dltSlideBoxContent = showDltContainer && getDltSlideBoxContent();
|
|
2266
|
-
const { dltHeader = '', dltContent = '' } = dltSlideBoxContent;
|
|
2267
|
-
return (
|
|
2268
|
-
<CapSlideBox
|
|
2269
|
-
show={showDltContainer}
|
|
2270
|
-
header={dltHeader}
|
|
2271
|
-
content={dltContent}
|
|
2272
|
-
handleClose={closeDltContainerHandler}
|
|
2273
|
-
size="size-xl"
|
|
2274
|
-
/>
|
|
2275
|
-
);
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
3218
|
return (
|
|
2279
3219
|
<>
|
|
2280
|
-
{templateStatus !== '' && (
|
|
2281
|
-
<
|
|
2282
|
-
{
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
3220
|
+
{templateStatus !== '' && (
|
|
3221
|
+
<CapRow useLegacy className="template-status-container">
|
|
3222
|
+
<CapColumn span={14}>
|
|
3223
|
+
<CapLabel type="label2">
|
|
3224
|
+
{formatMessage(messages.templateStatusLabel)}
|
|
3225
|
+
</CapLabel>
|
|
3226
|
+
|
|
3227
|
+
{!isHostInfoBip && templateStatus && (
|
|
3228
|
+
<CapAlert
|
|
3229
|
+
message={getTemplateStatusMessage()}
|
|
3230
|
+
type={getTemplateStatusType(templateStatus)}
|
|
3231
|
+
/>
|
|
3232
|
+
)}
|
|
3233
|
+
</CapColumn>
|
|
3234
|
+
</CapRow>
|
|
2292
3235
|
)}
|
|
2293
|
-
<CapRow className=
|
|
3236
|
+
<CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
|
|
2294
3237
|
<CapColumn span={14}>
|
|
2295
3238
|
{/* template name */}
|
|
2296
3239
|
{isFullMode && (
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
3240
|
+
isEditFlow ? (
|
|
3241
|
+
<div className="rcs-creative-name-readonly">
|
|
3242
|
+
<CapHeading type="h4">
|
|
3243
|
+
{formatMessage(globalMessages.creativeNameLabel)}
|
|
3244
|
+
</CapHeading>
|
|
3245
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
3246
|
+
{templateName || '-'}
|
|
3247
|
+
</CapHeading>
|
|
3248
|
+
</div>
|
|
3249
|
+
) : (
|
|
3250
|
+
<CapInput
|
|
3251
|
+
id="rcs_template_name_input"
|
|
3252
|
+
data-testid="template_name"
|
|
3253
|
+
onChange={onTemplateNameChange}
|
|
3254
|
+
errorMessage={templateNameError}
|
|
3255
|
+
placeholder={formatMessage(
|
|
3256
|
+
globalMessages.templateNamePlaceholder,
|
|
3257
|
+
)}
|
|
3258
|
+
value={templateName || ''}
|
|
3259
|
+
size="default"
|
|
3260
|
+
label={formatMessage(globalMessages.creativeNameLabel)}
|
|
3261
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
3262
|
+
/>
|
|
3263
|
+
)
|
|
3264
|
+
)}
|
|
3265
|
+
{isEditFlow ? (
|
|
3266
|
+
<div className="rcs-creative-name-readonly">
|
|
3267
|
+
<CapHeading type="h4">
|
|
3268
|
+
{formatMessage(messages.templateTypeLabel)}
|
|
3269
|
+
</CapHeading>
|
|
3270
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
3271
|
+
{TEMPLATE_TYPE_OPTIONS.find((option) => option.value === templateType)?.label || '-'}
|
|
3272
|
+
</CapHeading>
|
|
3273
|
+
</div>
|
|
3274
|
+
) : (
|
|
3275
|
+
<>
|
|
3276
|
+
{renderLabel('templateTypeLabel')}
|
|
3277
|
+
<ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
|
|
3278
|
+
<CapRadioGroup
|
|
3279
|
+
id="select-rcs-template-type"
|
|
3280
|
+
options={TEMPLATE_TYPE_OPTIONS}
|
|
3281
|
+
onChange={onTemplateTypeChange}
|
|
3282
|
+
value={templateType}
|
|
3283
|
+
disabled={!isFullMode}
|
|
3284
|
+
/>
|
|
3285
|
+
</ConfigProvider>
|
|
3286
|
+
</>
|
|
2310
3287
|
)}
|
|
2311
|
-
{renderLabel('templateTypeLabel')}
|
|
2312
|
-
<ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
|
|
2313
|
-
<CapRadioGroup
|
|
2314
|
-
id="select-rcs-template-type"
|
|
2315
|
-
options={TEMPLATE_TYPE_OPTIONS}
|
|
2316
|
-
onChange={onTemplateTypeChange}
|
|
2317
|
-
value={templateType}
|
|
2318
|
-
disabled={(isEditFlow || !isFullMode)}
|
|
2319
|
-
/>
|
|
2320
|
-
</ConfigProvider>
|
|
2321
3288
|
|
|
2322
|
-
{
|
|
2323
|
-
|
|
3289
|
+
{templateType === contentType.carousel ? (
|
|
3290
|
+
renderCarouselSection()
|
|
3291
|
+
) : (
|
|
2324
3292
|
<>
|
|
2325
|
-
{
|
|
2326
|
-
|
|
3293
|
+
{/* Show media only for rich_card */}
|
|
3294
|
+
{templateType === contentType.rich_card && (
|
|
3295
|
+
<>
|
|
3296
|
+
{renderLabel('mediaLabel')}
|
|
3297
|
+
<ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
|
|
2327
3298
|
<CapRadioGroup
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
3299
|
+
options={mediaRadioOptions || []}
|
|
3300
|
+
value={templateMediaType}
|
|
3301
|
+
onChange={onTemplateMediaTypeChange}
|
|
3302
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
3303
|
+
className="rcs-radio"
|
|
3304
|
+
/>
|
|
3305
|
+
</ConfigProvider>
|
|
2335
3306
|
<div className="rcs-container-image">
|
|
2336
|
-
|
|
2337
|
-
|
|
3307
|
+
{getMediaBasedComponent()}
|
|
3308
|
+
</div>
|
|
3309
|
+
</>
|
|
3310
|
+
)}
|
|
3311
|
+
{renderTextComponent()}
|
|
3312
|
+
</>
|
|
3313
|
+
)}
|
|
3314
|
+
{(!isEditFlow || hasMeaningfulSmsFallbackShape(smsFallbackData)) && (
|
|
3315
|
+
<>
|
|
3316
|
+
<CapDivider className="rcs-fallback-section-divider" />
|
|
3317
|
+
{renderFallBackSmsComponent()}
|
|
2338
3318
|
</>
|
|
2339
3319
|
)}
|
|
2340
|
-
{renderTextComponent()}
|
|
2341
|
-
<CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
|
|
2342
|
-
{renderFallBackSmsComponent()}
|
|
2343
3320
|
<div className="rcs-scroll-div" />
|
|
2344
3321
|
</CapColumn>
|
|
2345
3322
|
<CapColumn span={8} offset={2}>
|
|
@@ -2350,7 +3327,8 @@ const splitTemplateVarString = (str) => {
|
|
|
2350
3327
|
|
|
2351
3328
|
|
|
2352
3329
|
<div className="rcs-footer">
|
|
2353
|
-
{!
|
|
3330
|
+
{/* Full-mode create only: send-for-approval + disabled test/preview. Library mode uses Done below — onDoneCallback() is undefined when !isFullMode, so do not render this row (avoids a no-op primary button). */}
|
|
3331
|
+
{!isEditFlow && isFullMode && (
|
|
2354
3332
|
<>
|
|
2355
3333
|
<div className="button-disabled-tooltip-wrapper">
|
|
2356
3334
|
<CapButton
|
|
@@ -2358,7 +3336,9 @@ const splitTemplateVarString = (str) => {
|
|
|
2358
3336
|
disabled={isDisableDone()}
|
|
2359
3337
|
className="rcs-done-btn"
|
|
2360
3338
|
>
|
|
2361
|
-
<FormattedMessage
|
|
3339
|
+
<FormattedMessage
|
|
3340
|
+
{...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
|
|
3341
|
+
/>
|
|
2362
3342
|
</CapButton>
|
|
2363
3343
|
</div>
|
|
2364
3344
|
<CapTooltip
|
|
@@ -2371,7 +3351,6 @@ const splitTemplateVarString = (str) => {
|
|
|
2371
3351
|
className="rcs-test-preview-btn"
|
|
2372
3352
|
type="secondary"
|
|
2373
3353
|
disabled={true}
|
|
2374
|
-
style={{ marginLeft: "8px" }}
|
|
2375
3354
|
>
|
|
2376
3355
|
<FormattedMessage {...creativesMessages.testAndPreview} />
|
|
2377
3356
|
</CapButton>
|
|
@@ -2392,7 +3371,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2392
3371
|
</div>
|
|
2393
3372
|
</>
|
|
2394
3373
|
)}
|
|
2395
|
-
{isEditFlow && templateStatus === RCS_STATUSES.approved && (
|
|
3374
|
+
{isEditFlow && (isHostInfoBip || templateStatus === RCS_STATUSES.approved) && (
|
|
2396
3375
|
<>
|
|
2397
3376
|
<CapButton
|
|
2398
3377
|
onClick={handleTestAndPreview}
|
|
@@ -2404,51 +3383,6 @@ const splitTemplateVarString = (str) => {
|
|
|
2404
3383
|
</>
|
|
2405
3384
|
)}
|
|
2406
3385
|
</div>
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
{fallbackPreviewmode && (
|
|
2410
|
-
<CapSlideBox
|
|
2411
|
-
className="rcs-fallback-preview"
|
|
2412
|
-
show={fallbackPreviewmode}
|
|
2413
|
-
header={(
|
|
2414
|
-
<CapHeading type="h7" style={{ color: CAP_G01 }}>
|
|
2415
|
-
{formatMessage(messages.fallbackPreviewtitle)}
|
|
2416
|
-
</CapHeading>
|
|
2417
|
-
)}
|
|
2418
|
-
content={(
|
|
2419
|
-
<>
|
|
2420
|
-
<UnifiedPreview
|
|
2421
|
-
channel={RCS}
|
|
2422
|
-
content={{
|
|
2423
|
-
rcsPreviewContent: {
|
|
2424
|
-
rcsDesc: tempMsg,
|
|
2425
|
-
},
|
|
2426
|
-
}}
|
|
2427
|
-
device={ANDROID}
|
|
2428
|
-
showDeviceToggle={false}
|
|
2429
|
-
showHeader={false}
|
|
2430
|
-
formatMessage={formatMessage}
|
|
2431
|
-
/>
|
|
2432
|
-
<CapHeading
|
|
2433
|
-
type="h3"
|
|
2434
|
-
style={{ textAlign: 'center', width: '100%' }}
|
|
2435
|
-
className="margin-t-16"
|
|
2436
|
-
>
|
|
2437
|
-
{formatMessage(messages.totalCharacters, {
|
|
2438
|
-
smsCount: Math.ceil(
|
|
2439
|
-
tempMsg?.length / FALLBACK_MESSAGE_MAX_LENGTH,
|
|
2440
|
-
),
|
|
2441
|
-
number: tempMsg.length,
|
|
2442
|
-
})}
|
|
2443
|
-
</CapHeading>
|
|
2444
|
-
</>
|
|
2445
|
-
)}
|
|
2446
|
-
handleClose={() => {
|
|
2447
|
-
setFallbackPreviewmode(false);
|
|
2448
|
-
setDltPreviewData('');
|
|
2449
|
-
}}
|
|
2450
|
-
/>
|
|
2451
|
-
)}
|
|
2452
3386
|
</>
|
|
2453
3387
|
);
|
|
2454
3388
|
};
|
|
@@ -2457,29 +3391,49 @@ const splitTemplateVarString = (str) => {
|
|
|
2457
3391
|
<CapSpin spinning={loadingTags || spin}>
|
|
2458
3392
|
{getMainContent()}
|
|
2459
3393
|
</CapSpin>
|
|
3394
|
+
|
|
2460
3395
|
<TestAndPreviewSlidebox
|
|
2461
3396
|
show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
|
|
2462
3397
|
onClose={handleCloseTestAndPreview}
|
|
2463
|
-
formData={
|
|
2464
|
-
content={
|
|
3398
|
+
formData={testPreviewFormData}
|
|
3399
|
+
content={testAndPreviewContent}
|
|
2465
3400
|
currentChannel={RCS}
|
|
3401
|
+
orgUnitId={orgUnitId}
|
|
3402
|
+
rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
|
|
3403
|
+
smsFallbackContent={
|
|
3404
|
+
smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
|
|
3405
|
+
? {
|
|
3406
|
+
templateContent:
|
|
3407
|
+
smsFallbackData.templateContent || smsFallbackData.content || '',
|
|
3408
|
+
templateName: smsFallbackData.templateName || '',
|
|
3409
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
|
|
3410
|
+
? mergeRcsSmsFallbackVarMapLayers(
|
|
3411
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
3412
|
+
smsFallbackData,
|
|
3413
|
+
)
|
|
3414
|
+
: mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
|
|
3415
|
+
}
|
|
3416
|
+
: null
|
|
3417
|
+
}
|
|
3418
|
+
smsRegister={smsRegister}
|
|
2466
3419
|
/>
|
|
2467
3420
|
</>
|
|
2468
3421
|
);
|
|
2469
3422
|
};
|
|
2470
3423
|
|
|
3424
|
+
|
|
2471
3425
|
const mapStateToProps = createStructuredSelector({
|
|
2472
3426
|
rcsData: makeSelectRcs(),
|
|
2473
3427
|
accountData: makeSelectAccount(),
|
|
2474
3428
|
metaEntities: makeSelectMetaEntities(),
|
|
2475
3429
|
loadingTags: isLoadingMetaEntities(),
|
|
2476
3430
|
injectedTags: setInjectedTags(),
|
|
3431
|
+
currentOrgDetails: selectCurrentOrgDetails(),
|
|
2477
3432
|
});
|
|
2478
3433
|
|
|
2479
3434
|
const mapDispatchToProps = (dispatch) => ({
|
|
2480
3435
|
actions: bindActionCreators(RcsActions, dispatch),
|
|
2481
3436
|
globalActions: bindActionCreators(globalActions, dispatch),
|
|
2482
|
-
templatesActions: bindActionCreators(TemplatesActions, dispatch),
|
|
2483
3437
|
});
|
|
2484
3438
|
|
|
2485
3439
|
const withReducer = injectReducer({ key: 'rcs', reducer: v2RcsReducer });
|