@capillarytech/creatives-library 9.0.15-alpha.0 → 9.0.15-alpha.3
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 +19 -1
- package/utils/rcsPayloadUtils.js +92 -0
- package/utils/templateVarUtils.js +201 -0
- package/utils/tests/rcsPayloadUtils.test.js +226 -0
- package/utils/tests/templateVarUtils.test.js +204 -0
- package/v2Components/CapActionButton/constants.js +8 -1
- package/v2Components/CapActionButton/index.js +202 -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 +10 -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 +60 -11
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +202 -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 +38 -2
- package/v2Components/CommonTestAndPreview/index.js +761 -222
- 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 +284 -267
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
- 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 +34 -13
- 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/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 +422 -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/index.js +3 -3
- 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 +17 -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 +79 -0
- package/v2Containers/CreativesContainer/index.js +323 -104
- package/v2Containers/CreativesContainer/index.scss +83 -1
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
- package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +333 -90
- 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/MobilePush/Create/test/saga.test.js +2 -2
- package/v2Containers/Rcs/constants.js +131 -11
- package/v2Containers/Rcs/index.js +2685 -832
- package/v2Containers/Rcs/index.scss +443 -8
- package/v2Containers/Rcs/messages.js +45 -6
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +227 -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/index.test.js +151 -40
- package/v2Containers/Rcs/tests/mockData.js +38 -0
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
- package/v2Containers/Rcs/tests/utils.test.js +643 -30
- package/v2Containers/Rcs/utils.js +466 -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 +6 -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 +120 -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 +86 -23
- 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';
|
|
@@ -35,6 +34,14 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
|
|
|
35
34
|
import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
|
|
36
35
|
import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
|
|
37
36
|
import CapLink from '@capillarytech/cap-ui-library/CapLink';
|
|
37
|
+
import CapTab from '@capillarytech/cap-ui-library/CapTab';
|
|
38
|
+
import { flushSync } from 'react-dom';
|
|
39
|
+
import { isUrl, isValidText } from '../Line/Container/Wrapper/utils';
|
|
40
|
+
import {
|
|
41
|
+
invalidVarRegex,
|
|
42
|
+
RCS_CTA_URL_TYPE,
|
|
43
|
+
URL_MAX_LENGTH,
|
|
44
|
+
} from '../../v2Components/CapActionButton/constants';
|
|
38
45
|
|
|
39
46
|
import {
|
|
40
47
|
CAP_G01,
|
|
@@ -50,17 +57,30 @@ import {
|
|
|
50
57
|
import CapVideoUpload from '../../v2Components/CapVideoUpload';
|
|
51
58
|
import * as globalActions from '../Cap/actions';
|
|
52
59
|
import CapActionButton from '../../v2Components/CapActionButton';
|
|
60
|
+
import TemplatePreview from '../../v2Components/TemplatePreview';
|
|
53
61
|
import { makeSelectRcs, makeSelectAccount } from './selectors';
|
|
54
62
|
import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
|
|
55
63
|
import {
|
|
56
64
|
isLoadingMetaEntities,
|
|
57
65
|
makeSelectMetaEntities,
|
|
58
66
|
setInjectedTags,
|
|
67
|
+
selectCurrentOrgDetails,
|
|
59
68
|
} from '../Cap/selectors';
|
|
60
69
|
import * as RcsActions from './actions';
|
|
61
70
|
import { isAiContentBotDisabled } from '../../utils/common';
|
|
62
71
|
import * as TemplatesActions from '../Templates/actions';
|
|
63
72
|
import './index.scss';
|
|
73
|
+
import {
|
|
74
|
+
normalizeLibraryLoadedTitleDesc,
|
|
75
|
+
mergeRcsSmsFallBackContentFromDetails,
|
|
76
|
+
mergeRcsSmsFallbackVarMapLayers,
|
|
77
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord,
|
|
78
|
+
pickFirstSmsFallbackTemplateString,
|
|
79
|
+
syncCardVarMappedSemanticsFromSlots,
|
|
80
|
+
hasMeaningfulSmsFallbackShape,
|
|
81
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData,
|
|
82
|
+
pickRcsCardVarMappedEntries,
|
|
83
|
+
} from './rcsLibraryHydrationUtils';
|
|
64
84
|
import {
|
|
65
85
|
RCS,
|
|
66
86
|
SMS,
|
|
@@ -77,6 +97,7 @@ import {
|
|
|
77
97
|
RCS_IMG_SIZE,
|
|
78
98
|
RCS_DLT_MODE,
|
|
79
99
|
CTA,
|
|
100
|
+
AI_CONTENT_BOT_DISABLED,
|
|
80
101
|
RCS_STATUSES,
|
|
81
102
|
TITLE_TEXT,
|
|
82
103
|
MESSAGE_TEXT,
|
|
@@ -91,9 +112,15 @@ import {
|
|
|
91
112
|
rcsVarTestRegex,
|
|
92
113
|
RCS_IMAGE_DIMENSIONS,
|
|
93
114
|
RCS_TEXT_MESSAGE_MAX_LENGTH,
|
|
115
|
+
RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
|
|
94
116
|
RCS_RICH_CARD_MAX_LENGTH,
|
|
95
117
|
RCS_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
118
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS,
|
|
119
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
120
|
+
RCS_CAROUSEL_IMG_SIZE,
|
|
121
|
+
RCS_CAROUSEL_VIDEO_SIZE,
|
|
96
122
|
MAX_BUTTONS,
|
|
123
|
+
INITIAL_SUGGESTIONS,
|
|
97
124
|
INITIAL_SUGGESTIONS_DATA_STOP,
|
|
98
125
|
RCS_BUTTON_TYPES,
|
|
99
126
|
titletype,
|
|
@@ -103,25 +130,50 @@ import {
|
|
|
103
130
|
SMALL,
|
|
104
131
|
MEDIUM,
|
|
105
132
|
RICHCARD,
|
|
133
|
+
HOST_INFOBIP,
|
|
134
|
+
HOST_ICS,
|
|
135
|
+
CAROUSEL_HEIGHT_OPTIONS,
|
|
136
|
+
CAROUSEL_WIDTH_OPTIONS,
|
|
137
|
+
STOP,
|
|
138
|
+
RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
|
|
139
|
+
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
140
|
+
RCS_NUMERIC_VAR_TOKEN_REGEX,
|
|
141
|
+
RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
|
|
142
|
+
RCS_TAG_AREA_FIELD_TITLE,
|
|
143
|
+
RCS_TAG_AREA_FIELD_DESC,
|
|
144
|
+
REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
|
|
145
|
+
JS_TYPES,
|
|
106
146
|
} from './constants';
|
|
107
147
|
import globalMessages from '../Cap/messages';
|
|
108
148
|
import messages from './messages';
|
|
109
149
|
import creativesMessages from '../CreativesContainer/messages';
|
|
110
150
|
import withCreatives from '../../hoc/withCreatives';
|
|
111
151
|
import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
|
|
112
|
-
import
|
|
152
|
+
import VarSegmentMessageEditor from '../../v2Components/VarSegmentMessageEditor';
|
|
153
|
+
import { ANDROID, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
|
|
113
154
|
import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
|
|
155
|
+
import { splitTemplateVarString } from '../../utils/templateVarUtils';
|
|
114
156
|
import CapImageUpload from '../../v2Components/CapImageUpload';
|
|
115
|
-
import addCreativesIcon from '../Assets/images/addCreativesIllustration.svg';
|
|
116
157
|
import Templates from '../Templates';
|
|
117
158
|
import SmsTraiEdit from '../SmsTrai/Edit';
|
|
159
|
+
import SmsFallback from '../../v2Components/SmsFallback';
|
|
160
|
+
import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
|
|
118
161
|
import TagList from '../TagList';
|
|
119
162
|
import { validateTags } from '../../utils/tagValidations';
|
|
120
|
-
import {
|
|
163
|
+
import { isTraiDLTEnable } from '../../utils/common';
|
|
121
164
|
import { isTagIncluded } from '../../utils/commonUtils';
|
|
122
165
|
import injectReducer from '../../utils/injectReducer';
|
|
123
166
|
import v2RcsReducer from './reducer';
|
|
124
|
-
import {
|
|
167
|
+
import {
|
|
168
|
+
areAllRcsSmsFallbackVarSlotsFilled,
|
|
169
|
+
buildRcsNumericMustachePlaceholderRegex,
|
|
170
|
+
getTemplateStatusType,
|
|
171
|
+
normalizeCardVarMapped,
|
|
172
|
+
coalesceCardVarMappedToTemplate,
|
|
173
|
+
getRcsSemanticVarNamesSpanningTitleAndDesc,
|
|
174
|
+
resolveCardVarMappedSlotValue,
|
|
175
|
+
sanitizeCardVarMappedValue,
|
|
176
|
+
} from './utils';
|
|
125
177
|
|
|
126
178
|
|
|
127
179
|
const { Group: CapCheckboxGroup } = CapCheckbox;
|
|
@@ -138,28 +190,46 @@ export const Rcs = (props) => {
|
|
|
138
190
|
templatesActions,
|
|
139
191
|
globalActions,
|
|
140
192
|
location,
|
|
141
|
-
handleClose,
|
|
142
193
|
getDefaultTags,
|
|
143
194
|
supportedTags,
|
|
144
195
|
metaEntities,
|
|
145
196
|
injectedTags,
|
|
146
197
|
loadingTags,
|
|
147
198
|
getFormData,
|
|
148
|
-
isDltEnabled,
|
|
149
199
|
smsRegister,
|
|
200
|
+
orgUnitId,
|
|
150
201
|
selectedOfferDetails,
|
|
151
202
|
eventContextTags,
|
|
152
|
-
waitEventContextTags,
|
|
153
203
|
accountData = {},
|
|
204
|
+
currentOrgDetails,
|
|
154
205
|
// TestAndPreviewSlidebox props
|
|
155
206
|
showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
|
|
156
207
|
handleTestAndPreview: propsHandleTestAndPreview,
|
|
157
208
|
handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
|
|
209
|
+
handleClose,
|
|
158
210
|
} = props || {};
|
|
159
211
|
const { formatMessage } = intl;
|
|
160
212
|
const { TextArea } = CapInput;
|
|
161
213
|
const { CapCustomCardList } = CapCustomCard;
|
|
214
|
+
|
|
215
|
+
// Defensive: React cannot render plain objects as children (crashes with
|
|
216
|
+
// "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
|
|
217
|
+
// can accidentally set an error state to an object (e.g. `{}`).
|
|
218
|
+
const normalizeErrorMessage = (err) => {
|
|
219
|
+
if (!err) return '';
|
|
220
|
+
if (React.isValidElement(err)) return err;
|
|
221
|
+
if (typeof err === JS_TYPES.STRING) return err;
|
|
222
|
+
if (typeof err === JS_TYPES.NUMBER) return String(err);
|
|
223
|
+
if (typeof err === JS_TYPES.OBJECT && typeof err.message === JS_TYPES.STRING) return err.message;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.stringify(err);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
return String(err);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
162
231
|
const [isEditFlow, setEditFlow] = useState(false);
|
|
232
|
+
const isEditLike = isEditFlow || !isFullMode;
|
|
163
233
|
const [tags, updateTags] = useState([]);
|
|
164
234
|
const [spin, setSpin] = useState(false);
|
|
165
235
|
//template
|
|
@@ -168,33 +238,21 @@ export const Rcs = (props) => {
|
|
|
168
238
|
const [templateMediaType, setTemplateMediaType] = useState(
|
|
169
239
|
RCS_MEDIA_TYPES.NONE,
|
|
170
240
|
);
|
|
171
|
-
const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
|
|
172
241
|
const [templateTitle, setTemplateTitle] = useState('');
|
|
173
242
|
const [templateDesc, setTemplateDesc] = useState('');
|
|
174
243
|
const [templateDescError, setTemplateDescError] = useState(false);
|
|
175
244
|
const [templateStatus, setTemplateStatus] = useState('');
|
|
176
|
-
const [templateDate, setTemplateDate] = useState('');
|
|
177
|
-
//fallback
|
|
178
|
-
const [fallbackMessage, setFallbackMessage] = useState('');
|
|
179
|
-
const [fallbackMessageError, setFallbackMessageError] = useState(false);
|
|
180
245
|
//fallback dlt
|
|
181
246
|
const [showDltContainer, setShowDltContainer] = useState(false);
|
|
182
247
|
const [dltMode, setDltMode] = useState('');
|
|
183
248
|
const [dltEditData, setDltEditData] = useState({});
|
|
184
|
-
|
|
185
|
-
const [
|
|
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,58 @@ 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
|
+
const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
|
|
275
|
+
const [imageError, setImageError] = useState(null);
|
|
209
276
|
const [templateTitleError, setTemplateTitleError] = useState(false);
|
|
210
277
|
const [cardVarMapped, setCardVarMapped] = useState({});
|
|
278
|
+
/** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
|
|
279
|
+
const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
|
|
280
|
+
const lastHydratedRcsCardVarSignatureRef = useRef(null);
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
|
|
284
|
+
* without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
|
|
285
|
+
* fallback card / content stopped appearing until re-selected.
|
|
286
|
+
*/
|
|
287
|
+
const rcsHydrationDetails = useMemo(
|
|
288
|
+
() => (isFullMode ? rcsData?.templateDetails : templateData),
|
|
289
|
+
[isFullMode, rcsData?.templateDetails, templateData],
|
|
290
|
+
);
|
|
211
291
|
|
|
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;
|
|
292
|
+
/** 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'). */
|
|
293
|
+
const lastTagSchemaQueryKeyRef = useRef(null);
|
|
294
|
+
/**
|
|
295
|
+
* Library: parent often passes a new `templateData` object reference every render. Re-applying the same
|
|
296
|
+
* SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
|
|
297
|
+
*/
|
|
298
|
+
const lastSmsFallbackHydrationKeyRef = useRef(null);
|
|
299
|
+
|
|
300
|
+
const fetchTagSchemaIfNewQuery = useCallback(
|
|
301
|
+
(query) => {
|
|
302
|
+
const key = JSON.stringify(query);
|
|
303
|
+
if (lastTagSchemaQueryKeyRef.current === key) {
|
|
304
|
+
return;
|
|
243
305
|
}
|
|
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
|
-
};
|
|
306
|
+
lastTagSchemaQueryKeyRef.current = key;
|
|
307
|
+
globalActions.fetchSchemaForEntity(query);
|
|
308
|
+
},
|
|
309
|
+
[globalActions],
|
|
310
|
+
);
|
|
260
311
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
templateMediaType,
|
|
264
|
-
templateTitle,
|
|
265
|
-
templateDesc,
|
|
266
|
-
rcsImageSrc,
|
|
267
|
-
rcsVideoSrc,
|
|
268
|
-
rcsThumbnailSrc,
|
|
269
|
-
suggestions,
|
|
270
|
-
selectedDimension,
|
|
271
|
-
]);
|
|
312
|
+
// TestAndPreviewSlidebox state
|
|
313
|
+
const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
|
|
272
314
|
|
|
273
315
|
// Handle Test and Preview button click
|
|
274
316
|
const handleTestAndPreview = useCallback(() => {
|
|
275
317
|
setShowTestAndPreviewSlidebox(true);
|
|
276
|
-
setIsTestAndPreviewMode(true);
|
|
277
318
|
if (propsHandleTestAndPreview) {
|
|
278
319
|
propsHandleTestAndPreview();
|
|
279
320
|
}
|
|
@@ -297,31 +338,758 @@ export const Rcs = (props) => {
|
|
|
297
338
|
// For video
|
|
298
339
|
return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
|
|
299
340
|
};
|
|
341
|
+
/** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
|
|
342
|
+
const handleSmsFallbackEditorStateChange = useCallback((patch) => {
|
|
343
|
+
setSmsFallbackData((prev) => {
|
|
344
|
+
if (!patch || typeof patch !== 'object') return prev;
|
|
345
|
+
// Bail out when no template has been selected yet — SmsTraiEdit fires
|
|
346
|
+
// onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
|
|
347
|
+
// which would create a non-null smsFallbackData from nothing and cause
|
|
348
|
+
// the card to appear as "Untitled creative" before any save.
|
|
349
|
+
if (!prev) return prev;
|
|
350
|
+
return { ...prev, ...patch };
|
|
351
|
+
});
|
|
352
|
+
}, []);
|
|
300
353
|
|
|
354
|
+
/** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
|
|
301
355
|
const [accountId, setAccountId] = useState('');
|
|
356
|
+
/** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
|
|
357
|
+
const [wecrmAccountId, setWecrmAccountId] = useState('');
|
|
302
358
|
const [accessToken, setAccessToken] = useState('');
|
|
303
359
|
const [hostName, setHostName] = useState('');
|
|
304
360
|
const [accountName, setAccountName] = useState('');
|
|
361
|
+
const isHostInfoBip = hostName === HOST_INFOBIP;
|
|
362
|
+
const isHostIcs = hostName === HOST_ICS;
|
|
363
|
+
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
|
|
366
|
+
}, [isHostIcs]);
|
|
305
367
|
const [rcsAccount, setRcsAccount] = useState('');
|
|
306
368
|
useEffect(() => {
|
|
307
369
|
const accountObj = accountData.selectedRcsAccount || {};
|
|
308
370
|
if (!isEmpty(accountObj)) {
|
|
309
371
|
const {
|
|
372
|
+
id: wecrmId,
|
|
310
373
|
sourceAccountIdentifier = '',
|
|
311
374
|
configs = {},
|
|
312
375
|
} = accountObj;
|
|
313
|
-
|
|
314
376
|
setAccountId(sourceAccountIdentifier);
|
|
377
|
+
setWecrmAccountId(
|
|
378
|
+
wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
|
|
379
|
+
);
|
|
315
380
|
setAccessToken(configs.accessToken || '');
|
|
316
381
|
setHostName(accountObj.hostName || '');
|
|
317
382
|
setAccountName(accountObj.name || '');
|
|
318
383
|
setRcsAccount(accountObj.id || '');
|
|
384
|
+
} else {
|
|
385
|
+
setAccountId('');
|
|
386
|
+
setWecrmAccountId('');
|
|
387
|
+
setAccessToken('');
|
|
388
|
+
setHostName('');
|
|
389
|
+
setAccountName('');
|
|
319
390
|
}
|
|
320
391
|
}, [accountData.selectedRcsAccount]);
|
|
321
392
|
|
|
322
393
|
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
323
394
|
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
324
395
|
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
396
|
+
const isCarouselType = templateType === contentType.carousel;
|
|
397
|
+
|
|
398
|
+
const MAX_RCS_CAROUSEL_ALLOWED = 10;
|
|
399
|
+
// Uploads for RCS are stored in redux under dynamic keys `uploadedAssetData${index}`.
|
|
400
|
+
// Carousel needs per-card indices; otherwise all cards "restore" the last uploaded asset
|
|
401
|
+
// and show the same media/thumbnail.
|
|
402
|
+
const RCS_CAROUSEL_ASSET_INDEX_BASE = 10; // keep away from standalone indices 0 (media) and 1 (thumbnail)
|
|
403
|
+
const getCarouselImageAssetIndex = (cardIndex) =>
|
|
404
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
|
|
405
|
+
const getCarouselVideoAssetIndex = (cardIndex) =>
|
|
406
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 1;
|
|
407
|
+
const getCarouselThumbnailAssetIndex = (cardIndex) =>
|
|
408
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 2;
|
|
409
|
+
const isThumbnailAssetIndex = (index) => {
|
|
410
|
+
if (index === 1) return true; // standalone thumbnail
|
|
411
|
+
if (index >= RCS_CAROUSEL_ASSET_INDEX_BASE) {
|
|
412
|
+
return ((index - RCS_CAROUSEL_ASSET_INDEX_BASE) % 3) === 2; // carousel thumbnail slot
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Carousel dimension key: `${HEIGHT}_${WIDTH}` (e.g., SHORT_SMALL)
|
|
418
|
+
const getCarouselDimensionKey = () => `${selectedCarouselHeight}_${selectedCarouselWidth}`;
|
|
419
|
+
|
|
420
|
+
const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
|
|
421
|
+
setCarouselData((prev = []) => {
|
|
422
|
+
const updated = cloneDeep(prev);
|
|
423
|
+
if (!updated?.[cardIndex]) return updated;
|
|
424
|
+
if (clearImage) updated[cardIndex].imageSrc = '';
|
|
425
|
+
if (clearVideo) updated[cardIndex].videoAsset = {};
|
|
426
|
+
if (clearThumb) updated[cardIndex].thumbnailSrc = '';
|
|
427
|
+
return updated;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
|
|
431
|
+
if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
|
|
432
|
+
if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const resetCarouselMediaForAllCards = () => {
|
|
436
|
+
setCarouselData((prev = []) => {
|
|
437
|
+
const updated = cloneDeep(prev);
|
|
438
|
+
updated.forEach((card) => {
|
|
439
|
+
if (!card) return;
|
|
440
|
+
card.imageSrc = '';
|
|
441
|
+
card.videoAsset = {};
|
|
442
|
+
card.thumbnailSrc = '';
|
|
443
|
+
});
|
|
444
|
+
return updated;
|
|
445
|
+
});
|
|
446
|
+
(carouselData || []).forEach((_, idx) => {
|
|
447
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
|
|
448
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
|
|
449
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
|
|
450
|
+
});
|
|
451
|
+
// Force tab panes to remount after global reset (some tab implementations cache inactive panes)
|
|
452
|
+
setCarouselResetNonce((n) => n + 1);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const RCS_CAROUSEL_INITIAL_CARD = {
|
|
456
|
+
title: '',
|
|
457
|
+
description: '',
|
|
458
|
+
mediaType: RCS_MEDIA_TYPES.IMAGE, // per-card
|
|
459
|
+
imageSrc: '',
|
|
460
|
+
videoAsset: {}, // CapVideoUpload object shape
|
|
461
|
+
thumbnailSrc: '',
|
|
462
|
+
suggestions: [],
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const RCS_CAROUSEL_INITIAL_FIRST_CARD = {
|
|
466
|
+
...RCS_CAROUSEL_INITIAL_CARD,
|
|
467
|
+
suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const ensureFirstCardDefaultPhoneSuggestions = (cards) => {
|
|
471
|
+
const next = cloneDeep(cards || []);
|
|
472
|
+
if (next.length === 0) return next;
|
|
473
|
+
const s = next[0].suggestions;
|
|
474
|
+
if (!Array.isArray(s) || s.length === 0) {
|
|
475
|
+
next[0] = {
|
|
476
|
+
...next[0],
|
|
477
|
+
suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return next;
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
|
|
484
|
+
// updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
|
|
485
|
+
// suggestions typed since the last commit (stale closure).
|
|
486
|
+
const handleCarouselValueChange = (carouselIndex, fields) => {
|
|
487
|
+
setCarouselData((prev = []) => {
|
|
488
|
+
const updated = cloneDeep(prev);
|
|
489
|
+
if (!updated[carouselIndex]) return prev;
|
|
490
|
+
fields.forEach(({ fieldName, value }) => {
|
|
491
|
+
updated[carouselIndex][fieldName] = value;
|
|
492
|
+
});
|
|
493
|
+
return updated;
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const updateCarouselErrors = (carouselIndex, patch) => {
|
|
498
|
+
setCarouselErrors((prev = []) => {
|
|
499
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
500
|
+
next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
|
|
501
|
+
return next;
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const deleteCarouselCard = (index) => {
|
|
506
|
+
let nextIdx = 0;
|
|
507
|
+
flushSync(() => {
|
|
508
|
+
setCarouselData((prev = []) => {
|
|
509
|
+
const updated = cloneDeep(prev);
|
|
510
|
+
if (index < 0 || index >= updated.length) return updated;
|
|
511
|
+
updated.splice(index, 1);
|
|
512
|
+
nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
|
|
513
|
+
return ensureFirstCardDefaultPhoneSuggestions(updated);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
setCarouselErrors((prev = []) => {
|
|
517
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
518
|
+
next.splice(index, 1);
|
|
519
|
+
return next;
|
|
520
|
+
});
|
|
521
|
+
setActiveCarouselIndex(`${nextIdx}`);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const carouselButtonTextHasForbiddenChars = (value) => {
|
|
525
|
+
if (!value) return false;
|
|
526
|
+
if (value.includes('[') || value.includes(']')) return true;
|
|
527
|
+
const withoutValidVariables = value.replace(/\{\{[^}]*\}\}/g, '');
|
|
528
|
+
if (withoutValidVariables.includes('{') || withoutValidVariables.includes('}')) return true;
|
|
529
|
+
return false;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const isCompleteSavedCarouselSuggestion = (s) => {
|
|
533
|
+
if (!s || !s.isSaved) return false;
|
|
534
|
+
const text = (s.text || '').trim();
|
|
535
|
+
if (!text || !isValidText(text) || carouselButtonTextHasForbiddenChars(text)) return false;
|
|
536
|
+
if (s.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
|
|
537
|
+
return String(s.phoneNumber || '').length >= 5;
|
|
538
|
+
}
|
|
539
|
+
if (s.type === RCS_BUTTON_TYPES.CTA) {
|
|
540
|
+
const url = String(s.url || '').trim();
|
|
541
|
+
if (!url || url.length > URL_MAX_LENGTH) return false;
|
|
542
|
+
const subtype = s.urlType || RCS_CTA_URL_TYPE.STATIC;
|
|
543
|
+
if (subtype === RCS_CTA_URL_TYPE.DYNAMIC) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
if (!isUrl(url)) return false;
|
|
547
|
+
const varMatches = url.match(invalidVarRegex);
|
|
548
|
+
return !(varMatches && varMatches.length > 0);
|
|
549
|
+
}
|
|
550
|
+
if (s.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const isCarouselCardValid = (card, cardIndex) => {
|
|
557
|
+
if (!card) return false;
|
|
558
|
+
if (!card.title || !card.title.trim()) return false;
|
|
559
|
+
if ((card.title || '').length > TEMPLATE_TITLE_MAX_LENGTH) return false;
|
|
560
|
+
if (!card.description || !card.description.trim()) return false;
|
|
561
|
+
if ((card.description || '').length > RCS_RICH_CARD_MAX_LENGTH) return false;
|
|
562
|
+
let mediaOk = false;
|
|
563
|
+
if (card.mediaType === RCS_MEDIA_TYPES.IMAGE) {
|
|
564
|
+
mediaOk = !!(card.imageSrc && String(card.imageSrc).trim());
|
|
565
|
+
} else if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
|
|
566
|
+
const hasVideo = !!(card.videoAsset && card.videoAsset.videoSrc && String(card.videoAsset.videoSrc).trim());
|
|
567
|
+
const hasThumb = !!(card.thumbnailSrc && String(card.thumbnailSrc).trim());
|
|
568
|
+
mediaOk = hasVideo && hasThumb;
|
|
569
|
+
} else {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
if (!mediaOk) return false;
|
|
573
|
+
if (cardIndex === 0) {
|
|
574
|
+
const sugg = Array.isArray(card.suggestions) ? card.suggestions : [];
|
|
575
|
+
if (!sugg.some(isCompleteSavedCarouselSuggestion)) return false;
|
|
576
|
+
}
|
|
577
|
+
return true;
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const checkDisableAddCarouselButton = () => {
|
|
581
|
+
const idx = parseInt(activeCarouselIndex, 10);
|
|
582
|
+
const activeCard = carouselData?.[idx];
|
|
583
|
+
return !isCarouselCardValid(activeCard, idx);
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const addCarouselCard = () => {
|
|
587
|
+
let newIndex = 0;
|
|
588
|
+
flushSync(() => {
|
|
589
|
+
setCarouselData((prev = []) => {
|
|
590
|
+
const updated = cloneDeep(prev);
|
|
591
|
+
updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
|
|
592
|
+
newIndex = updated.length - 1;
|
|
593
|
+
return updated;
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
|
|
597
|
+
setActiveCarouselIndex(`${newIndex}`);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Initialize carousel data when switching to carousel type
|
|
601
|
+
useEffect(() => {
|
|
602
|
+
if (!isCarouselType) return;
|
|
603
|
+
if (!carouselData || carouselData.length === 0) {
|
|
604
|
+
setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
|
|
605
|
+
setCarouselErrors([{}]);
|
|
606
|
+
setActiveCarouselIndex('0');
|
|
607
|
+
}
|
|
608
|
+
}, [isCarouselType]);
|
|
609
|
+
|
|
610
|
+
// keep derived carousel key in sync
|
|
611
|
+
useEffect(() => {
|
|
612
|
+
if (!isCarouselType) return;
|
|
613
|
+
if (!selectedCarouselHeight || !selectedCarouselWidth) return;
|
|
614
|
+
// Required format: HEIGHT_WIDTH
|
|
615
|
+
setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
|
|
616
|
+
}, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
|
|
617
|
+
|
|
618
|
+
const renderCarouselDimensionSelection = () => {
|
|
619
|
+
if (!isCarouselType) return null;
|
|
620
|
+
return (
|
|
621
|
+
<CapRow className="rcs-carousel-dimension-section">
|
|
622
|
+
<CapRow gutter={16} className="rcs-carousel-dimension-row">
|
|
623
|
+
<CapColumn span={12}>
|
|
624
|
+
<CapHeading type="h4" className="rcs-carousel-dimension-label">Card height</CapHeading>
|
|
625
|
+
{isEditFlow ? (
|
|
626
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
627
|
+
{CAROUSEL_HEIGHT_OPTIONS.find((option) => option.value === selectedCarouselHeight)?.label || '-'}
|
|
628
|
+
</CapHeading>
|
|
629
|
+
) : (
|
|
630
|
+
<CapSelect
|
|
631
|
+
id="rcs-carousel-height-select"
|
|
632
|
+
value={selectedCarouselHeight}
|
|
633
|
+
onChange={(val) => {
|
|
634
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
635
|
+
resetCarouselMediaForAllCards();
|
|
636
|
+
setSelectedCarouselHeight(val);
|
|
637
|
+
}}
|
|
638
|
+
options={CAROUSEL_HEIGHT_OPTIONS}
|
|
639
|
+
disabled={!isFullMode}
|
|
640
|
+
/>
|
|
641
|
+
)}
|
|
642
|
+
</CapColumn>
|
|
643
|
+
<CapColumn span={12}>
|
|
644
|
+
<CapHeading type="h4" className="rcs-carousel-dimension-label">Card width</CapHeading>
|
|
645
|
+
{isEditFlow ? (
|
|
646
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
647
|
+
{CAROUSEL_WIDTH_OPTIONS.find((option) => option.value === selectedCarouselWidth)?.label || '-'}
|
|
648
|
+
</CapHeading>
|
|
649
|
+
) : (
|
|
650
|
+
<CapSelect
|
|
651
|
+
id="rcs-carousel-width-select"
|
|
652
|
+
value={selectedCarouselWidth}
|
|
653
|
+
onChange={(val) => {
|
|
654
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
655
|
+
resetCarouselMediaForAllCards();
|
|
656
|
+
setSelectedCarouselWidth(val);
|
|
657
|
+
}}
|
|
658
|
+
options={CAROUSEL_WIDTH_OPTIONS}
|
|
659
|
+
disabled={!isFullMode}
|
|
660
|
+
/>
|
|
661
|
+
)}
|
|
662
|
+
</CapColumn>
|
|
663
|
+
</CapRow>
|
|
664
|
+
{!!selectedCarousel && (
|
|
665
|
+
<CapLabel type="label3" className="rcs-carousel-selected-dimension">
|
|
666
|
+
Selected: {selectedCarousel}
|
|
667
|
+
</CapLabel>
|
|
668
|
+
)}
|
|
669
|
+
</CapRow>
|
|
670
|
+
);
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// Reuse rich-card buttons UI per carousel card
|
|
674
|
+
const renderButtonComponentForCarouselCard = (cardIndex) => {
|
|
675
|
+
const card = carouselData?.[cardIndex] || {};
|
|
676
|
+
const suggestionsForCard = card.suggestions || [];
|
|
677
|
+
return (
|
|
678
|
+
<>
|
|
679
|
+
<CapHeader
|
|
680
|
+
className="rcs-button-cta"
|
|
681
|
+
title={(
|
|
682
|
+
<CapRow type="flex">
|
|
683
|
+
<CapHeading type="h4">
|
|
684
|
+
{formatMessage(messages.btnLabel)}
|
|
685
|
+
</CapHeading>
|
|
686
|
+
</CapRow>
|
|
687
|
+
)}
|
|
688
|
+
description={!isEditFlow && (
|
|
689
|
+
<CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
|
|
690
|
+
)}
|
|
691
|
+
/>
|
|
692
|
+
<CapActionButton
|
|
693
|
+
buttonType={RCS_BUTTON_TYPES.NONE}
|
|
694
|
+
updateButtonChange={(data, btnIndex) => {
|
|
695
|
+
// Match existing behavior: allow CapActionButton to manage save gating.
|
|
696
|
+
const updated = cloneDeep(suggestionsForCard);
|
|
697
|
+
if (btnIndex === MAX_BUTTONS) {
|
|
698
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: data }]);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
updated[btnIndex] = data;
|
|
702
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
|
|
703
|
+
}}
|
|
704
|
+
deleteButtonHandler={(btnIndex) => {
|
|
705
|
+
if (cardIndex === 0 && btnIndex === 0) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const savedCount = (suggestionsForCard || []).filter((x) => x && x.isSaved).length;
|
|
709
|
+
const target = (suggestionsForCard || []).find((s) => s && s.index === btnIndex);
|
|
710
|
+
if (cardIndex === 0 && target?.isSaved && savedCount <= 1) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const updated = cloneDeep(suggestionsForCard)
|
|
714
|
+
.filter((i) => i.index !== btnIndex)
|
|
715
|
+
.map((i, idx) => ({ ...i, index: idx }));
|
|
716
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
|
|
717
|
+
}}
|
|
718
|
+
suggestions={suggestionsForCard}
|
|
719
|
+
isEditFlow={isEditFlow}
|
|
720
|
+
isFullMode={isFullMode}
|
|
721
|
+
maxButtons={MAX_BUTTONS}
|
|
722
|
+
host={hostName}
|
|
723
|
+
minSavedSuggestions={cardIndex === 0 ? 1 : 0}
|
|
724
|
+
hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
|
|
725
|
+
/>
|
|
726
|
+
</>
|
|
727
|
+
);
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const renderCarouselCardMedia = (cardIndex) => {
|
|
731
|
+
const card = carouselData?.[cardIndex] || {};
|
|
732
|
+
const dimKey = getCarouselDimensionKey();
|
|
733
|
+
|
|
734
|
+
if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
|
|
735
|
+
return (
|
|
736
|
+
<>
|
|
737
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Video</CapHeading>
|
|
738
|
+
<CapVideoUpload
|
|
739
|
+
index={getCarouselVideoAssetIndex(cardIndex)}
|
|
740
|
+
allowedExtensionsRegex={ALLOWED_EXTENSIONS_VIDEO_REGEX}
|
|
741
|
+
videoSize={RCS_CAROUSEL_VIDEO_SIZE}
|
|
742
|
+
isFullMode={isFullMode}
|
|
743
|
+
uploadAsset={uploadRcsVideo}
|
|
744
|
+
uploadedAssetList={card.videoAsset || {}}
|
|
745
|
+
onVideoUploadUpdateAssestList={(_, val) => {
|
|
746
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'videoAsset', value: val }]);
|
|
747
|
+
}}
|
|
748
|
+
videoData={rcsData}
|
|
749
|
+
className="cap-custom-video-upload"
|
|
750
|
+
formClassName={"rcs-video-upload"}
|
|
751
|
+
channel={RCS}
|
|
752
|
+
errorMessage={formatMessage(messages.videoErrorMessage)}
|
|
753
|
+
showVideoNameAndDuration={false}
|
|
754
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
755
|
+
channelSpecificStyle={!isFullMode}
|
|
756
|
+
/>
|
|
757
|
+
|
|
758
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
|
|
759
|
+
<CapImageUpload
|
|
760
|
+
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
761
|
+
imgWidth={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.width}
|
|
762
|
+
imgHeight={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.height}
|
|
763
|
+
imgSize={RCS_THUMBNAIL_MAX_SIZE}
|
|
764
|
+
uploadAsset={uploadRcsImage}
|
|
765
|
+
isFullMode={isFullMode}
|
|
766
|
+
imageSrc={card.thumbnailSrc}
|
|
767
|
+
updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: val }])}
|
|
768
|
+
updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: '' }])}
|
|
769
|
+
minImgSize={RCS_THUMBNAIL_MIN_SIZE}
|
|
770
|
+
index={getCarouselThumbnailAssetIndex(cardIndex)}
|
|
771
|
+
className="cap-custom-image-upload"
|
|
772
|
+
key={`rcs-carousel-thumb-${cardIndex}-${dimKey}`}
|
|
773
|
+
imageData={rcsData}
|
|
774
|
+
channel={RCS}
|
|
775
|
+
channelSpecificStyle={!isFullMode}
|
|
776
|
+
skipDimensionValidation={true}
|
|
777
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
778
|
+
disabled={isEditFlow || !isFullMode}
|
|
779
|
+
/>
|
|
780
|
+
</>
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Default: IMAGE
|
|
785
|
+
return (
|
|
786
|
+
<>
|
|
787
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">{formatMessage(messages.mediaImage)}</CapHeading>
|
|
788
|
+
<CapImageUpload
|
|
789
|
+
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
790
|
+
imgWidth={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.width}
|
|
791
|
+
imgHeight={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.height}
|
|
792
|
+
imgSize={RCS_CAROUSEL_IMG_SIZE}
|
|
793
|
+
uploadAsset={uploadRcsImage}
|
|
794
|
+
isFullMode={isFullMode}
|
|
795
|
+
imageSrc={card.imageSrc}
|
|
796
|
+
updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: val }])}
|
|
797
|
+
updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: '' }])}
|
|
798
|
+
index={getCarouselImageAssetIndex(cardIndex)}
|
|
799
|
+
className="cap-custom-image-upload"
|
|
800
|
+
key={`rcs-carousel-image-${cardIndex}-${dimKey}`}
|
|
801
|
+
imageData={rcsData}
|
|
802
|
+
channel={RCS}
|
|
803
|
+
channelSpecificStyle={!isFullMode}
|
|
804
|
+
skipDimensionValidation={true}
|
|
805
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
806
|
+
disabled={isEditFlow || !isFullMode}
|
|
807
|
+
/>
|
|
808
|
+
</>
|
|
809
|
+
);
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const renderCarouselCardButtons = (cardIndex) => {
|
|
813
|
+
return renderButtonComponentForCarouselCard(cardIndex);
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const getCarouselTabPanes = () => {
|
|
817
|
+
return (carouselData || []).map((card, index) => {
|
|
818
|
+
return {
|
|
819
|
+
key: String(index),
|
|
820
|
+
tab: index + 1,
|
|
821
|
+
content: (
|
|
822
|
+
<CapCard
|
|
823
|
+
title={`Card ${index + 1}`}
|
|
824
|
+
extra={
|
|
825
|
+
!isEditFlow &&
|
|
826
|
+
(carouselData.length === 1 ? (
|
|
827
|
+
<CapTooltip title={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}>
|
|
828
|
+
<span className="button-disabled-tooltip-wrapper rcs-carousel-delete-tooltip-wrap">
|
|
829
|
+
<CapButton
|
|
830
|
+
className="rcs-carousel-card-delete"
|
|
831
|
+
type="flat"
|
|
832
|
+
onClick={() => deleteCarouselCard(index)}
|
|
833
|
+
disabled
|
|
834
|
+
aria-label={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}
|
|
835
|
+
>
|
|
836
|
+
<CapIcon type="delete" />
|
|
837
|
+
</CapButton>
|
|
838
|
+
</span>
|
|
839
|
+
</CapTooltip>
|
|
840
|
+
) : (
|
|
841
|
+
<CapButton
|
|
842
|
+
className="rcs-carousel-card-delete"
|
|
843
|
+
type="flat"
|
|
844
|
+
onClick={() => deleteCarouselCard(index)}
|
|
845
|
+
aria-label={formatMessage(globalMessages.delete)}
|
|
846
|
+
>
|
|
847
|
+
<CapIcon type="delete" />
|
|
848
|
+
</CapButton>
|
|
849
|
+
))
|
|
850
|
+
}
|
|
851
|
+
className="rcs-carousel-card"
|
|
852
|
+
>
|
|
853
|
+
{/* Media selection should be at top of card */}
|
|
854
|
+
{isEditFlow ? (
|
|
855
|
+
<div className="rcs-creative-name-readonly">
|
|
856
|
+
<CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
|
|
857
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
858
|
+
{mediaRadioOptions.find((option) => option.value === card.mediaType)?.label || '-'}
|
|
859
|
+
</CapHeading>
|
|
860
|
+
</div>
|
|
861
|
+
) : (
|
|
862
|
+
<CapRow className="rcs-carousel-media-selection">
|
|
863
|
+
<CapColumn className="rcs-carousel-media-selection-heading">
|
|
864
|
+
<CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
|
|
865
|
+
</CapColumn>
|
|
866
|
+
<CapColumn>
|
|
867
|
+
<CapRadioGroup
|
|
868
|
+
id={`rcs-carousel-media-radio-${index}`}
|
|
869
|
+
options={mediaRadioOptions}
|
|
870
|
+
value={card.mediaType}
|
|
871
|
+
onChange={({ target: { value } }) => {
|
|
872
|
+
// Reset media fields when switching type
|
|
873
|
+
if (value === RCS_MEDIA_TYPES.IMAGE) {
|
|
874
|
+
// Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
|
|
875
|
+
clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
|
|
876
|
+
handleCarouselValueChange(index, [
|
|
877
|
+
{ fieldName: 'mediaType', value },
|
|
878
|
+
{ fieldName: 'videoAsset', value: {} },
|
|
879
|
+
{ fieldName: 'thumbnailSrc', value: '' },
|
|
880
|
+
]);
|
|
881
|
+
} else {
|
|
882
|
+
// Switching to VIDEO: clear image upload so it doesn't auto-restore.
|
|
883
|
+
clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
|
|
884
|
+
handleCarouselValueChange(index, [
|
|
885
|
+
{ fieldName: 'mediaType', value },
|
|
886
|
+
{ fieldName: 'imageSrc', value: '' },
|
|
887
|
+
]);
|
|
888
|
+
}
|
|
889
|
+
}}
|
|
890
|
+
disabled={!isFullMode}
|
|
891
|
+
className="rcs-radio"
|
|
892
|
+
/>
|
|
893
|
+
</CapColumn>
|
|
894
|
+
</CapRow>
|
|
895
|
+
)}
|
|
896
|
+
|
|
897
|
+
<CapRow className={`rcs-carousel-media-upload ${isEditFlow ? 'rcs-carousel-media-upload--readonly-above' : ''}`}>
|
|
898
|
+
{renderCarouselCardMedia(index)}
|
|
899
|
+
</CapRow>
|
|
900
|
+
|
|
901
|
+
{/* Title after media */}
|
|
902
|
+
<CapRow className="rcs-carousel-card-row">
|
|
903
|
+
<CapHeader
|
|
904
|
+
className="rcs-template-title-label"
|
|
905
|
+
title={<CapHeading type="h4">Card title</CapHeading>}
|
|
906
|
+
suffix={
|
|
907
|
+
(isEditFlow || !isFullMode) ? (
|
|
908
|
+
<TagList
|
|
909
|
+
label={formatMessage(globalMessages.addLabels)}
|
|
910
|
+
onTagSelect={onCarouselTagSelect}
|
|
911
|
+
location={location}
|
|
912
|
+
tags={getRcsTags()}
|
|
913
|
+
onContextChange={handleOnTagsContextChange}
|
|
914
|
+
injectedTags={injectedTags || {}}
|
|
915
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
916
|
+
/>
|
|
917
|
+
) : (
|
|
918
|
+
<CapButton
|
|
919
|
+
data-testid={`rcs-carousel-title-add-var-${index}`}
|
|
920
|
+
type="flat"
|
|
921
|
+
isAddBtn
|
|
922
|
+
onClick={() => appendVarToCarouselField(index, 'title')}
|
|
923
|
+
disabled={!!carouselErrors?.[index]?.title}
|
|
924
|
+
>
|
|
925
|
+
{formatMessage(messages.addVar)}
|
|
926
|
+
</CapButton>
|
|
927
|
+
)
|
|
928
|
+
}
|
|
929
|
+
/>
|
|
930
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
931
|
+
{(isEditFlow || !isFullMode) ? (
|
|
932
|
+
renderCarouselEditMessage(card.title || '')
|
|
933
|
+
) : (
|
|
934
|
+
<CapInput
|
|
935
|
+
value={card.title || ''}
|
|
936
|
+
placeholder={formatMessage(messages.templateTitlePlaceholder)}
|
|
937
|
+
onChange={({ target: { value } }) => {
|
|
938
|
+
let error = false;
|
|
939
|
+
if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
940
|
+
error = formatMessage(messages.templateHeaderLengthError);
|
|
941
|
+
} else {
|
|
942
|
+
error = variableErrorHandling(value);
|
|
943
|
+
}
|
|
944
|
+
updateCarouselErrors(index, { title: error });
|
|
945
|
+
handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
|
|
946
|
+
}}
|
|
947
|
+
disabled={isEditFlow || !isFullMode}
|
|
948
|
+
errorMessage={carouselErrors?.[index]?.title}
|
|
949
|
+
/>
|
|
950
|
+
)}
|
|
951
|
+
</CapRow>
|
|
952
|
+
</CapRow>
|
|
953
|
+
{!isEditFlow && (
|
|
954
|
+
<CapRow className="rcs-carousel-character-count-row">
|
|
955
|
+
{renderCarouselCharacterCount(
|
|
956
|
+
getCarouselTitleCharacterCount(index),
|
|
957
|
+
getTitleMaxLength(),
|
|
958
|
+
)}
|
|
959
|
+
</CapRow>
|
|
960
|
+
)}
|
|
961
|
+
|
|
962
|
+
{/* Description after title */}
|
|
963
|
+
<CapRow className="rcs-carousel-card-row">
|
|
964
|
+
<CapHeader
|
|
965
|
+
title={<CapHeading type="h4">Card body text</CapHeading>}
|
|
966
|
+
suffix={
|
|
967
|
+
(isEditFlow || !isFullMode) ? (
|
|
968
|
+
<TagList
|
|
969
|
+
label={formatMessage(globalMessages.addLabels)}
|
|
970
|
+
onTagSelect={onCarouselTagSelect}
|
|
971
|
+
location={location}
|
|
972
|
+
tags={getRcsTags()}
|
|
973
|
+
onContextChange={handleOnTagsContextChange}
|
|
974
|
+
injectedTags={injectedTags || {}}
|
|
975
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
976
|
+
/>
|
|
977
|
+
) : (
|
|
978
|
+
<CapButton
|
|
979
|
+
data-testid={`rcs-carousel-desc-add-var-${index}`}
|
|
980
|
+
type="flat"
|
|
981
|
+
isAddBtn
|
|
982
|
+
onClick={() => appendVarToCarouselField(index, 'description')}
|
|
983
|
+
disabled={!!carouselErrors?.[index]?.description}
|
|
984
|
+
>
|
|
985
|
+
{formatMessage(messages.addVar)}
|
|
986
|
+
</CapButton>
|
|
987
|
+
)
|
|
988
|
+
}
|
|
989
|
+
/>
|
|
990
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
991
|
+
{(isEditFlow || !isFullMode) ? (
|
|
992
|
+
renderCarouselEditMessage(card.description || '')
|
|
993
|
+
) : (
|
|
994
|
+
<TextArea
|
|
995
|
+
autosize={{ minRows: 3, maxRows: 5 }}
|
|
996
|
+
value={card.description || ''}
|
|
997
|
+
placeholder={formatMessage(messages.templateDescPlaceholder)}
|
|
998
|
+
onChange={({ target: { value } }) => {
|
|
999
|
+
let error = false;
|
|
1000
|
+
if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
|
|
1001
|
+
error = formatMessage(messages.templateMessageLengthError);
|
|
1002
|
+
} else {
|
|
1003
|
+
error = variableErrorHandling(value);
|
|
1004
|
+
}
|
|
1005
|
+
updateCarouselErrors(index, { description: error });
|
|
1006
|
+
handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
|
|
1007
|
+
}}
|
|
1008
|
+
disabled={isEditFlow || !isFullMode}
|
|
1009
|
+
errorMessage={
|
|
1010
|
+
carouselErrors?.[index]?.description && (
|
|
1011
|
+
<CapError className="rcs-template-message-error">
|
|
1012
|
+
{carouselErrors[index].description}
|
|
1013
|
+
</CapError>
|
|
1014
|
+
)
|
|
1015
|
+
}
|
|
1016
|
+
/>
|
|
1017
|
+
)}
|
|
1018
|
+
</CapRow>
|
|
1019
|
+
</CapRow>
|
|
1020
|
+
{!isEditFlow && (
|
|
1021
|
+
<CapRow className="rcs-carousel-character-count-row">
|
|
1022
|
+
{renderCarouselCharacterCount(
|
|
1023
|
+
getCarouselDescriptionCharacterCount(index),
|
|
1024
|
+
getDescriptionMaxLength(),
|
|
1025
|
+
)}
|
|
1026
|
+
</CapRow>
|
|
1027
|
+
)}
|
|
1028
|
+
|
|
1029
|
+
<CapDivider className="rcs-carousel-card-divider" />
|
|
1030
|
+
{renderCarouselCardButtons(index)}
|
|
1031
|
+
</CapCard>
|
|
1032
|
+
),
|
|
1033
|
+
};
|
|
1034
|
+
});
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const renderCarouselSection = () => {
|
|
1038
|
+
if (!isCarouselType) return null;
|
|
1039
|
+
|
|
1040
|
+
const isMaxCarouselCardsReached = MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0);
|
|
1041
|
+
const addCardTooltipTitle = isMaxCarouselCardsReached
|
|
1042
|
+
? formatMessage(messages.rcsCarouselMaxCardTooltip, { max: MAX_RCS_CAROUSEL_ALLOWED })
|
|
1043
|
+
// "Set up the first card" only makes sense while creating a new template —
|
|
1044
|
+
// in edit flow the cards already exist, so don't show it there.
|
|
1045
|
+
: (!isEditFlow && checkDisableAddCarouselButton() ? formatMessage(messages.rcsCarouselAddCardDisabledTooltip) : '');
|
|
1046
|
+
|
|
1047
|
+
// No "+" in edit flow (cards can't be added there) — and with no "+", the
|
|
1048
|
+
// divider before it has nothing to separate, so skip both together instead
|
|
1049
|
+
// of rendering a dangling divider.
|
|
1050
|
+
const operations = isEditFlow ? null : (
|
|
1051
|
+
<>
|
|
1052
|
+
<CapDivider type="vertical" />
|
|
1053
|
+
<CapTooltip title={addCardTooltipTitle}>
|
|
1054
|
+
<span className="button-disabled-tooltip-wrapper">
|
|
1055
|
+
<CapButton
|
|
1056
|
+
onClick={addCarouselCard}
|
|
1057
|
+
type="flat"
|
|
1058
|
+
className="add-carousel-content-button"
|
|
1059
|
+
disabled={
|
|
1060
|
+
isEditFlow ||
|
|
1061
|
+
!isFullMode ||
|
|
1062
|
+
isMaxCarouselCardsReached ||
|
|
1063
|
+
checkDisableAddCarouselButton()
|
|
1064
|
+
}
|
|
1065
|
+
>
|
|
1066
|
+
<CapIcon type="plus" />
|
|
1067
|
+
</CapButton>
|
|
1068
|
+
</span>
|
|
1069
|
+
</CapTooltip>
|
|
1070
|
+
</>
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
return (
|
|
1074
|
+
<CapRow className={`rcs-carousel-section ${isEditFlow ? 'rcs-carousel-section--readonly-above' : ''}`}>
|
|
1075
|
+
{renderCarouselDimensionSelection()}
|
|
1076
|
+
<CapRow className="rcs-carousel-tab">
|
|
1077
|
+
<CapTab
|
|
1078
|
+
key={`rcs-carousel-tab-${carouselResetNonce}`}
|
|
1079
|
+
defaultActiveKey="0"
|
|
1080
|
+
activeKey={activeCarouselIndex}
|
|
1081
|
+
tabBarExtraContent={operations}
|
|
1082
|
+
onChange={(key) => {
|
|
1083
|
+
setActiveCarouselIndex(`${key}`);
|
|
1084
|
+
// reset focused var when switching cards (as per requirement)
|
|
1085
|
+
setCarouselFocusedVarId('');
|
|
1086
|
+
}}
|
|
1087
|
+
panes={getCarouselTabPanes()}
|
|
1088
|
+
/>
|
|
1089
|
+
</CapRow>
|
|
1090
|
+
</CapRow>
|
|
1091
|
+
);
|
|
1092
|
+
};
|
|
325
1093
|
|
|
326
1094
|
const mediaRadioOptions = [
|
|
327
1095
|
{
|
|
@@ -330,11 +1098,16 @@ export const Rcs = (props) => {
|
|
|
330
1098
|
},
|
|
331
1099
|
{
|
|
332
1100
|
value: RCS_MEDIA_TYPES.VIDEO,
|
|
333
|
-
label: formatMessage(
|
|
1101
|
+
label: formatMessage(
|
|
1102
|
+
templateType === contentType.carousel
|
|
1103
|
+
? messages.carouselMediaVideoOption
|
|
1104
|
+
: messages.mediaVideo,
|
|
1105
|
+
),
|
|
334
1106
|
},
|
|
335
1107
|
];
|
|
336
1108
|
const aiContentBotDisabled = isAiContentBotDisabled();
|
|
337
1109
|
|
|
1110
|
+
|
|
338
1111
|
const updateButtonChange = (data, index) => {
|
|
339
1112
|
if (data && data.text) {
|
|
340
1113
|
const forbiddenError = forbiddenCharactersValidation(data.text);
|
|
@@ -371,7 +1144,9 @@ export const Rcs = (props) => {
|
|
|
371
1144
|
if (isFullMode) return;
|
|
372
1145
|
if (loadingTags || !tags || tags.length === 0) return;
|
|
373
1146
|
const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
374
|
-
const
|
|
1147
|
+
const slotOffset =
|
|
1148
|
+
type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
|
|
1149
|
+
const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
|
|
375
1150
|
if (!resolved) {
|
|
376
1151
|
if (type === TITLE_TEXT) setTemplateTitleError(false);
|
|
377
1152
|
if (type === MESSAGE_TEXT) setTemplateDescError(false);
|
|
@@ -381,7 +1156,7 @@ export const Rcs = (props) => {
|
|
|
381
1156
|
let contentForValidation = resolved;
|
|
382
1157
|
const placeholderTokens = templateStr.match(rcsVarRegex) || [];
|
|
383
1158
|
placeholderTokens.forEach((t) => {
|
|
384
|
-
const escaped = t.replace(
|
|
1159
|
+
const escaped = t.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
|
|
385
1160
|
contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
|
|
386
1161
|
});
|
|
387
1162
|
if (!contentForValidation.trim()) {
|
|
@@ -398,10 +1173,16 @@ export const Rcs = (props) => {
|
|
|
398
1173
|
tagModule: getDefaultTags,
|
|
399
1174
|
isFullMode,
|
|
400
1175
|
}) || {};
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
1176
|
+
const unsupportedTagsLengthCheck =
|
|
1177
|
+
validationResponse?.unsupportedTags?.length > 0;
|
|
1178
|
+
const errorMsg =
|
|
1179
|
+
(unsupportedTagsLengthCheck &&
|
|
1180
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1181
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
1182
|
+
})) ||
|
|
1183
|
+
(validationResponse.isBraceError &&
|
|
1184
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
1185
|
+
false;
|
|
405
1186
|
if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
|
|
406
1187
|
if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
|
|
407
1188
|
};
|
|
@@ -414,15 +1195,98 @@ export const Rcs = (props) => {
|
|
|
414
1195
|
validateResolvedTagsForType(MESSAGE_TEXT);
|
|
415
1196
|
}, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
|
|
416
1197
|
|
|
1198
|
+
useEffect(() => {
|
|
1199
|
+
if (isFullMode || !isCarouselType) return;
|
|
1200
|
+
if (loadingTags || !tags || tags.length === 0) return;
|
|
1201
|
+
(carouselData || []).forEach((card, idx) => {
|
|
1202
|
+
['title', 'description'].forEach((field) => {
|
|
1203
|
+
const templateStr = card?.[field] || '';
|
|
1204
|
+
if (!templateStr) return;
|
|
1205
|
+
const resolved = resolveTemplateWithMap(templateStr);
|
|
1206
|
+
if (!resolved) {
|
|
1207
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
let contentForValidation = resolved;
|
|
1211
|
+
const placeholderTokens = templateStr.match(rcsVarRegex) || [];
|
|
1212
|
+
placeholderTokens.forEach((t) => {
|
|
1213
|
+
const escaped = t.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
|
|
1214
|
+
contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
|
|
1215
|
+
});
|
|
1216
|
+
if (!contentForValidation.trim()) {
|
|
1217
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const validationResponse = validateTags({
|
|
1221
|
+
content: contentForValidation,
|
|
1222
|
+
tagsParam: tags,
|
|
1223
|
+
injectedTagsParams: injectedTags,
|
|
1224
|
+
location,
|
|
1225
|
+
tagModule: getDefaultTags,
|
|
1226
|
+
eventContextTags,
|
|
1227
|
+
isFullMode,
|
|
1228
|
+
}) || {};
|
|
1229
|
+
const errorMsg =
|
|
1230
|
+
(validationResponse?.unsupportedTags?.length > 0 &&
|
|
1231
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1232
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
1233
|
+
})) ||
|
|
1234
|
+
(validationResponse.isBraceError &&
|
|
1235
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
1236
|
+
false;
|
|
1237
|
+
updateCarouselErrors(idx, { [field]: errorMsg });
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
}, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
|
|
1241
|
+
|
|
417
1242
|
const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
|
|
418
1243
|
|
|
419
|
-
const
|
|
1244
|
+
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
1245
|
+
|
|
1246
|
+
/** Same `{{tag}}` in both title and description must not share one semantic map entry. */
|
|
1247
|
+
const rcsSpanningSemanticVarNames = useMemo(
|
|
1248
|
+
() => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
|
|
1249
|
+
[templateTitle, templateDesc],
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
/** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
|
|
1253
|
+
const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
|
|
1254
|
+
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
1255
|
+
const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
|
|
1256
|
+
const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
|
|
1257
|
+
let varOrdinal = 0;
|
|
1258
|
+
for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
|
|
1259
|
+
const segmentToken = templateSegments[segmentIndexInField];
|
|
1260
|
+
if (rcsVarTestRegex.test(segmentToken)) {
|
|
1261
|
+
if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
|
|
1262
|
+
return offset + varOrdinal;
|
|
1263
|
+
}
|
|
1264
|
+
varOrdinal += 1;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return null;
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
|
|
1272
|
+
* multi-field variable resolution.
|
|
1273
|
+
*/
|
|
1274
|
+
const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
|
|
420
1275
|
if (!str) return '';
|
|
421
|
-
const arr =
|
|
1276
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
1277
|
+
let varOrdinal = 0;
|
|
422
1278
|
return arr.map((elem) => {
|
|
423
1279
|
if (rcsVarTestRegex.test(elem)) {
|
|
424
1280
|
const key = getVarNameFromToken(elem);
|
|
425
|
-
const
|
|
1281
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
1282
|
+
varOrdinal += 1;
|
|
1283
|
+
const v = resolveCardVarMappedSlotValue(
|
|
1284
|
+
cardVarMapped,
|
|
1285
|
+
key,
|
|
1286
|
+
globalSlot,
|
|
1287
|
+
isEditLike,
|
|
1288
|
+
rcsSpanningSemanticVarNames.has(key),
|
|
1289
|
+
);
|
|
426
1290
|
if (isNil(v) || String(v)?.trim?.() === '') return elem;
|
|
427
1291
|
return String(v);
|
|
428
1292
|
}
|
|
@@ -430,39 +1294,145 @@ export const Rcs = (props) => {
|
|
|
430
1294
|
}).join('');
|
|
431
1295
|
};
|
|
432
1296
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1297
|
+
const buildCarouselCardsForPreview = (cards = []) => {
|
|
1298
|
+
// Track cumulative variable slot index across all cards so resolveTemplateWithMap
|
|
1299
|
+
// uses the correct globalSlot → slotKey for each token (e.g. card 1's {{3}} needs
|
|
1300
|
+
// slotOffset=2 so slotKey becomes "3", not "1").
|
|
1301
|
+
let slotOffset = 0;
|
|
1302
|
+
return (cards || []).map((card = {}) => {
|
|
1303
|
+
const rawTitle = card.title || '';
|
|
1304
|
+
const rawDesc = card.description || '';
|
|
1305
|
+
const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
|
|
1306
|
+
const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
|
|
1307
|
+
const resolvedTitle = !isFullMode
|
|
1308
|
+
? resolveTemplateWithMap(rawTitle, slotOffset)
|
|
1309
|
+
: rawTitle;
|
|
1310
|
+
const resolvedDesc = !isFullMode
|
|
1311
|
+
? resolveTemplateWithMap(rawDesc, slotOffset + titleVarCount)
|
|
1312
|
+
: rawDesc;
|
|
1313
|
+
if (!isFullMode) {
|
|
1314
|
+
slotOffset += titleVarCount + descVarCount;
|
|
1315
|
+
}
|
|
1316
|
+
const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
|
|
1317
|
+
const videoSrc = card.videoAsset?.videoSrc || '';
|
|
1318
|
+
return {
|
|
1319
|
+
mediaType: (card.mediaType || '').toLowerCase(),
|
|
1320
|
+
imageSrc: card.imageSrc || '',
|
|
1321
|
+
videoSrc,
|
|
1322
|
+
videoPreviewImg: videoThumb,
|
|
1323
|
+
title: resolvedTitle,
|
|
1324
|
+
bodyText: resolvedDesc,
|
|
1325
|
+
suggestions: card.suggestions || [],
|
|
1326
|
+
};
|
|
452
1327
|
});
|
|
453
|
-
}
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
|
|
1332
|
+
* (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
|
|
1333
|
+
* TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
|
|
1334
|
+
*/
|
|
1335
|
+
const getTemplateContent = useCallback(() => {
|
|
1336
|
+
if (templateType === contentType.carousel) {
|
|
1337
|
+
const carouselDimKey = getCarouselDimensionKey();
|
|
1338
|
+
const carouselImgDims =
|
|
1339
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
1340
|
+
const carouselVidDims =
|
|
1341
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
1342
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
1343
|
+
return {
|
|
1344
|
+
carouselData: buildCarouselCardsForPreview(carouselData),
|
|
1345
|
+
carouselPreviewDimensions: {
|
|
1346
|
+
imageWidth: carouselImgDims.width,
|
|
1347
|
+
imageHeight: carouselImgDims.height,
|
|
1348
|
+
videoThumbWidth: carouselVidDims.width,
|
|
1349
|
+
videoThumbHeight: carouselVidDims.height,
|
|
1350
|
+
},
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
1354
|
+
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
1355
|
+
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
454
1356
|
|
|
1357
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
1358
|
+
const titleVarCountForResolve = isMediaTypeText
|
|
1359
|
+
? 0
|
|
1360
|
+
: ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
|
|
1361
|
+
const resolvedTitle = isMediaTypeText
|
|
1362
|
+
? ''
|
|
1363
|
+
: (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
|
|
1364
|
+
const resolvedDesc = isSlotMappingMode
|
|
1365
|
+
? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
|
|
1366
|
+
: templateDesc;
|
|
1367
|
+
|
|
1368
|
+
const mediaPreview = {};
|
|
1369
|
+
if (isMediaTypeImage && rcsImageSrc) {
|
|
1370
|
+
mediaPreview.rcsImageSrc = rcsImageSrc;
|
|
1371
|
+
}
|
|
1372
|
+
if (isMediaTypeVideo && !isMediaTypeText) {
|
|
1373
|
+
if (rcsThumbnailSrc) {
|
|
1374
|
+
mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
|
|
1375
|
+
} else if (rcsVideoSrc?.videoSrc) {
|
|
1376
|
+
mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
|
|
1377
|
+
}
|
|
1378
|
+
if (rcsThumbnailSrc) {
|
|
1379
|
+
mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const contentObj = {
|
|
1384
|
+
templateHeader: resolvedTitle,
|
|
1385
|
+
templateMessage: resolvedDesc,
|
|
1386
|
+
...mediaPreview,
|
|
1387
|
+
...(suggestions.length > 0 && {
|
|
1388
|
+
suggestions: suggestions,
|
|
1389
|
+
}),
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
return contentObj;
|
|
1393
|
+
}, [
|
|
1394
|
+
templateMediaType,
|
|
1395
|
+
templateTitle,
|
|
1396
|
+
templateDesc,
|
|
1397
|
+
rcsImageSrc,
|
|
1398
|
+
rcsVideoSrc,
|
|
1399
|
+
rcsThumbnailSrc,
|
|
1400
|
+
suggestions,
|
|
1401
|
+
selectedDimension,
|
|
1402
|
+
isFullMode,
|
|
1403
|
+
isEditFlow,
|
|
1404
|
+
cardVarMapped,
|
|
1405
|
+
rcsSpanningSemanticVarNames,
|
|
1406
|
+
carouselData,
|
|
1407
|
+
selectedCarouselHeight,
|
|
1408
|
+
selectedCarouselWidth,
|
|
1409
|
+
]);
|
|
1410
|
+
|
|
1411
|
+
const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
|
|
455
1412
|
|
|
456
|
-
const RcsLabel = styled.div`
|
|
457
|
-
display: flex;
|
|
458
|
-
margin-top: 20px;
|
|
459
|
-
`;
|
|
460
1413
|
const paramObj = params || {};
|
|
461
1414
|
useEffect(() => {
|
|
462
1415
|
const { id } = paramObj;
|
|
463
1416
|
if (id && isFullMode) {
|
|
464
1417
|
setSpin(true);
|
|
465
1418
|
actions.getTemplateDetails(id, setSpin);
|
|
1419
|
+
} else if (!id && isFullMode) {
|
|
1420
|
+
// Create New: clear standalone media and ALL possible carousel card Redux slots.
|
|
1421
|
+
// Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
|
|
1422
|
+
updateRcsImageSrc('');
|
|
1423
|
+
setRcsVideoSrc({});
|
|
1424
|
+
setRcsThumbnailSrc('');
|
|
1425
|
+
setAssetList({});
|
|
1426
|
+
setEditFlow(false);
|
|
1427
|
+
actions.clearRcsMediaAsset(0);
|
|
1428
|
+
actions.clearRcsMediaAsset(1);
|
|
1429
|
+
for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
|
|
1430
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
|
|
1431
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
|
|
1432
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
|
|
1433
|
+
}
|
|
1434
|
+
setCarouselData([]);
|
|
1435
|
+
setCarouselErrors([]);
|
|
466
1436
|
}
|
|
467
1437
|
return () => {
|
|
468
1438
|
actions.clearEditResponse();
|
|
@@ -470,59 +1440,68 @@ export const Rcs = (props) => {
|
|
|
470
1440
|
}, [paramObj.id]);
|
|
471
1441
|
|
|
472
1442
|
useEffect(() => {
|
|
473
|
-
|
|
1443
|
+
if (!(isEditFlow || !isFullMode)) return;
|
|
474
1444
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1445
|
+
const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
|
|
1446
|
+
|
|
1447
|
+
const initField = (targetString, setVarMap, slotOffset) => {
|
|
1448
|
+
const arr = splitTemplateVarStringRcs(targetString);
|
|
1449
|
+
if (!arr?.length) {
|
|
1450
|
+
setVarMap({});
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const nextVarMap = {};
|
|
1454
|
+
let varOrdinal = 0;
|
|
1455
|
+
arr.forEach((elem, idx) => {
|
|
1456
|
+
// Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
|
|
1457
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
1458
|
+
const id = `${elem}_${idx}`;
|
|
1459
|
+
const varName = getVarNameFromToken(elem);
|
|
1460
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
1461
|
+
varOrdinal += 1;
|
|
1462
|
+
const mappedValue = resolveCardVarMappedSlotValue(
|
|
1463
|
+
cardVarMapped,
|
|
1464
|
+
varName,
|
|
1465
|
+
globalSlot,
|
|
1466
|
+
isEditLike,
|
|
1467
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
1468
|
+
);
|
|
1469
|
+
nextVarMap[id] = mappedValue;
|
|
481
1470
|
}
|
|
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
|
-
};
|
|
1471
|
+
});
|
|
1472
|
+
setVarMap(nextVarMap);
|
|
1473
|
+
};
|
|
501
1474
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1475
|
+
initField(templateTitle, setTitleVarMappedData, 0);
|
|
1476
|
+
initField(templateDesc, setDescVarMappedData, titleTokenCount);
|
|
1477
|
+
}, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
|
|
1478
|
+
|
|
1479
|
+
useEffect(() => {
|
|
1480
|
+
if (!isEditFlow && isFullMode) {
|
|
508
1481
|
setRcsVideoSrc({});
|
|
509
1482
|
updateRcsImageSrc('');
|
|
510
|
-
|
|
511
|
-
updateRcsThumbnailSrc('');
|
|
1483
|
+
setRcsThumbnailSrc('');
|
|
512
1484
|
setAssetList({});
|
|
513
|
-
|
|
514
|
-
|
|
1485
|
+
}
|
|
1486
|
+
}, [templateMediaType]);
|
|
515
1487
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1488
|
+
/** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
|
|
1489
|
+
const templateStatusHelper = (cardContentFirst) => {
|
|
1490
|
+
const raw =
|
|
1491
|
+
cardContentFirst?.Status
|
|
1492
|
+
?? cardContentFirst?.status
|
|
1493
|
+
?? cardContentFirst?.approvalStatus
|
|
1494
|
+
?? '';
|
|
1495
|
+
const status = typeof raw === 'string' ? raw.trim() : String(raw);
|
|
1496
|
+
const n = status.toLowerCase();
|
|
1497
|
+
switch (n) {
|
|
519
1498
|
case RCS_STATUSES.approved:
|
|
520
1499
|
setTemplateStatus(RCS_STATUSES.approved);
|
|
521
1500
|
break;
|
|
522
1501
|
case RCS_STATUSES.pending:
|
|
523
1502
|
setTemplateStatus(RCS_STATUSES.pending);
|
|
524
1503
|
break;
|
|
525
|
-
case RCS_STATUSES.awaitingApproval:
|
|
1504
|
+
case RCS_STATUSES.awaitingApproval.toLowerCase():
|
|
526
1505
|
setTemplateStatus(RCS_STATUSES.awaitingApproval);
|
|
527
1506
|
break;
|
|
528
1507
|
case RCS_STATUSES.unavailable:
|
|
@@ -531,6 +1510,9 @@ export const Rcs = (props) => {
|
|
|
531
1510
|
case RCS_STATUSES.rejected:
|
|
532
1511
|
setTemplateStatus(RCS_STATUSES.rejected);
|
|
533
1512
|
break;
|
|
1513
|
+
case RCS_STATUSES.created:
|
|
1514
|
+
setTemplateStatus(RCS_STATUSES.created);
|
|
1515
|
+
break;
|
|
534
1516
|
default:
|
|
535
1517
|
setTemplateStatus(status);
|
|
536
1518
|
break;
|
|
@@ -584,48 +1566,357 @@ export const Rcs = (props) => {
|
|
|
584
1566
|
};
|
|
585
1567
|
|
|
586
1568
|
useEffect(() => {
|
|
587
|
-
const details =
|
|
1569
|
+
const details = rcsHydrationDetails;
|
|
588
1570
|
if (details && Object.keys(details).length > 0) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1571
|
+
// Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
|
|
1572
|
+
const cardFromVersions = get(
|
|
1573
|
+
details,
|
|
1574
|
+
'versions.base.content.RCS.rcsContent.cardContent[0]',
|
|
1575
|
+
);
|
|
1576
|
+
const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
|
|
1577
|
+
const cardType = (rcsContent?.cardType || '').toString();
|
|
1578
|
+
|
|
1579
|
+
setEditFlow(true);
|
|
1580
|
+
setTemplateName(details?.name || details?.creativeName || '');
|
|
1581
|
+
|
|
1582
|
+
const cardFromTop = get(details, 'rcsContent.cardContent[0]');
|
|
1583
|
+
const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
|
|
1584
|
+
const cardVarMappedFromCardContent =
|
|
1585
|
+
card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
|
|
1586
|
+
? card0.cardVarMapped
|
|
1587
|
+
: {};
|
|
1588
|
+
const cardVarMappedFromRootMirror =
|
|
1589
|
+
details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
|
|
1590
|
+
? details.rcsCardVarMapped
|
|
1591
|
+
: {};
|
|
1592
|
+
// Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
|
|
1593
|
+
// nested versions.cardContent[0].cardVarMapped is dropped on reload.
|
|
1594
|
+
const mergedCardVarMappedFromPayload = {
|
|
1595
|
+
...cardVarMappedFromRootMirror,
|
|
1596
|
+
...cardVarMappedFromCardContent,
|
|
1597
|
+
};
|
|
1598
|
+
const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
|
|
1599
|
+
const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
|
|
1600
|
+
const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
|
|
1601
|
+
Object.keys(mergedCardVarMappedFromPayload)
|
|
1602
|
+
.sort()
|
|
1603
|
+
.reduce((accumulator, mapKey) => {
|
|
1604
|
+
accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
|
|
1605
|
+
return accumulator;
|
|
1606
|
+
}, {}),
|
|
1607
|
+
)}`;
|
|
1608
|
+
if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
|
|
1609
|
+
lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
|
|
1610
|
+
setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
|
|
592
1611
|
}
|
|
593
|
-
const
|
|
594
|
-
|
|
1612
|
+
const tokenListForMap = [
|
|
1613
|
+
...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
|
|
1614
|
+
...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
|
|
1615
|
+
];
|
|
1616
|
+
const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
|
|
1617
|
+
// Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
|
|
1618
|
+
// getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
|
|
1619
|
+
const cardVarMappedBeforeCoalesce = isFullMode
|
|
1620
|
+
? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
|
|
1621
|
+
: { ...mergedCardVarMappedFromPayload };
|
|
1622
|
+
const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
|
|
1623
|
+
cardVarMappedBeforeCoalesce,
|
|
1624
|
+
loadedTitleForMap,
|
|
1625
|
+
loadedDescForMap,
|
|
1626
|
+
rcsVarRegex,
|
|
1627
|
+
);
|
|
1628
|
+
const cardVarMappedAfterNumericSlotSync = !isFullMode
|
|
1629
|
+
? syncCardVarMappedSemanticsFromSlots(
|
|
1630
|
+
cardVarMappedAfterCoalesce,
|
|
1631
|
+
loadedTitleForMap,
|
|
1632
|
+
loadedDescForMap,
|
|
1633
|
+
rcsVarRegex,
|
|
1634
|
+
)
|
|
1635
|
+
: cardVarMappedAfterCoalesce;
|
|
1636
|
+
const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
|
|
1637
|
+
// Pre-populate variable/tag mappings while opening an existing template in edit flows
|
|
1638
|
+
setCardVarMapped((previousVarMapState) => {
|
|
1639
|
+
const previousVarMap = previousVarMapState ?? {};
|
|
1640
|
+
if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
|
|
1641
|
+
const previousVarMapKeys = Object.keys(previousVarMap);
|
|
1642
|
+
const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
|
|
1643
|
+
if (previousVarMapKeys.length === nextVarMapKeys.length) {
|
|
1644
|
+
const allSlotValuesMatchPrevious = previousVarMapKeys.every(
|
|
1645
|
+
(key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
|
|
1646
|
+
);
|
|
1647
|
+
if (allSlotValuesMatchPrevious) return previousVarMapState;
|
|
1648
|
+
}
|
|
1649
|
+
return hydratedCardVarMappedResult;
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
if (cardType.toUpperCase() === contentType.carousel) {
|
|
1653
|
+
|
|
1654
|
+
setTemplateType(contentType.carousel);
|
|
1655
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1656
|
+
// Library/campaign payloads can store cardSettings/cardContent at the flat top-level
|
|
1657
|
+
// `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
|
|
1658
|
+
// used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
|
|
1659
|
+
// silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
|
|
1660
|
+
// actually uploaded at and causing object-fit:cover to crop the preview image.
|
|
1661
|
+
const cardSettings = rcsContent?.cardSettings
|
|
1662
|
+
|| get(details, 'rcsContent.cardSettings', {});
|
|
1663
|
+
const cardWidth = cardSettings?.cardWidth || SMALL;
|
|
1664
|
+
setSelectedCarouselWidth(cardWidth);
|
|
1665
|
+
|
|
1666
|
+
const cards = Array.isArray(rcsContent?.cardContent)
|
|
1667
|
+
? rcsContent.cardContent
|
|
1668
|
+
: get(details, 'rcsContent.cardContent', []);
|
|
1669
|
+
const firstHeight = cards?.[0]?.media?.height || MEDIUM;
|
|
1670
|
+
setSelectedCarouselHeight(firstHeight);
|
|
1671
|
+
setSelectedCarousel(`${firstHeight}_${cardWidth}`);
|
|
1672
|
+
setActiveCarouselIndex('0');
|
|
1673
|
+
|
|
1674
|
+
const hydratedCards = cards.map((c = {}, idx) => {
|
|
1675
|
+
const mediaType = c.mediaType;
|
|
1676
|
+
const media = c.media || {};
|
|
1677
|
+
const mediaUrl = media.mediaUrl || '';
|
|
1678
|
+
const thumbUrl = media.thumbnailUrl || '';
|
|
1679
|
+
const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
|
|
1680
|
+
const suggestions = idx === 0 && rawSuggestions.length === 0
|
|
1681
|
+
? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
|
|
1682
|
+
: rawSuggestions;
|
|
1683
|
+
return {
|
|
1684
|
+
title: c.title || '',
|
|
1685
|
+
description: c.description || '',
|
|
1686
|
+
mediaType,
|
|
1687
|
+
imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
|
|
1688
|
+
videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
|
|
1689
|
+
videoSrc: mediaUrl,
|
|
1690
|
+
previewUrl: thumbUrl,
|
|
1691
|
+
videoThumbnail: thumbUrl,
|
|
1692
|
+
videoName: c?.media?.videoName || '',
|
|
1693
|
+
} : {},
|
|
1694
|
+
thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
|
|
1695
|
+
suggestions,
|
|
1696
|
+
};
|
|
1697
|
+
});
|
|
1698
|
+
setCarouselData(
|
|
1699
|
+
hydratedCards.length > 0
|
|
1700
|
+
? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
|
|
1701
|
+
: [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
|
|
1702
|
+
);
|
|
1703
|
+
setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
|
|
1704
|
+
|
|
1705
|
+
// Status bar uses first card's Status, keep existing behavior.
|
|
1706
|
+
if (isHostInfoBip) {
|
|
1707
|
+
setTemplateStatus('');
|
|
1708
|
+
} else {
|
|
1709
|
+
const firstCard = cards?.[0] || {};
|
|
1710
|
+
const cardForCarouselStatus = {
|
|
1711
|
+
...firstCard,
|
|
1712
|
+
Status:
|
|
1713
|
+
firstCard.Status
|
|
1714
|
+
?? firstCard.status
|
|
1715
|
+
?? firstCard.approvalStatus
|
|
1716
|
+
?? get(details, 'templateStatus')
|
|
1717
|
+
?? get(details, 'approvalStatus')
|
|
1718
|
+
?? get(details, 'creativeStatus')
|
|
1719
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1720
|
+
?? '',
|
|
1721
|
+
};
|
|
1722
|
+
templateStatusHelper(cardForCarouselStatus);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// Hydrate SMS fallback for carousel — same logic as for non-carousel types below.
|
|
1726
|
+
// Without this the early `return` skips fallback hydration entirely, so reopening
|
|
1727
|
+
// a carousel template never restores the saved SMS fallback data.
|
|
1728
|
+
const carouselSmsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
|
|
1729
|
+
const carouselSmsBase = get(carouselSmsFallbackContent, 'versions.base', {});
|
|
1730
|
+
const carouselUpdatedEditor = carouselSmsBase['updated-sms-editor'] ?? carouselSmsBase['sms-editor'];
|
|
1731
|
+
const carouselSmsEditor = carouselSmsBase['sms-editor'];
|
|
1732
|
+
const carouselFromNested = Array.isArray(carouselUpdatedEditor)
|
|
1733
|
+
? carouselUpdatedEditor.join('')
|
|
1734
|
+
: (typeof carouselUpdatedEditor === 'string' ? carouselUpdatedEditor : (carouselSmsEditor || ''));
|
|
1735
|
+
const carouselFallbackMessage = carouselSmsFallbackContent.smsContent
|
|
1736
|
+
|| carouselSmsFallbackContent.smsTemplateContent
|
|
1737
|
+
|| carouselSmsFallbackContent.message
|
|
1738
|
+
|| carouselFromNested
|
|
1739
|
+
|| '';
|
|
1740
|
+
const carouselVarMappedFromPayload = carouselSmsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
|
|
1741
|
+
const carouselHasVarMapped = Object.keys(carouselVarMappedFromPayload).length > 0;
|
|
1742
|
+
const carouselHasFallbackPayload =
|
|
1743
|
+
carouselSmsFallbackContent
|
|
1744
|
+
&& Object.keys(carouselSmsFallbackContent).length > 0
|
|
1745
|
+
&& (
|
|
1746
|
+
!!carouselSmsFallbackContent.smsTemplateName
|
|
1747
|
+
|| !!carouselFallbackMessage
|
|
1748
|
+
|| carouselHasVarMapped
|
|
1749
|
+
);
|
|
1750
|
+
if (carouselHasFallbackPayload) {
|
|
1751
|
+
const carouselUnicodeFromApi =
|
|
1752
|
+
typeof carouselSmsFallbackContent.unicodeValidity === 'boolean'
|
|
1753
|
+
? carouselSmsFallbackContent.unicodeValidity
|
|
1754
|
+
: (typeof carouselSmsBase['unicode-validity'] === 'boolean' ? carouselSmsBase['unicode-validity'] : true);
|
|
1755
|
+
const carouselRegisteredSenderIds =
|
|
1756
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord(carouselSmsFallbackContent);
|
|
1757
|
+
const carouselNextSmsState = {
|
|
1758
|
+
templateName: carouselSmsFallbackContent.smsTemplateName || '',
|
|
1759
|
+
content: carouselFallbackMessage,
|
|
1760
|
+
templateContent: carouselFallbackMessage,
|
|
1761
|
+
unicodeValidity: carouselUnicodeFromApi,
|
|
1762
|
+
...(carouselHasVarMapped && { rcsSmsFallbackVarMapped: carouselVarMappedFromPayload }),
|
|
1763
|
+
...(Array.isArray(carouselRegisteredSenderIds) && carouselRegisteredSenderIds.length > 0
|
|
1764
|
+
? { registeredSenderIds: carouselRegisteredSenderIds }
|
|
1765
|
+
: {}),
|
|
1766
|
+
};
|
|
1767
|
+
const carouselHydrationKey = JSON.stringify({
|
|
1768
|
+
creativeKey: details._id || details.name || details.creativeName || '',
|
|
1769
|
+
templateName: carouselNextSmsState.templateName,
|
|
1770
|
+
content: carouselNextSmsState.content,
|
|
1771
|
+
unicodeValidity: carouselNextSmsState.unicodeValidity,
|
|
1772
|
+
varMapped: carouselNextSmsState.rcsSmsFallbackVarMapped || {},
|
|
1773
|
+
senderIds: Array.isArray(carouselRegisteredSenderIds)
|
|
1774
|
+
? carouselRegisteredSenderIds.join('')
|
|
1775
|
+
: '',
|
|
1776
|
+
});
|
|
1777
|
+
if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== carouselHydrationKey) {
|
|
1778
|
+
lastSmsFallbackHydrationKeyRef.current = carouselHydrationKey;
|
|
1779
|
+
setSmsFallbackData(carouselNextSmsState);
|
|
1780
|
+
}
|
|
1781
|
+
} else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
|
|
1782
|
+
lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
|
|
1783
|
+
setSmsFallbackData(null);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
const mediaType =
|
|
1790
|
+
card0.mediaType
|
|
1791
|
+
|| get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
|
|
1792
|
+
if (cardType.toUpperCase() !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
|
|
595
1793
|
setTemplateType(contentType.text_message);
|
|
596
|
-
} else {
|
|
1794
|
+
} else if (cardType.toUpperCase() !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
|
|
597
1795
|
setTemplateType(contentType.rich_card);
|
|
598
1796
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1797
|
+
|
|
1798
|
+
const loadedTitle = loadedTitleForMap;
|
|
1799
|
+
const loadedDesc = loadedDescForMap;
|
|
1800
|
+
const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
|
|
1801
|
+
loadedTitle,
|
|
1802
|
+
loadedDesc,
|
|
1803
|
+
isFullMode,
|
|
1804
|
+
cardVarMappedAfterHydration: hydratedCardVarMappedResult,
|
|
1805
|
+
rcsVarRegex,
|
|
1806
|
+
});
|
|
606
1807
|
setTemplateTitle(normalizedTitle);
|
|
607
1808
|
setTemplateDesc(normalizedDesc);
|
|
608
|
-
setSuggestions(
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1809
|
+
setSuggestions(
|
|
1810
|
+
Array.isArray(card0.suggestions)
|
|
1811
|
+
? card0.suggestions
|
|
1812
|
+
: get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
|
|
1813
|
+
);
|
|
1814
|
+
const cardForStatus = {
|
|
1815
|
+
...card0,
|
|
1816
|
+
Status:
|
|
1817
|
+
card0.Status
|
|
1818
|
+
?? card0.status
|
|
1819
|
+
?? card0.approvalStatus
|
|
1820
|
+
?? get(details, 'templateStatus')
|
|
1821
|
+
?? get(details, 'approvalStatus')
|
|
1822
|
+
?? get(details, 'creativeStatus')
|
|
1823
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1824
|
+
?? '',
|
|
1825
|
+
};
|
|
1826
|
+
if (isHostInfoBip) {
|
|
1827
|
+
setTemplateStatus('');
|
|
1828
|
+
} else {
|
|
1829
|
+
templateStatusHelper(cardForStatus);
|
|
1830
|
+
}
|
|
1831
|
+
const mediaData =
|
|
1832
|
+
card0.media != null && card0.media !== ''
|
|
1833
|
+
? card0.media
|
|
1834
|
+
: get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
|
|
1835
|
+
const cardSettings =
|
|
1836
|
+
get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
|
|
1837
|
+
|| get(details, 'rcsContent.cardSettings', '');
|
|
612
1838
|
setMediaData(mediaData, mediaType, cardSettings);
|
|
613
1839
|
if (details?.edit) {
|
|
614
1840
|
const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
|
|
615
1841
|
setRcsAccount(rcsAccountId);
|
|
616
1842
|
}
|
|
1843
|
+
|
|
1844
|
+
const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
|
|
1845
|
+
const base = get(smsFallbackContent, 'versions.base', {});
|
|
1846
|
+
const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
|
|
1847
|
+
const smsEditor = base['sms-editor'];
|
|
1848
|
+
const fromNested = Array.isArray(updatedEditor)
|
|
1849
|
+
? updatedEditor.join('')
|
|
1850
|
+
: (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
|
|
1851
|
+
const fallbackMessage = smsFallbackContent.smsContent
|
|
1852
|
+
|| smsFallbackContent.smsTemplateContent
|
|
1853
|
+
|| smsFallbackContent.message
|
|
1854
|
+
|| fromNested
|
|
1855
|
+
|| '';
|
|
1856
|
+
const varMappedFromPayload = smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
|
|
1857
|
+
const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
|
|
1858
|
+
const hasFallbackPayload =
|
|
1859
|
+
smsFallbackContent
|
|
1860
|
+
&& Object.keys(smsFallbackContent).length > 0
|
|
1861
|
+
&& (
|
|
1862
|
+
!!smsFallbackContent.smsTemplateName
|
|
1863
|
+
|| !!fallbackMessage
|
|
1864
|
+
|| hasVarMapped
|
|
1865
|
+
);
|
|
1866
|
+
if (hasFallbackPayload) {
|
|
1867
|
+
if (!fallbackMessage && !hasVarMapped && process.env.NODE_ENV !== 'production') {
|
|
1868
|
+
console.warn('[RCS SMS Fallback] No message text found in API response. Inspect shape:', smsFallbackContent);
|
|
1869
|
+
}
|
|
1870
|
+
const unicodeFromApi =
|
|
1871
|
+
typeof smsFallbackContent.unicodeValidity === 'boolean'
|
|
1872
|
+
? smsFallbackContent.unicodeValidity
|
|
1873
|
+
: (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
|
|
1874
|
+
const registeredSenderIdsFromApi =
|
|
1875
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
|
|
1876
|
+
const nextSmsState = {
|
|
1877
|
+
templateName: smsFallbackContent.smsTemplateName || '',
|
|
1878
|
+
content: fallbackMessage,
|
|
1879
|
+
templateContent: fallbackMessage,
|
|
1880
|
+
unicodeValidity: unicodeFromApi,
|
|
1881
|
+
...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
|
|
1882
|
+
...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
|
|
1883
|
+
? { registeredSenderIds: registeredSenderIdsFromApi }
|
|
1884
|
+
: {}),
|
|
1885
|
+
};
|
|
1886
|
+
const hydrationKey = JSON.stringify({
|
|
1887
|
+
creativeKey: details._id || details.name || details.creativeName || '',
|
|
1888
|
+
templateName: nextSmsState.templateName,
|
|
1889
|
+
content: nextSmsState.content,
|
|
1890
|
+
unicodeValidity: nextSmsState.unicodeValidity,
|
|
1891
|
+
varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
|
|
1892
|
+
senderIds:
|
|
1893
|
+
Array.isArray(registeredSenderIdsFromApi)
|
|
1894
|
+
? registeredSenderIdsFromApi.join('\u001f')
|
|
1895
|
+
: '',
|
|
1896
|
+
});
|
|
1897
|
+
if (
|
|
1898
|
+
isFullMode
|
|
1899
|
+
|| lastSmsFallbackHydrationKeyRef.current !== hydrationKey
|
|
1900
|
+
) {
|
|
1901
|
+
lastSmsFallbackHydrationKeyRef.current = hydrationKey;
|
|
1902
|
+
setSmsFallbackData(nextSmsState);
|
|
1903
|
+
}
|
|
1904
|
+
} else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
|
|
1905
|
+
lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
|
|
1906
|
+
setSmsFallbackData(null);
|
|
1907
|
+
}
|
|
617
1908
|
}
|
|
618
|
-
}, [
|
|
619
|
-
|
|
1909
|
+
}, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
|
|
620
1910
|
|
|
621
1911
|
useEffect(() => {
|
|
622
1912
|
if (templateType === contentType.text_message) {
|
|
623
1913
|
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
624
|
-
|
|
625
|
-
|
|
1914
|
+
// Full-mode create only: switching to plain text clears draft title/media. Never clear when
|
|
1915
|
+
// hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
|
|
626
1916
|
if (!isEditFlow && isFullMode) {
|
|
1917
|
+
setTemplateTitle('');
|
|
1918
|
+
setTemplateTitleError('');
|
|
627
1919
|
setUpdateRcsImageSrc('');
|
|
628
|
-
setUpdateRcsVideoSrc({});
|
|
629
1920
|
setRcsVideoSrc({});
|
|
630
1921
|
setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
|
|
631
1922
|
}
|
|
@@ -652,7 +1943,8 @@ export const Rcs = (props) => {
|
|
|
652
1943
|
if (!showDltContainer) {
|
|
653
1944
|
const { type, module } = location.query || {};
|
|
654
1945
|
const isEmbedded = type === EMBEDDED;
|
|
655
|
-
|
|
1946
|
+
// Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
|
|
1947
|
+
const context = isEmbedded ? module : 'outbound';
|
|
656
1948
|
const embedded = isEmbedded ? type : FULL;
|
|
657
1949
|
const query = {
|
|
658
1950
|
layout: SMS,
|
|
@@ -663,9 +1955,9 @@ export const Rcs = (props) => {
|
|
|
663
1955
|
if (getDefaultTags) {
|
|
664
1956
|
query.context = getDefaultTags;
|
|
665
1957
|
}
|
|
666
|
-
|
|
1958
|
+
fetchTagSchemaIfNewQuery(query);
|
|
667
1959
|
}
|
|
668
|
-
}, [showDltContainer]);
|
|
1960
|
+
}, [showDltContainer, fetchTagSchemaIfNewQuery]);
|
|
669
1961
|
|
|
670
1962
|
useEffect(() => {
|
|
671
1963
|
let tag = get(metaEntities, `tags.standard`, []);
|
|
@@ -688,16 +1980,128 @@ export const Rcs = (props) => {
|
|
|
688
1980
|
context,
|
|
689
1981
|
embedded,
|
|
690
1982
|
};
|
|
1983
|
+
if (getDefaultTags) {
|
|
1984
|
+
query.context = getDefaultTags;
|
|
1985
|
+
}
|
|
1986
|
+
fetchTagSchemaIfNewQuery(query);
|
|
691
1987
|
globalActions.fetchSchemaForEntity(query);
|
|
692
1988
|
};
|
|
693
1989
|
|
|
694
|
-
const
|
|
695
|
-
if (!
|
|
696
|
-
const
|
|
1990
|
+
const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
|
|
1991
|
+
if (!templateStr || !numericVarName || !tagName) return templateStr;
|
|
1992
|
+
const re = buildRcsNumericMustachePlaceholderRegex(numericVarName);
|
|
1993
|
+
return templateStr.replace(re, `{{${tagName}}}`);
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
|
|
1997
|
+
if (!varSegmentCompositeDomId) return;
|
|
1998
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
1999
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
2000
|
+
const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
|
|
2001
|
+
if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
|
|
2002
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
2003
|
+
const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
2004
|
+
if (!semanticOrNumericVarName) return;
|
|
2005
|
+
const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
|
|
2006
|
+
const templateStringForField =
|
|
2007
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
|
|
2008
|
+
const titleOrMessageFieldType =
|
|
2009
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
|
|
2010
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
2011
|
+
varSegmentCompositeDomId,
|
|
2012
|
+
templateStringForField,
|
|
2013
|
+
titleOrMessageFieldType,
|
|
2014
|
+
);
|
|
2015
|
+
const cardVarMappedNumericSlotKey =
|
|
2016
|
+
globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
|
|
2017
|
+
? String(globalVarSlotIndexZeroBased + 1)
|
|
2018
|
+
: null;
|
|
2019
|
+
|
|
2020
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
2021
|
+
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
2022
|
+
if (isNumericPlaceholderSlot) {
|
|
2023
|
+
const existingValueBeforeAppend = (
|
|
2024
|
+
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
2025
|
+
).toString();
|
|
2026
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
2027
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
2028
|
+
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
2029
|
+
} else {
|
|
2030
|
+
// Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
|
|
2031
|
+
// "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
|
|
2032
|
+
// read/write the global numeric slot only and drop the shared semantic key.
|
|
2033
|
+
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
2034
|
+
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
2035
|
+
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
2036
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
2037
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
2038
|
+
if (cardVarMappedNumericSlotKey) {
|
|
2039
|
+
updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
|
|
2040
|
+
// Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
|
|
2041
|
+
const fieldOffset = titleOrMessageFieldType === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
2042
|
+
const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
|
|
2043
|
+
let varOrdinal = 0;
|
|
2044
|
+
for (let i = 0; i < fieldSegments.length; i++) {
|
|
2045
|
+
if (rcsVarTestRegex.test(fieldSegments[i])) {
|
|
2046
|
+
const slotVarName = getVarNameFromToken(fieldSegments[i]);
|
|
2047
|
+
const globalSlot = fieldOffset + varOrdinal;
|
|
2048
|
+
varOrdinal += 1;
|
|
2049
|
+
if (slotVarName === semanticOrNumericVarName && globalSlot !== globalVarSlotIndexZeroBased) {
|
|
2050
|
+
updatedCardVarMapped[String(globalSlot + 1)] = mappedValueAfterAppendingTag;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
} else {
|
|
2055
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
return updatedCardVarMapped;
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
if (
|
|
2062
|
+
isNumericPlaceholderSlot
|
|
2063
|
+
&& (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
|
|
2064
|
+
) {
|
|
2065
|
+
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
2066
|
+
setTemplateTitle((previousTitle) => {
|
|
2067
|
+
const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
2068
|
+
previousTitle || '',
|
|
2069
|
+
semanticOrNumericVarName,
|
|
2070
|
+
selectedTagNameFromPicker,
|
|
2071
|
+
);
|
|
2072
|
+
if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
|
|
2073
|
+
setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
|
|
2074
|
+
// 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
|
|
2075
|
+
setRcsVarSegmentEditorRemountKey((k) => k + 1);
|
|
2076
|
+
return titleAfterReplacingNumericPlaceholder;
|
|
2077
|
+
});
|
|
2078
|
+
} else {
|
|
2079
|
+
setTemplateDesc((previousDescription) => {
|
|
2080
|
+
const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
2081
|
+
previousDescription || '',
|
|
2082
|
+
semanticOrNumericVarName,
|
|
2083
|
+
selectedTagNameFromPicker,
|
|
2084
|
+
);
|
|
2085
|
+
if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
|
|
2086
|
+
return previousDescription;
|
|
2087
|
+
}
|
|
2088
|
+
setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
|
|
2089
|
+
setRcsVarSegmentEditorRemountKey((k) => k + 1);
|
|
2090
|
+
return descriptionAfterReplacingNumericPlaceholder;
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
|
|
2096
|
+
const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
|
|
2097
|
+
|
|
2098
|
+
const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
|
|
2099
|
+
|
|
2100
|
+
const onCarouselTagSelect = (data) => {
|
|
2101
|
+
if (!carouselFocusedVarId) return;
|
|
2102
|
+
const sep = carouselFocusedVarId.lastIndexOf('_');
|
|
697
2103
|
if (sep === -1) return;
|
|
698
|
-
const
|
|
699
|
-
if (isNaN(numId)) return;
|
|
700
|
-
const token = areaId.slice(0, sep);
|
|
2104
|
+
const token = carouselFocusedVarId.slice(0, sep);
|
|
701
2105
|
const variableName = getVarNameFromToken(token);
|
|
702
2106
|
if (!variableName) return;
|
|
703
2107
|
setCardVarMapped((prev) => {
|
|
@@ -710,10 +2114,6 @@ export const Rcs = (props) => {
|
|
|
710
2114
|
});
|
|
711
2115
|
};
|
|
712
2116
|
|
|
713
|
-
const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
|
|
714
|
-
|
|
715
|
-
const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
|
|
716
|
-
|
|
717
2117
|
const onTagSelectFallback = (data) => {
|
|
718
2118
|
const tempMsg = `${fallbackMessage}{{${data}}}`;
|
|
719
2119
|
const error = fallbackMessageErrorHandler(tempMsg);
|
|
@@ -733,15 +2133,14 @@ export const Rcs = (props) => {
|
|
|
733
2133
|
};
|
|
734
2134
|
// tag Code end
|
|
735
2135
|
|
|
736
|
-
const renderLabel = (value,
|
|
737
|
-
const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
|
|
2136
|
+
const renderLabel = (value, desc) => {
|
|
738
2137
|
return (
|
|
739
2138
|
<>
|
|
740
|
-
<
|
|
2139
|
+
<div className="rcs-form-section-heading">
|
|
741
2140
|
<CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
|
|
742
|
-
</
|
|
2141
|
+
</div>
|
|
743
2142
|
{desc && (
|
|
744
|
-
<CapLabel type="label3"
|
|
2143
|
+
<CapLabel type="label3" className="rcs-form-field-caption">
|
|
745
2144
|
{formatMessage(messages[desc])}
|
|
746
2145
|
</CapLabel>
|
|
747
2146
|
)}
|
|
@@ -760,13 +2159,8 @@ export const Rcs = (props) => {
|
|
|
760
2159
|
},
|
|
761
2160
|
{
|
|
762
2161
|
value: contentType.carousel,
|
|
763
|
-
label: (
|
|
764
|
-
|
|
765
|
-
{formatMessage(messages.carousel)}
|
|
766
|
-
</CapTooltip>
|
|
767
|
-
),
|
|
768
|
-
disabled: true,
|
|
769
|
-
},
|
|
2162
|
+
label: formatMessage(messages.carousel),
|
|
2163
|
+
}
|
|
770
2164
|
];
|
|
771
2165
|
|
|
772
2166
|
const onTemplateNameChange = ({ target: { value } }) => {
|
|
@@ -777,6 +2171,10 @@ export const Rcs = (props) => {
|
|
|
777
2171
|
|
|
778
2172
|
const onTemplateTypeChange = ({ target: { value } }) => {
|
|
779
2173
|
setTemplateType(value);
|
|
2174
|
+
// Carousel has per-card media; keep template-level media type neutral.
|
|
2175
|
+
if (value === contentType.carousel) {
|
|
2176
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
2177
|
+
}
|
|
780
2178
|
};
|
|
781
2179
|
|
|
782
2180
|
|
|
@@ -797,8 +2195,39 @@ export const Rcs = (props) => {
|
|
|
797
2195
|
const onTemplateMediaTypeChange = ({ target: { value } }) => {
|
|
798
2196
|
setTemplateMediaType(value);
|
|
799
2197
|
};
|
|
800
|
-
|
|
801
|
-
|
|
2198
|
+
const renderedRCSEditMessage = (descArray, type) => {
|
|
2199
|
+
const renderArray = [];
|
|
2200
|
+
if (descArray?.length) {
|
|
2201
|
+
descArray.forEach((elem, index) => {
|
|
2202
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
2203
|
+
// Variable input
|
|
2204
|
+
renderArray.push(
|
|
2205
|
+
<TextArea
|
|
2206
|
+
id={`${elem}_${index}`}
|
|
2207
|
+
key={`${elem}_${index}`}
|
|
2208
|
+
placeholder={`enter the value for ${elem}`}
|
|
2209
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2210
|
+
onChange={e => textAreaValueChange(e, type)}
|
|
2211
|
+
value={textAreaValue(index, type)}
|
|
2212
|
+
onFocus={(e) => setTextAreaId(e, type)}
|
|
2213
|
+
/>
|
|
2214
|
+
);
|
|
2215
|
+
} else if (elem) {
|
|
2216
|
+
// Static text
|
|
2217
|
+
renderArray.push(
|
|
2218
|
+
<TextArea
|
|
2219
|
+
key={`static_${index}`}
|
|
2220
|
+
value={elem}
|
|
2221
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2222
|
+
disabled
|
|
2223
|
+
className="rcs-edit-template-message-static-textarea"
|
|
2224
|
+
/>
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
return renderArray;
|
|
2230
|
+
};
|
|
802
2231
|
const onTemplateTitleChange = ({ target: { value } }) => {
|
|
803
2232
|
let errorMessage = false;
|
|
804
2233
|
if (templateType === contentType.rich_card && !value.trim()) {
|
|
@@ -814,7 +2243,7 @@ export const Rcs = (props) => {
|
|
|
814
2243
|
|
|
815
2244
|
const onTemplateDescChange = ({ target: { value } }) => {
|
|
816
2245
|
let errorMessage = false;
|
|
817
|
-
if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
|
|
2246
|
+
if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
|
|
818
2247
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
819
2248
|
} else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
|
|
820
2249
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
@@ -830,16 +2259,16 @@ export const Rcs = (props) => {
|
|
|
830
2259
|
|
|
831
2260
|
const templateDescErrorHandler = (value) => {
|
|
832
2261
|
let errorMessage = false;
|
|
833
|
-
const { isBraceError } = validateTags({
|
|
2262
|
+
const { unsupportedTags, isBraceError } = validateTags({
|
|
834
2263
|
content: value,
|
|
835
2264
|
tagsParam: tags,
|
|
2265
|
+
injectedTagsParams: injectedTags,
|
|
836
2266
|
location,
|
|
837
2267
|
tagModule: getDefaultTags,
|
|
838
|
-
isFullMode,
|
|
839
2268
|
}) || {};
|
|
840
2269
|
|
|
841
2270
|
const maxLength = templateType === contentType.text_message
|
|
842
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH
|
|
2271
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
|
|
843
2272
|
: RCS_RICH_CARD_MAX_LENGTH;
|
|
844
2273
|
|
|
845
2274
|
if (value === '' && isMediaTypeText) {
|
|
@@ -862,10 +2291,25 @@ export const Rcs = (props) => {
|
|
|
862
2291
|
};
|
|
863
2292
|
|
|
864
2293
|
const fallbackMessageErrorHandler = (value) => {
|
|
2294
|
+
let errorMessage = false;
|
|
2295
|
+
const { unsupportedTags } = validateTags({
|
|
2296
|
+
content: value,
|
|
2297
|
+
tagsParam: tags,
|
|
2298
|
+
injectedTagsParams: injectedTags,
|
|
2299
|
+
location,
|
|
2300
|
+
tagModule: getDefaultTags,
|
|
2301
|
+
}) || {};
|
|
865
2302
|
if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
|
|
866
|
-
|
|
2303
|
+
errorMessage = formatMessage(messages.fallbackMsgLenError);
|
|
2304
|
+
} else if (unsupportedTags?.length > 0) {
|
|
2305
|
+
errorMessage = formatMessage(
|
|
2306
|
+
globalMessages.unsupportedTagsValidationError,
|
|
2307
|
+
{
|
|
2308
|
+
unsupportedTags,
|
|
2309
|
+
},
|
|
2310
|
+
);
|
|
867
2311
|
}
|
|
868
|
-
return
|
|
2312
|
+
return errorMessage;
|
|
869
2313
|
};
|
|
870
2314
|
|
|
871
2315
|
// Check for forbidden characters: square brackets [] and single curly braces {}
|
|
@@ -912,53 +2356,43 @@ export const Rcs = (props) => {
|
|
|
912
2356
|
if(!isFullMode){
|
|
913
2357
|
return false;
|
|
914
2358
|
}
|
|
915
|
-
|
|
2359
|
+
// Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
|
|
2360
|
+
if (!/^[\w.]+$/.test(paramName)) {
|
|
916
2361
|
return formatMessage(messages.unknownCharactersError);
|
|
917
2362
|
}
|
|
918
2363
|
}
|
|
919
2364
|
return false;
|
|
920
2365
|
};
|
|
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
2366
|
|
|
949
2367
|
const onMessageAddVar = () => {
|
|
950
|
-
onAddVar(
|
|
2368
|
+
onAddVar(templateDesc);
|
|
951
2369
|
};
|
|
952
2370
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
2371
|
+
/**
|
|
2372
|
+
* Returns the smallest positive integer not already used as a `{{N}}` variable
|
|
2373
|
+
* in either the title or description fields, or null if the limit (19) is reached.
|
|
2374
|
+
* Scans both fields so title and description vars never share the same number
|
|
2375
|
+
* (duplicate numbers would share a cardVarMapped key and bleed values across fields).
|
|
2376
|
+
*/
|
|
2377
|
+
const getNextRcsNumericVarNumber = (titleStr, descStr) => {
|
|
2378
|
+
const allExistingVars = [
|
|
2379
|
+
...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
|
|
2380
|
+
...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
|
|
2381
|
+
];
|
|
2382
|
+
const existingNumbers = allExistingVars.flatMap(v => {
|
|
2383
|
+
const m = v.match(/\d+/);
|
|
2384
|
+
return m ? [parseInt(m[0], 10)] : [];
|
|
2385
|
+
});
|
|
957
2386
|
let nextNumber = 1;
|
|
958
2387
|
while (existingNumbers.includes(nextNumber)) {
|
|
959
2388
|
nextNumber++;
|
|
960
2389
|
}
|
|
961
|
-
|
|
2390
|
+
return nextNumber > 19 ? null : nextNumber;
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
const onAddVar = (messageContent) => {
|
|
2394
|
+
const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
|
|
2395
|
+
if (nextNumber === null) {
|
|
962
2396
|
return;
|
|
963
2397
|
}
|
|
964
2398
|
const nextVar = `{{${nextNumber}}}`;
|
|
@@ -970,15 +2404,13 @@ const onAddVar = (type, messageContent, regex) => {
|
|
|
970
2404
|
};
|
|
971
2405
|
|
|
972
2406
|
const onTitleAddVar = () => {
|
|
973
|
-
//
|
|
2407
|
+
// Scan both title AND description so the new title var number doesn't
|
|
2408
|
+
// duplicate a number already used in the description. Duplicate numeric
|
|
2409
|
+
// names would share the same cardVarMapped semantic key, causing the
|
|
2410
|
+
// description slot to reflect the title slot value and vice-versa.
|
|
974
2411
|
const messageContent = templateTitle;
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
let nextNumber = 1;
|
|
978
|
-
while (existingNumbers.includes(nextNumber)) {
|
|
979
|
-
nextNumber++;
|
|
980
|
-
}
|
|
981
|
-
if (nextNumber > 19) {
|
|
2412
|
+
const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
|
|
2413
|
+
if (nextNumber === null) {
|
|
982
2414
|
return;
|
|
983
2415
|
}
|
|
984
2416
|
const nextVar = `{{${nextNumber}}}`;
|
|
@@ -990,26 +2422,35 @@ const onTitleAddVar = () => {
|
|
|
990
2422
|
setTemplateTitleError(error);
|
|
991
2423
|
};
|
|
992
2424
|
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
2425
|
+
// Carousel: global variables across the whole carousel (all cards, title+body)
|
|
2426
|
+
const getNextCarouselVarToken = () => {
|
|
2427
|
+
const nums = [];
|
|
2428
|
+
(carouselData || []).forEach((c = {}) => {
|
|
2429
|
+
const titleMatches = (c.title || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
|
|
2430
|
+
const descMatches = (c.description || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
|
|
2431
|
+
[...titleMatches, ...descMatches].forEach((tok) => {
|
|
2432
|
+
const n = parseInt((tok.match(/\d+/) || [])[0], 10);
|
|
2433
|
+
if (!Number.isNaN(n)) nums.push(n);
|
|
2434
|
+
});
|
|
2435
|
+
});
|
|
2436
|
+
const existing = new Set(nums);
|
|
2437
|
+
let nextNumber = 1;
|
|
2438
|
+
while (existing.has(nextNumber)) nextNumber++;
|
|
2439
|
+
if (nextNumber > 19) return '';
|
|
2440
|
+
return `{{${nextNumber}}}`;
|
|
2441
|
+
};
|
|
2442
|
+
|
|
2443
|
+
const appendVarToCarouselField = (cardIndex, fieldName) => {
|
|
2444
|
+
const token = getNextCarouselVarToken();
|
|
2445
|
+
if (!token) return;
|
|
2446
|
+
setCarouselData((prev = []) => {
|
|
2447
|
+
const updated = cloneDeep(prev);
|
|
2448
|
+
if (!updated[cardIndex]) return prev;
|
|
2449
|
+
const current = (updated[cardIndex][fieldName] || '').toString();
|
|
2450
|
+
updated[cardIndex][fieldName] = `${current}${token}`;
|
|
2451
|
+
return updated;
|
|
2452
|
+
});
|
|
2453
|
+
};
|
|
1013
2454
|
|
|
1014
2455
|
const textAreaValue = (idValue, type) => {
|
|
1015
2456
|
if (idValue >= 0) {
|
|
@@ -1025,6 +2466,46 @@ const splitTemplateVarString = (str) => {
|
|
|
1025
2466
|
return "";
|
|
1026
2467
|
};
|
|
1027
2468
|
|
|
2469
|
+
// Carousel: render variable-value editor for a given template string (title/description).
|
|
2470
|
+
// This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
|
|
2471
|
+
const renderCarouselEditMessage = (templateStr) => {
|
|
2472
|
+
const renderArray = [];
|
|
2473
|
+
const templateArr = splitTemplateVarString(templateStr);
|
|
2474
|
+
if (templateArr?.length) {
|
|
2475
|
+
templateArr.forEach((elem, index) => {
|
|
2476
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
2477
|
+
const varName = getVarNameFromToken(elem);
|
|
2478
|
+
renderArray.push(
|
|
2479
|
+
<div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
|
|
2480
|
+
<TextArea
|
|
2481
|
+
id={`${elem}_${index}`}
|
|
2482
|
+
placeholder={`enter the value for ${elem}`}
|
|
2483
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2484
|
+
onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
|
|
2485
|
+
value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
|
|
2486
|
+
onFocus={(e) => {
|
|
2487
|
+
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
2488
|
+
setCarouselFocusedVarId(id);
|
|
2489
|
+
}}
|
|
2490
|
+
/>
|
|
2491
|
+
</div>
|
|
2492
|
+
);
|
|
2493
|
+
} else if (elem) {
|
|
2494
|
+
renderArray.push(
|
|
2495
|
+
<CapHeading
|
|
2496
|
+
key={`static_${index}`}
|
|
2497
|
+
type="h4"
|
|
2498
|
+
className="rcs-edit-template-message-split"
|
|
2499
|
+
>
|
|
2500
|
+
{elem}
|
|
2501
|
+
</CapHeading>
|
|
2502
|
+
);
|
|
2503
|
+
}
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
2507
|
+
};
|
|
2508
|
+
|
|
1028
2509
|
const textAreaValueChange = (e, type) => {
|
|
1029
2510
|
const value = e?.target?.value ?? '';
|
|
1030
2511
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
@@ -1044,7 +2525,9 @@ const splitTemplateVarString = (str) => {
|
|
|
1044
2525
|
};
|
|
1045
2526
|
|
|
1046
2527
|
const setTextAreaId = (e, type) => {
|
|
1047
|
-
|
|
2528
|
+
// VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
|
|
2529
|
+
// have an `.target.id` shape. Support both.
|
|
2530
|
+
const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
|
|
1048
2531
|
if (!id) return;
|
|
1049
2532
|
if (type === TITLE_TEXT) setTitleTextAreaId(id);
|
|
1050
2533
|
else setDescTextAreaId(id);
|
|
@@ -1063,10 +2546,12 @@ const splitTemplateVarString = (str) => {
|
|
|
1063
2546
|
</CapRow>
|
|
1064
2547
|
}
|
|
1065
2548
|
description={
|
|
1066
|
-
|
|
2549
|
+
!isEditFlow && (
|
|
2550
|
+
<CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
|
|
2551
|
+
)
|
|
1067
2552
|
}
|
|
1068
2553
|
/>
|
|
1069
|
-
|
|
2554
|
+
|
|
1070
2555
|
<CapActionButton
|
|
1071
2556
|
buttonType={buttonType}
|
|
1072
2557
|
updateButtonChange={updateButtonChange}
|
|
@@ -1075,44 +2560,85 @@ const splitTemplateVarString = (str) => {
|
|
|
1075
2560
|
isEditFlow={isEditFlow}
|
|
1076
2561
|
isFullMode={isFullMode}
|
|
1077
2562
|
maxButtons={MAX_BUTTONS}
|
|
1078
|
-
|
|
2563
|
+
host={hostName}
|
|
2564
|
+
/>
|
|
1079
2565
|
</>
|
|
1080
2566
|
);
|
|
1081
2567
|
};
|
|
1082
2568
|
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
-
|
|
2569
|
+
const getRcsValueMap = (fieldTemplateString, fieldType) => {
|
|
2570
|
+
if (!fieldTemplateString) return {};
|
|
2571
|
+
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
2572
|
+
const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
|
|
2573
|
+
const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
|
|
2574
|
+
const segmentIdToResolvedValue = {};
|
|
2575
|
+
let varOrdinal = 0;
|
|
2576
|
+
templateSegments.forEach((segmentToken, segmentIndexInField) => {
|
|
2577
|
+
if (rcsVarTestRegex.test(segmentToken)) {
|
|
2578
|
+
const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
|
|
2579
|
+
const varName = getVarNameFromToken(segmentToken);
|
|
2580
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
2581
|
+
varOrdinal += 1;
|
|
2582
|
+
segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
|
|
2583
|
+
cardVarMapped,
|
|
2584
|
+
varName,
|
|
2585
|
+
globalSlot,
|
|
2586
|
+
isEditLike,
|
|
2587
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
});
|
|
2591
|
+
return segmentIdToResolvedValue;
|
|
2592
|
+
};
|
|
2593
|
+
|
|
2594
|
+
const titleVarSegmentValueMapById = useMemo(
|
|
2595
|
+
() => getRcsValueMap(templateTitle, TITLE_TEXT),
|
|
2596
|
+
[templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
2597
|
+
);
|
|
2598
|
+
const descriptionVarSegmentValueMapById = useMemo(
|
|
2599
|
+
() => getRcsValueMap(templateDesc, MESSAGE_TEXT),
|
|
2600
|
+
[templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
2601
|
+
);
|
|
2602
|
+
|
|
2603
|
+
const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
|
|
2604
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
2605
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
2606
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
2607
|
+
const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
2608
|
+
if (variableName === undefined || variableName === null || variableName === '') return;
|
|
2609
|
+
const isInvalidValue = value?.trim() === '';
|
|
2610
|
+
const coercedSlotValue = isInvalidValue ? '' : value;
|
|
2611
|
+
const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
2612
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
2613
|
+
varSegmentCompositeDomId,
|
|
2614
|
+
templateStringForField,
|
|
2615
|
+
type,
|
|
2616
|
+
);
|
|
2617
|
+
const fieldOffset = type === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
2618
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
2619
|
+
const updatedCardVarMapped = { ...previousCardVarMapped };
|
|
2620
|
+
// Remove stale semantic key: keeping it causes every other slot sharing the same
|
|
2621
|
+
// variable name (e.g. {{adv}} in both title and description) to read the same value
|
|
2622
|
+
// via the semantic-key fallback in resolveCardVarMappedSlotValue.
|
|
2623
|
+
delete updatedCardVarMapped[variableName];
|
|
2624
|
+
if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
|
|
2625
|
+
updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
|
|
2626
|
+
// Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
|
|
2627
|
+
const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
|
|
2628
|
+
let varOrdinal = 0;
|
|
2629
|
+
for (let i = 0; i < fieldSegments.length; i++) {
|
|
2630
|
+
if (rcsVarTestRegex.test(fieldSegments[i])) {
|
|
2631
|
+
const slotVarName = getVarNameFromToken(fieldSegments[i]);
|
|
2632
|
+
const globalSlot = fieldOffset + varOrdinal;
|
|
2633
|
+
varOrdinal += 1;
|
|
2634
|
+
if (slotVarName === variableName && globalSlot !== globalVarSlotIndexZeroBased) {
|
|
2635
|
+
updatedCardVarMapped[String(globalSlot + 1)] = coercedSlotValue;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
1112
2638
|
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
|
|
2639
|
+
}
|
|
2640
|
+
return updatedCardVarMapped;
|
|
2641
|
+
});
|
|
1116
2642
|
};
|
|
1117
2643
|
|
|
1118
2644
|
const renderTextComponent = () => {
|
|
@@ -1132,6 +2658,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1132
2658
|
}
|
|
1133
2659
|
suffix={
|
|
1134
2660
|
<>
|
|
2661
|
+
|
|
1135
2662
|
{(isEditFlow || !isFullMode) ? (
|
|
1136
2663
|
<TagList
|
|
1137
2664
|
label={formatMessage(globalMessages.addLabels)}
|
|
@@ -1148,18 +2675,28 @@ const splitTemplateVarString = (str) => {
|
|
|
1148
2675
|
type="flat"
|
|
1149
2676
|
isAddBtn
|
|
1150
2677
|
onClick={onTitleAddVar}
|
|
1151
|
-
disabled={
|
|
2678
|
+
disabled={templateTitleError}
|
|
1152
2679
|
>
|
|
1153
2680
|
{formatMessage(messages.addVar)}
|
|
1154
2681
|
</CapButton>
|
|
1155
|
-
|
|
2682
|
+
)}
|
|
1156
2683
|
</>
|
|
1157
|
-
}
|
|
2684
|
+
}
|
|
2685
|
+
/>
|
|
2686
|
+
|
|
2687
|
+
{(isEditFlow || !isFullMode) ? (
|
|
2688
|
+
<VarSegmentMessageEditor
|
|
2689
|
+
key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
2690
|
+
templateString={templateTitle}
|
|
2691
|
+
valueMap={titleVarSegmentValueMapById}
|
|
2692
|
+
onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
|
|
2693
|
+
onFocus={(id) => setTitleTextAreaId(id)}
|
|
2694
|
+
varRegex={rcsVarRegex}
|
|
2695
|
+
placeholderPrefix=""
|
|
2696
|
+
getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
|
|
1158
2697
|
/>
|
|
1159
|
-
<div className="rcs_text_area_wrapper">
|
|
1160
|
-
{(isEditFlow || !isFullMode) ? (
|
|
1161
|
-
renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
|
|
1162
2698
|
) : (
|
|
2699
|
+
<div className="rcs_text_area_wrapper">
|
|
1163
2700
|
<CapInput
|
|
1164
2701
|
className={`rcs-template-title-input ${
|
|
1165
2702
|
!isTemplateApproved ? "rcs-edit-disabled" : ""
|
|
@@ -1172,8 +2709,8 @@ const splitTemplateVarString = (str) => {
|
|
|
1172
2709
|
errorMessage={templateTitleError}
|
|
1173
2710
|
disabled={isEditFlow || !isFullMode}
|
|
1174
2711
|
/>
|
|
2712
|
+
</div>
|
|
1175
2713
|
)}
|
|
1176
|
-
</div>
|
|
1177
2714
|
{(isEditFlow || !isFullMode) && templateTitleError && (
|
|
1178
2715
|
<CapError className="rcs-template-title-error">
|
|
1179
2716
|
{templateTitleError}
|
|
@@ -1181,7 +2718,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1181
2718
|
)}
|
|
1182
2719
|
{!isEditFlow && isFullMode && renderTitleCharacterCount()}
|
|
1183
2720
|
</>
|
|
1184
|
-
|
|
2721
|
+
)}
|
|
1185
2722
|
|
|
1186
2723
|
{/* Template Message */}
|
|
1187
2724
|
<CapRow useLegacy id="rcs-template-message-label">
|
|
@@ -1219,9 +2756,21 @@ const splitTemplateVarString = (str) => {
|
|
|
1219
2756
|
/>
|
|
1220
2757
|
</CapRow>
|
|
1221
2758
|
<CapRow useLegacy className="rcs-create-template-message-input">
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
2759
|
+
{/* 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. */}
|
|
2760
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
2761
|
+
{(isEditFlow || !isFullMode)?
|
|
2762
|
+
(
|
|
2763
|
+
<VarSegmentMessageEditor
|
|
2764
|
+
key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
2765
|
+
templateString={templateDesc}
|
|
2766
|
+
valueMap={descriptionVarSegmentValueMapById}
|
|
2767
|
+
onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
|
|
2768
|
+
onFocus={(id) => setDescTextAreaId(id)}
|
|
2769
|
+
varRegex={rcsVarRegex}
|
|
2770
|
+
placeholderPrefix=""
|
|
2771
|
+
getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
|
|
2772
|
+
/>
|
|
2773
|
+
)
|
|
1225
2774
|
: (
|
|
1226
2775
|
<>
|
|
1227
2776
|
<TextArea
|
|
@@ -1259,13 +2808,15 @@ const splitTemplateVarString = (str) => {
|
|
|
1259
2808
|
</>
|
|
1260
2809
|
)
|
|
1261
2810
|
}
|
|
1262
|
-
</
|
|
2811
|
+
</CapRow>
|
|
1263
2812
|
{(isEditFlow || !isFullMode) && templateDescError && (
|
|
1264
2813
|
<CapError className="rcs-template-message-error">
|
|
1265
2814
|
{templateDescError}
|
|
1266
2815
|
</CapError>
|
|
1267
2816
|
)}
|
|
1268
|
-
{
|
|
2817
|
+
{(isEditFlow || !isFullMode)
|
|
2818
|
+
? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
|
|
2819
|
+
: (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
|
|
1269
2820
|
{!isFullMode && hasTag() && (
|
|
1270
2821
|
<CapAlert
|
|
1271
2822
|
message={
|
|
@@ -1279,24 +2830,13 @@ const splitTemplateVarString = (str) => {
|
|
|
1279
2830
|
/>
|
|
1280
2831
|
)}
|
|
1281
2832
|
</CapRow>
|
|
1282
|
-
{
|
|
2833
|
+
{((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
|
|
2834
|
+
renderButtonComponent()}
|
|
1283
2835
|
</>
|
|
1284
2836
|
|
|
1285
2837
|
);
|
|
1286
2838
|
};
|
|
1287
2839
|
|
|
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
2840
|
// Get character count for title (rich card only)
|
|
1301
2841
|
const getTitleCharacterCount = () => {
|
|
1302
2842
|
if (templateType === contentType.text_message) return 0;
|
|
@@ -1311,7 +2851,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1311
2851
|
// Get max length for description based on template type
|
|
1312
2852
|
const getDescriptionMaxLength = () => {
|
|
1313
2853
|
return templateType === contentType.text_message
|
|
1314
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
|
|
2854
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
|
|
1315
2855
|
: RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
|
|
1316
2856
|
};
|
|
1317
2857
|
|
|
@@ -1337,6 +2877,58 @@ const splitTemplateVarString = (str) => {
|
|
|
1337
2877
|
);
|
|
1338
2878
|
};
|
|
1339
2879
|
|
|
2880
|
+
const rcsDltCardDeleteHandler = () => {
|
|
2881
|
+
closeDltContainerHandler();
|
|
2882
|
+
setDltEditData({});
|
|
2883
|
+
setFallbackMessage('');
|
|
2884
|
+
setFallbackMessageError(false);
|
|
2885
|
+
};
|
|
2886
|
+
|
|
2887
|
+
const dltFallbackListingPreviewhandler = (data) => {
|
|
2888
|
+
const {
|
|
2889
|
+
'updated-sms-editor': updatedSmsEditor = [],
|
|
2890
|
+
'sms-editor': smsEditor = '',
|
|
2891
|
+
} = data.versions.base || {};
|
|
2892
|
+
};
|
|
2893
|
+
|
|
2894
|
+
const getDltContentCardList = (content, channel) => {
|
|
2895
|
+
const extra = [
|
|
2896
|
+
<CapIcon
|
|
2897
|
+
type="edit"
|
|
2898
|
+
className="rcs-dlt-edit-icon"
|
|
2899
|
+
onClick={() => rcsDltEditSelectHandler(dltEditData)}
|
|
2900
|
+
/>,
|
|
2901
|
+
<CapDropdown
|
|
2902
|
+
overlay={(
|
|
2903
|
+
<CapMenu>
|
|
2904
|
+
<>
|
|
2905
|
+
<CapMenu.Item
|
|
2906
|
+
className="ant-dropdown-menu-item"
|
|
2907
|
+
onClick={() => {}}
|
|
2908
|
+
>
|
|
2909
|
+
{formatMessage(globalMessages.preview)}
|
|
2910
|
+
</CapMenu.Item>
|
|
2911
|
+
<CapMenu.Item
|
|
2912
|
+
className="ant-dropdown-menu-item"
|
|
2913
|
+
onClick={rcsDltCardDeleteHandler}
|
|
2914
|
+
>
|
|
2915
|
+
{formatMessage(globalMessages.delete)}
|
|
2916
|
+
</CapMenu.Item>
|
|
2917
|
+
</>
|
|
2918
|
+
</CapMenu>
|
|
2919
|
+
)}
|
|
2920
|
+
>
|
|
2921
|
+
<CapIcon type="more" />
|
|
2922
|
+
</CapDropdown>,
|
|
2923
|
+
];
|
|
2924
|
+
return {
|
|
2925
|
+
title: channel,
|
|
2926
|
+
content,
|
|
2927
|
+
cardType: channel,
|
|
2928
|
+
extra,
|
|
2929
|
+
};
|
|
2930
|
+
};
|
|
2931
|
+
|
|
1340
2932
|
// Render character count for description/message
|
|
1341
2933
|
const renderDescriptionCharacterCount = (className = "rcs-character-count") => {
|
|
1342
2934
|
const currentLength = getDescriptionCharacterCount();
|
|
@@ -1352,6 +2944,26 @@ const splitTemplateVarString = (str) => {
|
|
|
1352
2944
|
);
|
|
1353
2945
|
};
|
|
1354
2946
|
|
|
2947
|
+
// Carousel: per-card character counts (same limits as rich card)
|
|
2948
|
+
const getCarouselTitleCharacterCount = (cardIndex) => {
|
|
2949
|
+
const titleText = carouselData?.[cardIndex]?.title || '';
|
|
2950
|
+
return titleText ? titleText.length : 0;
|
|
2951
|
+
};
|
|
2952
|
+
|
|
2953
|
+
const getCarouselDescriptionCharacterCount = (cardIndex) => {
|
|
2954
|
+
const descriptionText = carouselData?.[cardIndex]?.description || '';
|
|
2955
|
+
return descriptionText ? descriptionText.length : 0;
|
|
2956
|
+
};
|
|
2957
|
+
|
|
2958
|
+
const renderCarouselCharacterCount = (currentLength, maxLength, className = "rcs-character-count") => (
|
|
2959
|
+
<CapLabel type="label1" className={className}>
|
|
2960
|
+
{formatMessage(messages.templateMessageLength, {
|
|
2961
|
+
currentLength,
|
|
2962
|
+
maxLength,
|
|
2963
|
+
})}
|
|
2964
|
+
</CapLabel>
|
|
2965
|
+
);
|
|
2966
|
+
|
|
1355
2967
|
// Check if any RCS variables contain tags (similar to Zalo hasTag logic)
|
|
1356
2968
|
const hasTag = () => {
|
|
1357
2969
|
// Check cardVarMapped values for tags
|
|
@@ -1389,83 +3001,32 @@ const splitTemplateVarString = (str) => {
|
|
|
1389
3001
|
};
|
|
1390
3002
|
|
|
1391
3003
|
const rcsDltEditSelectHandler = (data) => {
|
|
1392
|
-
setShowDltContainer(true);
|
|
1393
|
-
setDltMode(RCS_DLT_MODE.EDIT);
|
|
1394
|
-
setDltEditData(data);
|
|
1395
|
-
};
|
|
1396
|
-
|
|
1397
|
-
const rcsDltEditCompletionHandler = (data) => {
|
|
1398
|
-
const tempData = {
|
|
1399
|
-
versions: {
|
|
1400
|
-
// eslint-disable-next-line no-undef
|
|
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
|
-
);
|
|
3004
|
+
setShowDltContainer(true);
|
|
3005
|
+
setDltMode(RCS_DLT_MODE.EDIT);
|
|
3006
|
+
setDltEditData(data);
|
|
1431
3007
|
};
|
|
1432
3008
|
|
|
1433
|
-
const
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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,
|
|
3009
|
+
const rcsDltEditCompletionHandler = (data) => {
|
|
3010
|
+
const tempData = {
|
|
3011
|
+
versions: {
|
|
3012
|
+
// eslint-disable-next-line no-undef
|
|
3013
|
+
base: data?.value?.base || {},
|
|
3014
|
+
},
|
|
1468
3015
|
};
|
|
3016
|
+
const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
|
|
3017
|
+
'',
|
|
3018
|
+
);
|
|
3019
|
+
const templateNameFromDlt = get(dltEditData, 'name', '')
|
|
3020
|
+
|| get(tempData, 'versions.base.name', '')
|
|
3021
|
+
|| '';
|
|
3022
|
+
closeDltContainerHandler();
|
|
3023
|
+
setDltEditData(tempData);
|
|
3024
|
+
const unicodeFromDlt = get(tempData, 'versions.base.unicode-validity');
|
|
3025
|
+
setSmsFallbackData({
|
|
3026
|
+
templateName: templateNameFromDlt,
|
|
3027
|
+
content: fallMsg,
|
|
3028
|
+
...(typeof unicodeFromDlt === 'boolean' ? { unicodeValidity: unicodeFromDlt } : {}),
|
|
3029
|
+
});
|
|
1469
3030
|
};
|
|
1470
3031
|
|
|
1471
3032
|
const getDltSlideBoxContent = () => {
|
|
@@ -1513,148 +3074,34 @@ const splitTemplateVarString = (str) => {
|
|
|
1513
3074
|
return { dltHeader, dltContent };
|
|
1514
3075
|
};
|
|
1515
3076
|
|
|
1516
|
-
const renderFallBackSmsComponent = () =>
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
};
|
|
3077
|
+
const renderFallBackSmsComponent = () => (
|
|
3078
|
+
<SmsFallback
|
|
3079
|
+
value={smsFallbackData}
|
|
3080
|
+
onChange={setSmsFallbackData}
|
|
3081
|
+
parentLocation={location}
|
|
3082
|
+
smsRegister={smsRegister}
|
|
3083
|
+
isFullMode={isFullMode}
|
|
3084
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
3085
|
+
channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
|
|
3086
|
+
sectionTitle={
|
|
3087
|
+
smsFallbackData
|
|
3088
|
+
? formatMessage(messages.fallbackLabel)
|
|
3089
|
+
: formatMessage(messages.smsFallbackOptional)
|
|
3090
|
+
}
|
|
3091
|
+
templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
|
|
3092
|
+
templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
|
|
3093
|
+
/* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
|
|
3094
|
+
showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
|
|
3095
|
+
disableSelectTemplate={isEditFlow}
|
|
3096
|
+
eventContextTags={eventContextTags}
|
|
3097
|
+
onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
|
|
3098
|
+
isRcsEditFlow={isEditFlow}
|
|
3099
|
+
/>
|
|
3100
|
+
);
|
|
1654
3101
|
|
|
1655
3102
|
const uploadRcsImage = useCallback((file, type, fileParams, index) => {
|
|
1656
3103
|
setImageError(null);
|
|
1657
|
-
const isRcsThumbnail = index
|
|
3104
|
+
const isRcsThumbnail = isThumbnailAssetIndex(index);
|
|
1658
3105
|
actions.uploadRcsAsset(file, type, {
|
|
1659
3106
|
isRcsThumbnail,
|
|
1660
3107
|
...fileParams,
|
|
@@ -1692,10 +3139,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1692
3139
|
const updateOnRcsImageReUpload = useCallback(() => {
|
|
1693
3140
|
setUpdateRcsImageSrc('');
|
|
1694
3141
|
}, []);
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
3142
|
const uploadRcsVideo = (file, type, fileParams) => {
|
|
1698
|
-
setImageError(null);
|
|
1699
3143
|
actions.uploadRcsAsset(file, type, {
|
|
1700
3144
|
...fileParams,
|
|
1701
3145
|
type: 'video',
|
|
@@ -1704,9 +3148,6 @@ const splitTemplateVarString = (str) => {
|
|
|
1704
3148
|
});
|
|
1705
3149
|
};
|
|
1706
3150
|
|
|
1707
|
-
const updateRcsVideoSrc = (val) => {
|
|
1708
|
-
setRcsVideoSrc(val);
|
|
1709
|
-
};
|
|
1710
3151
|
const setUpdateRcsVideoSrc = useCallback((index, val) => {
|
|
1711
3152
|
setRcsVideoSrc(val);
|
|
1712
3153
|
setAssetList(val);
|
|
@@ -1729,13 +3170,13 @@ const splitTemplateVarString = (str) => {
|
|
|
1729
3170
|
updateRcsThumbnailSrc('');
|
|
1730
3171
|
};
|
|
1731
3172
|
|
|
1732
|
-
|
|
3173
|
+
const renderThumbnailComponent = () => {
|
|
1733
3174
|
const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
1734
3175
|
return !isEditFlow && (
|
|
1735
3176
|
<>
|
|
1736
3177
|
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
|
|
1737
3178
|
<CapImageUpload
|
|
1738
|
-
|
|
3179
|
+
className="rcs-image-upload-with-top-padding"
|
|
1739
3180
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1740
3181
|
imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
|
|
1741
3182
|
imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
|
|
@@ -1753,6 +3194,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1753
3194
|
channel={RCS}
|
|
1754
3195
|
channelSpecificStyle={!isFullMode}
|
|
1755
3196
|
skipDimensionValidation={true}
|
|
3197
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
1756
3198
|
/>
|
|
1757
3199
|
</>
|
|
1758
3200
|
)
|
|
@@ -1778,7 +3220,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1778
3220
|
value: dim.type,
|
|
1779
3221
|
label: `${dim.label}`
|
|
1780
3222
|
}))}
|
|
1781
|
-
|
|
3223
|
+
className="rcs-dimension-select--bottom-spacing"
|
|
1782
3224
|
/>
|
|
1783
3225
|
</>
|
|
1784
3226
|
)}
|
|
@@ -1792,7 +3234,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1792
3234
|
</div>
|
|
1793
3235
|
) : (
|
|
1794
3236
|
<CapImageUpload
|
|
1795
|
-
|
|
3237
|
+
className="rcs-image-upload-with-top-padding"
|
|
1796
3238
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1797
3239
|
imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
|
|
1798
3240
|
imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
|
|
@@ -1803,7 +3245,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1803
3245
|
updateImageSrc={setUpdateRcsImageSrc}
|
|
1804
3246
|
updateOnReUpload={updateOnRcsImageReUpload}
|
|
1805
3247
|
index={0}
|
|
1806
|
-
className="cap-custom-image-upload"
|
|
3248
|
+
className="cap-custom-image-upload rcs-image-upload--top-spacing"
|
|
1807
3249
|
key={`rcs-uploaded-image-${selectedDimension}`}
|
|
1808
3250
|
imageData={rcsData}
|
|
1809
3251
|
channel={RCS}
|
|
@@ -1814,7 +3256,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1814
3256
|
|
|
1815
3257
|
</>
|
|
1816
3258
|
);
|
|
1817
|
-
|
|
3259
|
+
}
|
|
1818
3260
|
|
|
1819
3261
|
const renderVideoComponent = () => {
|
|
1820
3262
|
const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
@@ -1833,7 +3275,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1833
3275
|
value: dim.type,
|
|
1834
3276
|
label: `${dim.label}`
|
|
1835
3277
|
}))}
|
|
1836
|
-
|
|
3278
|
+
className="rcs-dimension-select--bottom-spacing"
|
|
1837
3279
|
/>
|
|
1838
3280
|
)}
|
|
1839
3281
|
{(isEditFlow || !isFullMode) ? (
|
|
@@ -1891,10 +3333,48 @@ const splitTemplateVarString = (str) => {
|
|
|
1891
3333
|
};
|
|
1892
3334
|
|
|
1893
3335
|
const getRcsPreview = () => {
|
|
3336
|
+
|
|
3337
|
+
if (templateType === contentType.carousel) {
|
|
3338
|
+
const cardsForPreview = buildCarouselCardsForPreview(carouselData);
|
|
3339
|
+
const carouselDimKey = getCarouselDimensionKey();
|
|
3340
|
+
const carouselImgDims =
|
|
3341
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
3342
|
+
const carouselVidDims =
|
|
3343
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
3344
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
3345
|
+
// Debug log for embedded/library mode preview payload (carousel)
|
|
3346
|
+
// eslint-disable-next-line no-console
|
|
3347
|
+
return (
|
|
3348
|
+
<UnifiedPreview
|
|
3349
|
+
channel={RCS}
|
|
3350
|
+
content={{
|
|
3351
|
+
carouselData: cardsForPreview,
|
|
3352
|
+
carouselPreviewDimensions: {
|
|
3353
|
+
imageWidth: carouselImgDims.width,
|
|
3354
|
+
imageHeight: carouselImgDims.height,
|
|
3355
|
+
videoThumbWidth: carouselVidDims.width,
|
|
3356
|
+
videoThumbHeight: carouselVidDims.height,
|
|
3357
|
+
},
|
|
3358
|
+
}}
|
|
3359
|
+
device={ANDROID}
|
|
3360
|
+
showDeviceToggle={false}
|
|
3361
|
+
showHeader={false}
|
|
3362
|
+
formatMessage={formatMessage}
|
|
3363
|
+
/>
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
1894
3366
|
|
|
1895
3367
|
const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
|
|
1896
|
-
const
|
|
1897
|
-
const
|
|
3368
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
3369
|
+
const titleVarCountForResolve = isMediaTypeText
|
|
3370
|
+
? 0
|
|
3371
|
+
: ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
|
|
3372
|
+
const resolvedTitle = isMediaTypeText
|
|
3373
|
+
? ''
|
|
3374
|
+
: (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
|
|
3375
|
+
const resolvedDesc = isSlotMappingMode
|
|
3376
|
+
? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
|
|
3377
|
+
: templateDesc;
|
|
1898
3378
|
return (
|
|
1899
3379
|
<UnifiedPreview
|
|
1900
3380
|
channel={RCS}
|
|
@@ -1917,51 +3397,108 @@ const splitTemplateVarString = (str) => {
|
|
|
1917
3397
|
);
|
|
1918
3398
|
};
|
|
1919
3399
|
|
|
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
3400
|
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;
|
|
3401
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
1961
3402
|
const alignment = isMediaTypeImage
|
|
1962
3403
|
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
|
|
1963
3404
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
|
|
1964
3405
|
|
|
3406
|
+
const heightTypeForCardWidth = isMediaTypeText
|
|
3407
|
+
? undefined
|
|
3408
|
+
: isMediaTypeImage
|
|
3409
|
+
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
|
|
3410
|
+
: isMediaTypeVideo
|
|
3411
|
+
? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
|
|
3412
|
+
: undefined;
|
|
3413
|
+
const cardWidthFromSelection =
|
|
3414
|
+
heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
|
|
3415
|
+
|
|
3416
|
+
/** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
|
|
3417
|
+
const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
|
|
3418
|
+
const smsFallbackMerged = !isFullMode
|
|
3419
|
+
? (() => {
|
|
3420
|
+
const local =
|
|
3421
|
+
smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
|
|
3422
|
+
return {
|
|
3423
|
+
...smsFromApiShape,
|
|
3424
|
+
...local,
|
|
3425
|
+
rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
|
|
3426
|
+
};
|
|
3427
|
+
})()
|
|
3428
|
+
: (smsFallbackData || {});
|
|
3429
|
+
const smsFallbackForPayload = (() => {
|
|
3430
|
+
if (isFullMode) {
|
|
3431
|
+
return hasMeaningfulSmsFallbackShape(smsFallbackData) ? smsFallbackData : null;
|
|
3432
|
+
}
|
|
3433
|
+
const mapped = {
|
|
3434
|
+
templateName:
|
|
3435
|
+
smsFallbackMerged.templateName
|
|
3436
|
+
|| smsFallbackMerged.smsTemplateName
|
|
3437
|
+
|| '',
|
|
3438
|
+
// Use `||` so empty `content` does not block campaign/API `message` (common in embedded flows).
|
|
3439
|
+
content:
|
|
3440
|
+
smsFallbackMerged.content
|
|
3441
|
+
|| smsFallbackMerged.smsContent
|
|
3442
|
+
|| smsFallbackMerged.smsTemplateContent
|
|
3443
|
+
|| smsFallbackMerged.message
|
|
3444
|
+
|| '',
|
|
3445
|
+
templateContent:
|
|
3446
|
+
pickFirstSmsFallbackTemplateString(smsFallbackMerged)
|
|
3447
|
+
|| '',
|
|
3448
|
+
...(typeof smsFallbackMerged.unicodeValidity === 'boolean'
|
|
3449
|
+
&& { unicodeValidity: smsFallbackMerged.unicodeValidity }),
|
|
3450
|
+
...(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
|
|
3451
|
+
&& Object.keys(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
|
|
3452
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
|
|
3453
|
+
smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
|
|
3454
|
+
}),
|
|
3455
|
+
};
|
|
3456
|
+
return hasMeaningfulSmsFallbackShape(mapped) ? mapped : null;
|
|
3457
|
+
})();
|
|
3458
|
+
|
|
3459
|
+
const carouselCardContent = isCarouselType
|
|
3460
|
+
? (carouselData || []).map((card = {}) => {
|
|
3461
|
+
const cardMediaType = card.mediaType;
|
|
3462
|
+
const isCardVideo = cardMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
3463
|
+
const isCardImage = cardMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
3464
|
+
const mediaUrl = isCardVideo
|
|
3465
|
+
? (card.videoAsset?.videoSrc || '')
|
|
3466
|
+
: (card.imageSrc || '');
|
|
3467
|
+
const thumbnailUrl = isCardVideo
|
|
3468
|
+
? (card.thumbnailSrc || card.videoAsset?.videoThumbnail || '')
|
|
3469
|
+
: '';
|
|
3470
|
+
const cardVarTokens = [
|
|
3471
|
+
...((card.title || '').match(rcsVarRegex) ?? []),
|
|
3472
|
+
...((card.description || '').match(rcsVarRegex) ?? []),
|
|
3473
|
+
];
|
|
3474
|
+
const cardVarMappedForCard = {};
|
|
3475
|
+
if (isSlotMappingMode && cardVarTokens.length > 0) {
|
|
3476
|
+
cardVarTokens.forEach((token) => {
|
|
3477
|
+
const varName = getVarNameFromToken(token);
|
|
3478
|
+
if (!varName) return;
|
|
3479
|
+
cardVarMappedForCard[varName] = sanitizeCardVarMappedValue(cardVarMapped?.[varName] ?? '');
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
return {
|
|
3483
|
+
title: card.title || '',
|
|
3484
|
+
description: card.description || '',
|
|
3485
|
+
mediaType: cardMediaType,
|
|
3486
|
+
...((isCardImage || isCardVideo) && {
|
|
3487
|
+
media: {
|
|
3488
|
+
mediaUrl,
|
|
3489
|
+
thumbnailUrl,
|
|
3490
|
+
height: selectedCarouselHeight || MEDIUM,
|
|
3491
|
+
...(isCardVideo && card.videoAsset?.videoName && { videoName: card.videoAsset.videoName }),
|
|
3492
|
+
},
|
|
3493
|
+
}),
|
|
3494
|
+
...(Array.isArray(card.suggestions) && card.suggestions.length > 0 && {
|
|
3495
|
+
suggestions: card.suggestions,
|
|
3496
|
+
}),
|
|
3497
|
+
...(Object.keys(cardVarMappedForCard).length > 0 && { cardVarMapped: cardVarMappedForCard }),
|
|
3498
|
+
};
|
|
3499
|
+
})
|
|
3500
|
+
: null;
|
|
3501
|
+
|
|
1965
3502
|
const payload = {
|
|
1966
3503
|
name: templateName,
|
|
1967
3504
|
versions: {
|
|
@@ -1970,16 +3507,22 @@ const splitTemplateVarString = (str) => {
|
|
|
1970
3507
|
RCS: {
|
|
1971
3508
|
rcsContent: {
|
|
1972
3509
|
...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
|
|
1973
|
-
cardType: STANDALONE,
|
|
1974
|
-
cardSettings:
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
3510
|
+
cardType: isCarouselType ? contentType.carousel : STANDALONE,
|
|
3511
|
+
cardSettings: isCarouselType
|
|
3512
|
+
? { cardOrientation: VERTICAL, cardWidth: selectedCarouselWidth || SMALL }
|
|
3513
|
+
: {
|
|
3514
|
+
cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
|
|
3515
|
+
...(alignment && { mediaAlignment: alignment }),
|
|
3516
|
+
cardWidth: SMALL,
|
|
3517
|
+
},
|
|
3518
|
+
cardContent: isCarouselType
|
|
3519
|
+
? carouselCardContent
|
|
3520
|
+
: [
|
|
1980
3521
|
{
|
|
1981
|
-
|
|
1982
|
-
|
|
3522
|
+
// Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
|
|
3523
|
+
// / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
|
|
3524
|
+
title: templateTitle,
|
|
3525
|
+
description: templateDesc,
|
|
1983
3526
|
mediaType: templateMediaType,
|
|
1984
3527
|
...(!isMediaTypeText && {media: {
|
|
1985
3528
|
mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
|
|
@@ -1988,23 +3531,32 @@ const splitTemplateVarString = (str) => {
|
|
|
1988
3531
|
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
|
|
1989
3532
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
|
|
1990
3533
|
}}),
|
|
1991
|
-
...(
|
|
1992
|
-
const
|
|
1993
|
-
...(templateTitle
|
|
1994
|
-
...(templateDesc
|
|
3534
|
+
...(isSlotMappingMode && (() => {
|
|
3535
|
+
const templateVarTokens = [
|
|
3536
|
+
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
3537
|
+
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
1995
3538
|
];
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
3539
|
+
const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
|
|
3540
|
+
cardVarMapped,
|
|
3541
|
+
);
|
|
3542
|
+
// Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
|
|
3543
|
+
// semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
|
|
3544
|
+
// and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
|
|
3545
|
+
const persistedSlotVarMap = {};
|
|
3546
|
+
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
3547
|
+
const varName = getVarNameFromToken(token);
|
|
3548
|
+
if (!varName) return;
|
|
3549
|
+
const resolvedRawValue = resolveCardVarMappedSlotValue(
|
|
3550
|
+
cardVarMappedForRcsCardOnly,
|
|
3551
|
+
varName,
|
|
3552
|
+
slotIndexZeroBased,
|
|
3553
|
+
isSlotMappingMode,
|
|
3554
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
3555
|
+
);
|
|
3556
|
+
const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
|
|
3557
|
+
persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
|
|
2006
3558
|
});
|
|
2007
|
-
return { cardVarMapped:
|
|
3559
|
+
return { cardVarMapped: persistedSlotVarMap };
|
|
2008
3560
|
})()),
|
|
2009
3561
|
...(suggestions.length > 0 && { suggestions }),
|
|
2010
3562
|
}
|
|
@@ -2012,17 +3564,109 @@ const splitTemplateVarString = (str) => {
|
|
|
2012
3564
|
contentType: isFullMode ? templateType : RICHCARD,
|
|
2013
3565
|
...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
|
|
2014
3566
|
},
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
3567
|
+
...(smsFallbackForPayload && (() => {
|
|
3568
|
+
const smsBodyText =
|
|
3569
|
+
smsFallbackForPayload.content
|
|
3570
|
+
|| smsFallbackForPayload.templateContent
|
|
3571
|
+
|| smsFallbackForPayload.message
|
|
3572
|
+
|| smsFallbackForPayload.smsContent
|
|
3573
|
+
|| '';
|
|
3574
|
+
/**
|
|
3575
|
+
* Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
|
|
3576
|
+
* Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
|
|
3577
|
+
*/
|
|
3578
|
+
const m = smsFallbackMerged || {};
|
|
3579
|
+
const tcSibling = m.templateConfigs && typeof m.templateConfigs === 'object'
|
|
3580
|
+
? m.templateConfigs
|
|
3581
|
+
: {};
|
|
3582
|
+
const smsFallbackTemplateId =
|
|
3583
|
+
(m.smsTemplateId != null && String(m.smsTemplateId).trim() !== ''
|
|
3584
|
+
? String(m.smsTemplateId)
|
|
3585
|
+
: '')
|
|
3586
|
+
|| (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
|
|
3587
|
+
? String(tcSibling.templateId)
|
|
3588
|
+
: '');
|
|
3589
|
+
const smsFallbackTemplateStr =
|
|
3590
|
+
pickFirstSmsFallbackTemplateString(m)
|
|
3591
|
+
|| (typeof m.templateContent === 'string' ? m.templateContent : '')
|
|
3592
|
+
|| (typeof tcSibling.template === 'string' ? tcSibling.template : '')
|
|
3593
|
+
|| '';
|
|
3594
|
+
const smsFallbackTemplateName =
|
|
3595
|
+
m.templateName
|
|
3596
|
+
|| m.smsTemplateName
|
|
3597
|
+
|| tcSibling.templateName
|
|
3598
|
+
|| tcSibling.name
|
|
3599
|
+
|| '';
|
|
3600
|
+
const registeredSenderIdsForPayload = Array.isArray(m.registeredSenderIds)
|
|
3601
|
+
? m.registeredSenderIds
|
|
3602
|
+
: Array.isArray(tcSibling.registeredSenderIds)
|
|
3603
|
+
? tcSibling.registeredSenderIds
|
|
3604
|
+
: Array.isArray(tcSibling.header)
|
|
3605
|
+
? tcSibling.header
|
|
3606
|
+
: null;
|
|
3607
|
+
const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
|
|
3608
|
+
const smsFallbackTemplateConfigs =
|
|
3609
|
+
smsFallbackTemplateId || hasRegisteredSenderIds
|
|
3610
|
+
? {
|
|
3611
|
+
...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
|
|
3612
|
+
...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
|
|
3613
|
+
...(smsFallbackTemplateName && {
|
|
3614
|
+
templateName: smsFallbackTemplateName,
|
|
3615
|
+
}),
|
|
3616
|
+
...(hasRegisteredSenderIds && {
|
|
3617
|
+
registeredSenderIds: registeredSenderIdsForPayload,
|
|
3618
|
+
}),
|
|
3619
|
+
}
|
|
3620
|
+
: null;
|
|
3621
|
+
const isDltCampaign = !isFullMode && isTraiDLTEnable(isFullMode, smsRegister);
|
|
3622
|
+
return {
|
|
3623
|
+
smsFallBackContent: isFullMode
|
|
3624
|
+
? {
|
|
3625
|
+
smsTemplateName: smsFallbackForPayload.templateName || '',
|
|
3626
|
+
smsContent: smsBodyText,
|
|
3627
|
+
// cap-campaigns-v2 `normalizeRcsMessageContentForApi` only serializes `message` (+ templateConfigs); without this key SMS fallback is dropped on send.
|
|
3628
|
+
message: smsBodyText,
|
|
3629
|
+
...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
|
|
3630
|
+
unicodeValidity: smsFallbackForPayload.unicodeValidity,
|
|
3631
|
+
}),
|
|
3632
|
+
...(smsFallbackForPayload.rcsSmsFallbackVarMapped
|
|
3633
|
+
&& Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
|
|
3634
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
|
|
3635
|
+
}),
|
|
3636
|
+
...(smsFallbackTemplateConfigs && {
|
|
3637
|
+
templateConfigs: smsFallbackTemplateConfigs,
|
|
3638
|
+
}),
|
|
3639
|
+
}
|
|
3640
|
+
: {
|
|
3641
|
+
// Round-trip storage: full shape so reopening the editor restores template
|
|
3642
|
+
// name, sender IDs, and var mappings. normalizeRcsMessageContentForApi
|
|
3643
|
+
// (called in CreativesContainer getCreativesData) strips this to
|
|
3644
|
+
// { message, templateConfigs } before the API call — the extra fields here
|
|
3645
|
+
// are only used for re-hydration, never sent to the API.
|
|
3646
|
+
message: smsBodyText,
|
|
3647
|
+
...(smsFallbackForPayload.templateName && {
|
|
3648
|
+
smsTemplateName: smsFallbackForPayload.templateName,
|
|
3649
|
+
}),
|
|
3650
|
+
...(smsFallbackForPayload.templateContent && {
|
|
3651
|
+
templateContent: smsFallbackForPayload.templateContent,
|
|
3652
|
+
}),
|
|
3653
|
+
...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
|
|
3654
|
+
unicodeValidity: smsFallbackForPayload.unicodeValidity,
|
|
3655
|
+
}),
|
|
3656
|
+
...(Array.isArray(registeredSenderIdsForPayload) && {
|
|
3657
|
+
registeredSenderIds: registeredSenderIdsForPayload,
|
|
3658
|
+
}),
|
|
3659
|
+
...(smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
|
|
3660
|
+
&& Object.keys(smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
|
|
3661
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
|
|
3662
|
+
smsFallbackForPayload[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
|
|
3663
|
+
}),
|
|
3664
|
+
...(isDltCampaign && smsFallbackTemplateConfigs && {
|
|
3665
|
+
templateConfigs: smsFallbackTemplateConfigs,
|
|
3666
|
+
}),
|
|
3667
|
+
},
|
|
3668
|
+
};
|
|
3669
|
+
})()),
|
|
2026
3670
|
},
|
|
2027
3671
|
},
|
|
2028
3672
|
},
|
|
@@ -2032,6 +3676,141 @@ const splitTemplateVarString = (str) => {
|
|
|
2032
3676
|
return payload;
|
|
2033
3677
|
};
|
|
2034
3678
|
|
|
3679
|
+
/** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
|
|
3680
|
+
const testPreviewFormData = useMemo(() => {
|
|
3681
|
+
const payload = createPayload();
|
|
3682
|
+
const rcs = payload?.versions?.base?.content?.RCS;
|
|
3683
|
+
if (!rcs) return null;
|
|
3684
|
+
// createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
|
|
3685
|
+
const accountIdForCreateMessageMeta =
|
|
3686
|
+
(wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
|
|
3687
|
+
? String(wecrmAccountId)
|
|
3688
|
+
: accountId;
|
|
3689
|
+
const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
|
|
3690
|
+
let rcsForTest = {
|
|
3691
|
+
...rcs,
|
|
3692
|
+
rcsContent: {
|
|
3693
|
+
...rcs.rcsContent,
|
|
3694
|
+
...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
|
|
3695
|
+
},
|
|
3696
|
+
};
|
|
3697
|
+
/** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
|
|
3698
|
+
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
3699
|
+
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
3700
|
+
if (isCarouselType) {
|
|
3701
|
+
// Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
|
|
3702
|
+
// user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
3703
|
+
// - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
|
|
3704
|
+
// - prepareTagExtractionPayload can extract tag metadata from the card content.
|
|
3705
|
+
// Track cumulative slotOffset across cards: each card's title/description vars occupy
|
|
3706
|
+
// sequential global slot indices ({{1}},{{2}} in card 0; {{3}},{{4}} in card 1, …).
|
|
3707
|
+
// Without the offset, every card restarts at slotKey="1" and resolves against card 0's
|
|
3708
|
+
// mappings, causing tags from card 1+ to be missing or wrong.
|
|
3709
|
+
let carouselSlotOffset = 0;
|
|
3710
|
+
rcsForTest = {
|
|
3711
|
+
...rcsForTest,
|
|
3712
|
+
rcsContent: {
|
|
3713
|
+
...rcsForTest.rcsContent,
|
|
3714
|
+
cardContent: cardContent.map((card) => {
|
|
3715
|
+
const rawTitle = card.title || '';
|
|
3716
|
+
const rawDesc = card.description || '';
|
|
3717
|
+
const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
|
|
3718
|
+
const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
|
|
3719
|
+
const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
|
|
3720
|
+
const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
|
|
3721
|
+
carouselSlotOffset += titleVarCount + descVarCount;
|
|
3722
|
+
return { ...card, title: resolvedTitle, description: resolvedDesc };
|
|
3723
|
+
}),
|
|
3724
|
+
},
|
|
3725
|
+
};
|
|
3726
|
+
} else if (isSlotMappingModeForPreview) {
|
|
3727
|
+
// Standalone card: coalesce cardVarMapped with the template's slot names so the preview
|
|
3728
|
+
// API receives a correctly-keyed var map for non-carousel templates.
|
|
3729
|
+
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
3730
|
+
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
3731
|
+
templateTitle,
|
|
3732
|
+
templateDesc,
|
|
3733
|
+
rcsVarRegex,
|
|
3734
|
+
);
|
|
3735
|
+
rcsForTest = {
|
|
3736
|
+
...rcsForTest,
|
|
3737
|
+
rcsContent: {
|
|
3738
|
+
...rcsForTest.rcsContent,
|
|
3739
|
+
cardContent: [
|
|
3740
|
+
{ ...cardContent[0], cardVarMapped: fullCardVarMapped },
|
|
3741
|
+
...cardContent.slice(1),
|
|
3742
|
+
],
|
|
3743
|
+
},
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
const out = {
|
|
3748
|
+
versions: {
|
|
3749
|
+
base: {
|
|
3750
|
+
content: {
|
|
3751
|
+
RCS: rcsForTest,
|
|
3752
|
+
},
|
|
3753
|
+
},
|
|
3754
|
+
},
|
|
3755
|
+
};
|
|
3756
|
+
// Use the smsFallbackTemplateConfigs already built by createPayload() — it uses smsFallbackMerged
|
|
3757
|
+
// which merges smsFromApiShape (API baseline with templateId, registeredSenderIds, templateName, etc.)
|
|
3758
|
+
// with local smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced
|
|
3759
|
+
// smsTemplateId in campaign mode (smsFallbackMerged = { ...smsFromApiShape, ...local } there).
|
|
3760
|
+
// This also keeps templateName (and all other fields) in sync with the approval payload automatically.
|
|
3761
|
+
const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
|
|
3762
|
+
if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
|
|
3763
|
+
out.templateConfigs = {
|
|
3764
|
+
...smsFallbackTcFromPayload,
|
|
3765
|
+
traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
return out;
|
|
3769
|
+
}, [
|
|
3770
|
+
templateName,
|
|
3771
|
+
templateTitle,
|
|
3772
|
+
templateDesc,
|
|
3773
|
+
templateMediaType,
|
|
3774
|
+
cardVarMapped,
|
|
3775
|
+
carouselData,
|
|
3776
|
+
suggestions,
|
|
3777
|
+
rcsImageSrc,
|
|
3778
|
+
rcsVideoSrc,
|
|
3779
|
+
rcsThumbnailSrc,
|
|
3780
|
+
selectedDimension,
|
|
3781
|
+
smsFallbackData,
|
|
3782
|
+
isFullMode,
|
|
3783
|
+
isEditFlow,
|
|
3784
|
+
templateType,
|
|
3785
|
+
accountId,
|
|
3786
|
+
wecrmAccountId,
|
|
3787
|
+
accessToken,
|
|
3788
|
+
accountName,
|
|
3789
|
+
hostName,
|
|
3790
|
+
smsRegister,
|
|
3791
|
+
]);
|
|
3792
|
+
|
|
3793
|
+
/**
|
|
3794
|
+
* Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
|
|
3795
|
+
* with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
|
|
3796
|
+
* miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
|
|
3797
|
+
*/
|
|
3798
|
+
const librarySmsFallbackMergedForValidation = useMemo(() => {
|
|
3799
|
+
if (isFullMode) {
|
|
3800
|
+
return smsFallbackData;
|
|
3801
|
+
}
|
|
3802
|
+
const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
|
|
3803
|
+
const local =
|
|
3804
|
+
smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
|
|
3805
|
+
return {
|
|
3806
|
+
...smsFromApiShape,
|
|
3807
|
+
...local,
|
|
3808
|
+
rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
|
|
3809
|
+
};
|
|
3810
|
+
}, [isFullMode, templateData, smsFallbackData]);
|
|
3811
|
+
|
|
3812
|
+
|
|
3813
|
+
|
|
2035
3814
|
const actionCallback = ({ errorMessage, resp }, isEdit) => {
|
|
2036
3815
|
// eslint-disable-next-line no-undef
|
|
2037
3816
|
const error = errorMessage?.message || errorMessage;
|
|
@@ -2061,6 +3840,9 @@ const splitTemplateVarString = (str) => {
|
|
|
2061
3840
|
_id: params?.id,
|
|
2062
3841
|
validity: true,
|
|
2063
3842
|
type: RCS,
|
|
3843
|
+
// CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
|
|
3844
|
+
// the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
|
|
3845
|
+
closeSlideBoxAfterSubmit: !isFullMode,
|
|
2064
3846
|
};
|
|
2065
3847
|
getFormData(formDataParams);
|
|
2066
3848
|
};
|
|
@@ -2074,6 +3856,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2074
3856
|
actionCallback({ resp, errorMessage });
|
|
2075
3857
|
setSpin(false); // Always turn off spinner
|
|
2076
3858
|
if (!errorMessage) {
|
|
3859
|
+
setTemplateStatus(RCS_STATUSES.pending);
|
|
2077
3860
|
onCreateComplete();
|
|
2078
3861
|
}
|
|
2079
3862
|
});
|
|
@@ -2085,6 +3868,91 @@ const splitTemplateVarString = (str) => {
|
|
|
2085
3868
|
}
|
|
2086
3869
|
};
|
|
2087
3870
|
|
|
3871
|
+
/** When a fallback SMS row exists, require non-empty body (trimmed) and filled var slots (DLT). */
|
|
3872
|
+
const smsFallbackBlocksDone = () => {
|
|
3873
|
+
// Non-DLT library: user removed SMS fallback (local null) but template still carries fallback — block Done.
|
|
3874
|
+
if (
|
|
3875
|
+
!isFullMode
|
|
3876
|
+
&& !isTraiDLTEnable(isFullMode, smsRegister)
|
|
3877
|
+
&& smsFallbackData == null
|
|
3878
|
+
&& hasMeaningfulSmsFallbackShape(
|
|
3879
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
3880
|
+
)
|
|
3881
|
+
) {
|
|
3882
|
+
return true;
|
|
3883
|
+
}
|
|
3884
|
+
if (!smsFallbackData) return false;
|
|
3885
|
+
// Full-mode (Send for approval): SMS fallback is optional. Tag-slot mapping is a display/preview
|
|
3886
|
+
// concern, not a structural requirement for approval — the registered SMS template body stands on
|
|
3887
|
+
// its own. Never block the Send for approval button due to missing or unfilled fallback var slots.
|
|
3888
|
+
if (isFullMode) return false;
|
|
3889
|
+
const merged = librarySmsFallbackMergedForValidation;
|
|
3890
|
+
const templateText = pickFirstSmsFallbackTemplateString(merged);
|
|
3891
|
+
if (!templateText) {
|
|
3892
|
+
return true;
|
|
3893
|
+
}
|
|
3894
|
+
const rawVarMap =
|
|
3895
|
+
merged.rcsSmsFallbackVarMapped
|
|
3896
|
+
|| merged['rcs-sms-fallback-var-mapped'];
|
|
3897
|
+
const varMap =
|
|
3898
|
+
rawVarMap != null && typeof rawVarMap === 'object' ? rawVarMap : {};
|
|
3899
|
+
return !areAllRcsSmsFallbackVarSlotsFilled(templateText, varMap);
|
|
3900
|
+
};
|
|
3901
|
+
|
|
3902
|
+
/**
|
|
3903
|
+
* Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
|
|
3904
|
+
* semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
|
|
3905
|
+
* / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
|
|
3906
|
+
*/
|
|
3907
|
+
const isLibraryCampaignCardVarMappingIncomplete = () => {
|
|
3908
|
+
if (isFullMode) return false;
|
|
3909
|
+
const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
|
|
3910
|
+
rcsVarTestRegex.test(elem),
|
|
3911
|
+
);
|
|
3912
|
+
const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
|
|
3913
|
+
rcsVarTestRegex.test(elem),
|
|
3914
|
+
);
|
|
3915
|
+
const orderedVarNames = [
|
|
3916
|
+
...titleTokens.map((t) => t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
|
|
3917
|
+
...descTokens.map((t) => t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
|
|
3918
|
+
];
|
|
3919
|
+
if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
|
|
3920
|
+
return true;
|
|
3921
|
+
}
|
|
3922
|
+
return orderedVarNames.some((name, globalIdx) => {
|
|
3923
|
+
const v = resolveCardVarMappedSlotValue(
|
|
3924
|
+
cardVarMapped,
|
|
3925
|
+
name,
|
|
3926
|
+
globalIdx,
|
|
3927
|
+
true,
|
|
3928
|
+
rcsSpanningSemanticVarNames.has(name),
|
|
3929
|
+
);
|
|
3930
|
+
const s = v == null ? '' : String(v);
|
|
3931
|
+
return s.trim() === '';
|
|
3932
|
+
});
|
|
3933
|
+
};
|
|
3934
|
+
|
|
3935
|
+
const isCarouselLibraryIncomplete = () => {
|
|
3936
|
+
if (!isCarouselType || isFullMode) return false;
|
|
3937
|
+
if ((carouselErrors || []).some((err) => err?.title || err?.description)) return true;
|
|
3938
|
+
// Block if any card's title or description has no content at all (no static text and no variable tokens).
|
|
3939
|
+
const hasEmptyField = (carouselData || []).some((card) =>
|
|
3940
|
+
['title', 'description'].some((field) => !(card?.[field] || '').trim())
|
|
3941
|
+
);
|
|
3942
|
+
if (hasEmptyField) return true;
|
|
3943
|
+
const unfilledVar = (carouselData || []).some((card) =>
|
|
3944
|
+
['title', 'description'].some((field) => {
|
|
3945
|
+
const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((t) => rcsVarTestRegex.test(t));
|
|
3946
|
+
return tokens.some((t) => {
|
|
3947
|
+
const name = t.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
3948
|
+
const v = cardVarMapped?.[name];
|
|
3949
|
+
return v == null || String(v).trim() === '';
|
|
3950
|
+
});
|
|
3951
|
+
})
|
|
3952
|
+
);
|
|
3953
|
+
return unfilledVar;
|
|
3954
|
+
};
|
|
3955
|
+
|
|
2088
3956
|
const isDisableDone = () => {
|
|
2089
3957
|
if(isEditFlow){
|
|
2090
3958
|
return false;
|
|
@@ -2095,46 +3963,26 @@ const splitTemplateVarString = (str) => {
|
|
|
2095
3963
|
}
|
|
2096
3964
|
}
|
|
2097
3965
|
|
|
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
|
-
}
|
|
3966
|
+
if (isCarouselLibraryIncomplete()) {
|
|
3967
|
+
return true;
|
|
3968
|
+
}
|
|
2118
3969
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
if (typeof v !== 'string') return !v;
|
|
2122
|
-
return v.trim() === '';
|
|
2123
|
-
});
|
|
2124
|
-
if (anyMissing) {
|
|
2125
|
-
return true;
|
|
2126
|
-
}
|
|
3970
|
+
if (isLibraryCampaignCardVarMappingIncomplete()) {
|
|
3971
|
+
return true;
|
|
2127
3972
|
}
|
|
2128
3973
|
|
|
2129
|
-
|
|
3974
|
+
if (smsFallbackBlocksDone()) {
|
|
2130
3975
|
return true;
|
|
3976
|
+
}
|
|
2131
3977
|
|
|
3978
|
+
if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
|
|
3979
|
+
return true;
|
|
2132
3980
|
}
|
|
2133
|
-
if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
3981
|
+
if (!isCarouselType && isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2134
3982
|
return true;
|
|
2135
3983
|
}
|
|
2136
3984
|
|
|
2137
|
-
if (isMediaTypeVideo && (rcsVideoSrc.videoSrc
|
|
3985
|
+
if (!isCarouselType && isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2138
3986
|
return true;
|
|
2139
3987
|
}
|
|
2140
3988
|
if (buttonType.includes(CTA)) {
|
|
@@ -2146,72 +3994,60 @@ const splitTemplateVarString = (str) => {
|
|
|
2146
3994
|
return true;
|
|
2147
3995
|
}
|
|
2148
3996
|
}
|
|
2149
|
-
if (templateDescError || templateTitleError
|
|
3997
|
+
if (templateDescError || templateTitleError) {
|
|
3998
|
+
return true;
|
|
3999
|
+
}
|
|
4000
|
+
if (
|
|
4001
|
+
smsFallbackData?.content
|
|
4002
|
+
&& smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
|
|
4003
|
+
) {
|
|
2150
4004
|
return true;
|
|
2151
4005
|
}
|
|
2152
4006
|
return false;
|
|
2153
4007
|
};
|
|
2154
4008
|
|
|
2155
4009
|
const isEditDisableDone = () => {
|
|
2156
|
-
|
|
2157
|
-
if (templateStatus !== RCS_STATUSES.approved) {
|
|
4010
|
+
if (isFullMode && !isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
|
|
2158
4011
|
return true;
|
|
2159
4012
|
}
|
|
2160
4013
|
|
|
2161
|
-
if (
|
|
2162
|
-
|
|
2163
|
-
return true;
|
|
2164
|
-
}
|
|
4014
|
+
if (isCarouselLibraryIncomplete()) {
|
|
4015
|
+
return true;
|
|
2165
4016
|
}
|
|
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
4017
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
4018
|
+
if (isLibraryCampaignCardVarMappingIncomplete()) {
|
|
4019
|
+
return true;
|
|
4020
|
+
}
|
|
2186
4021
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
if (typeof v !== 'string') return !v;
|
|
2190
|
-
return v.trim() === '';
|
|
2191
|
-
});
|
|
2192
|
-
if (anyMissing) {
|
|
2193
|
-
return true;
|
|
2194
|
-
}
|
|
4022
|
+
if (smsFallbackBlocksDone()) {
|
|
4023
|
+
return true;
|
|
2195
4024
|
}
|
|
2196
|
-
|
|
4025
|
+
|
|
4026
|
+
if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
|
|
2197
4027
|
return true;
|
|
2198
4028
|
}
|
|
2199
|
-
if (isMediaTypeImage && rcsImageSrc === '') {
|
|
4029
|
+
if (!isCarouselType && isMediaTypeImage && rcsImageSrc === '') {
|
|
2200
4030
|
return true;
|
|
2201
4031
|
}
|
|
2202
|
-
if(isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
|
|
4032
|
+
if (!isCarouselType && isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
|
|
2203
4033
|
return true;
|
|
2204
4034
|
}
|
|
2205
4035
|
|
|
2206
4036
|
if (buttonType.includes(CTA)) {
|
|
2207
|
-
const hasValidButtons = suggestions.every(suggestion =>
|
|
4037
|
+
const hasValidButtons = suggestions.every(suggestion =>
|
|
2208
4038
|
suggestion.text && suggestion.url && !suggestionError && !forbiddenCharactersValidation(suggestion.text)
|
|
2209
4039
|
);
|
|
2210
4040
|
if (!hasValidButtons) {
|
|
2211
4041
|
return true;
|
|
2212
4042
|
}
|
|
2213
4043
|
}
|
|
2214
|
-
if (templateTitleError || templateDescError
|
|
4044
|
+
if (templateTitleError || templateDescError) {
|
|
4045
|
+
return true;
|
|
4046
|
+
}
|
|
4047
|
+
if (
|
|
4048
|
+
smsFallbackData?.content
|
|
4049
|
+
&& smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
|
|
4050
|
+
) {
|
|
2215
4051
|
return true;
|
|
2216
4052
|
}
|
|
2217
4053
|
return false;
|
|
@@ -2261,85 +4097,111 @@ const splitTemplateVarString = (str) => {
|
|
|
2261
4097
|
};
|
|
2262
4098
|
|
|
2263
4099
|
const getMainContent = () => {
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
return (
|
|
2268
|
-
<CapSlideBox
|
|
2269
|
-
show={showDltContainer}
|
|
2270
|
-
header={dltHeader}
|
|
2271
|
-
content={dltContent}
|
|
2272
|
-
handleClose={closeDltContainerHandler}
|
|
2273
|
-
size="size-xl"
|
|
2274
|
-
/>
|
|
2275
|
-
);
|
|
2276
|
-
}
|
|
2277
|
-
|
|
4100
|
+
// Slideboxes are rendered outside the page-level spinner to avoid
|
|
4101
|
+
// stacking/blur issues during initial loads.
|
|
4102
|
+
if (showDltContainer) return null;
|
|
2278
4103
|
return (
|
|
2279
4104
|
<>
|
|
2280
|
-
{templateStatus !== '' && (
|
|
2281
|
-
<
|
|
2282
|
-
{
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
4105
|
+
{templateStatus !== '' && (
|
|
4106
|
+
<CapRow useLegacy className="template-status-container">
|
|
4107
|
+
<CapColumn span={14}>
|
|
4108
|
+
<CapLabel type="label2">
|
|
4109
|
+
{formatMessage(messages.templateStatusLabel)}
|
|
4110
|
+
</CapLabel>
|
|
4111
|
+
|
|
4112
|
+
{!isHostInfoBip && templateStatus && (
|
|
4113
|
+
<CapAlert
|
|
4114
|
+
message={getTemplateStatusMessage()}
|
|
4115
|
+
type={getTemplateStatusType(templateStatus)}
|
|
4116
|
+
/>
|
|
4117
|
+
)}
|
|
4118
|
+
</CapColumn>
|
|
4119
|
+
</CapRow>
|
|
2292
4120
|
)}
|
|
2293
|
-
<CapRow className=
|
|
4121
|
+
<CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
|
|
2294
4122
|
<CapColumn span={14}>
|
|
2295
4123
|
{/* template name */}
|
|
2296
4124
|
{isFullMode && (
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
4125
|
+
isEditFlow ? (
|
|
4126
|
+
<div className="rcs-creative-name-readonly">
|
|
4127
|
+
<CapHeading type="h4">
|
|
4128
|
+
{formatMessage(globalMessages.creativeNameLabel)}
|
|
4129
|
+
</CapHeading>
|
|
4130
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
4131
|
+
{templateName || '-'}
|
|
4132
|
+
</CapHeading>
|
|
4133
|
+
</div>
|
|
4134
|
+
) : (
|
|
4135
|
+
<CapInput
|
|
4136
|
+
id="rcs_template_name_input"
|
|
4137
|
+
data-testid="template_name"
|
|
4138
|
+
onChange={onTemplateNameChange}
|
|
4139
|
+
errorMessage={templateNameError}
|
|
4140
|
+
placeholder={formatMessage(
|
|
4141
|
+
globalMessages.templateNamePlaceholder,
|
|
4142
|
+
)}
|
|
4143
|
+
value={templateName || ''}
|
|
4144
|
+
size="default"
|
|
4145
|
+
label={formatMessage(globalMessages.creativeNameLabel)}
|
|
4146
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
4147
|
+
/>
|
|
4148
|
+
)
|
|
4149
|
+
)}
|
|
4150
|
+
{isEditFlow ? (
|
|
4151
|
+
<div className="rcs-creative-name-readonly">
|
|
4152
|
+
<CapHeading type="h4">
|
|
4153
|
+
{formatMessage(messages.templateTypeLabel)}
|
|
4154
|
+
</CapHeading>
|
|
4155
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
4156
|
+
{TEMPLATE_TYPE_OPTIONS.find((option) => option.value === templateType)?.label || '-'}
|
|
4157
|
+
</CapHeading>
|
|
4158
|
+
</div>
|
|
4159
|
+
) : (
|
|
4160
|
+
<>
|
|
4161
|
+
{renderLabel('templateTypeLabel')}
|
|
4162
|
+
<ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
|
|
4163
|
+
<CapRadioGroup
|
|
4164
|
+
id="select-rcs-template-type"
|
|
4165
|
+
options={TEMPLATE_TYPE_OPTIONS}
|
|
4166
|
+
onChange={onTemplateTypeChange}
|
|
4167
|
+
value={templateType}
|
|
4168
|
+
disabled={!isFullMode}
|
|
4169
|
+
/>
|
|
4170
|
+
</ConfigProvider>
|
|
4171
|
+
</>
|
|
2310
4172
|
)}
|
|
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
4173
|
|
|
2322
|
-
{
|
|
2323
|
-
|
|
4174
|
+
{templateType === contentType.carousel ? (
|
|
4175
|
+
renderCarouselSection()
|
|
4176
|
+
) : (
|
|
2324
4177
|
<>
|
|
2325
|
-
{
|
|
2326
|
-
|
|
4178
|
+
{/* Show media only for rich_card */}
|
|
4179
|
+
{templateType === contentType.rich_card && (
|
|
4180
|
+
<>
|
|
4181
|
+
{renderLabel('mediaLabel')}
|
|
4182
|
+
<ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
|
|
2327
4183
|
<CapRadioGroup
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
4184
|
+
options={mediaRadioOptions || []}
|
|
4185
|
+
value={templateMediaType}
|
|
4186
|
+
onChange={onTemplateMediaTypeChange}
|
|
4187
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
4188
|
+
className="rcs-radio"
|
|
4189
|
+
/>
|
|
4190
|
+
</ConfigProvider>
|
|
2335
4191
|
<div className="rcs-container-image">
|
|
2336
|
-
|
|
2337
|
-
|
|
4192
|
+
{getMediaBasedComponent()}
|
|
4193
|
+
</div>
|
|
4194
|
+
</>
|
|
4195
|
+
)}
|
|
4196
|
+
{renderTextComponent()}
|
|
4197
|
+
</>
|
|
4198
|
+
)}
|
|
4199
|
+
{(!isEditFlow || hasMeaningfulSmsFallbackShape(smsFallbackData)) && (
|
|
4200
|
+
<>
|
|
4201
|
+
<CapDivider className="rcs-fallback-section-divider" />
|
|
4202
|
+
{renderFallBackSmsComponent()}
|
|
2338
4203
|
</>
|
|
2339
4204
|
)}
|
|
2340
|
-
{renderTextComponent()}
|
|
2341
|
-
<CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
|
|
2342
|
-
{renderFallBackSmsComponent()}
|
|
2343
4205
|
<div className="rcs-scroll-div" />
|
|
2344
4206
|
</CapColumn>
|
|
2345
4207
|
<CapColumn span={8} offset={2}>
|
|
@@ -2350,7 +4212,8 @@ const splitTemplateVarString = (str) => {
|
|
|
2350
4212
|
|
|
2351
4213
|
|
|
2352
4214
|
<div className="rcs-footer">
|
|
2353
|
-
{!
|
|
4215
|
+
{/* 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). */}
|
|
4216
|
+
{!isEditFlow && isFullMode && (
|
|
2354
4217
|
<>
|
|
2355
4218
|
<div className="button-disabled-tooltip-wrapper">
|
|
2356
4219
|
<CapButton
|
|
@@ -2358,7 +4221,9 @@ const splitTemplateVarString = (str) => {
|
|
|
2358
4221
|
disabled={isDisableDone()}
|
|
2359
4222
|
className="rcs-done-btn"
|
|
2360
4223
|
>
|
|
2361
|
-
<FormattedMessage
|
|
4224
|
+
<FormattedMessage
|
|
4225
|
+
{...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
|
|
4226
|
+
/>
|
|
2362
4227
|
</CapButton>
|
|
2363
4228
|
</div>
|
|
2364
4229
|
<CapTooltip
|
|
@@ -2371,7 +4236,6 @@ const splitTemplateVarString = (str) => {
|
|
|
2371
4236
|
className="rcs-test-preview-btn"
|
|
2372
4237
|
type="secondary"
|
|
2373
4238
|
disabled={true}
|
|
2374
|
-
style={{ marginLeft: "8px" }}
|
|
2375
4239
|
>
|
|
2376
4240
|
<FormattedMessage {...creativesMessages.testAndPreview} />
|
|
2377
4241
|
</CapButton>
|
|
@@ -2392,7 +4256,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2392
4256
|
</div>
|
|
2393
4257
|
</>
|
|
2394
4258
|
)}
|
|
2395
|
-
{isEditFlow && templateStatus === RCS_STATUSES.approved && (
|
|
4259
|
+
{isEditFlow && (isHostInfoBip || templateStatus === RCS_STATUSES.approved) && (
|
|
2396
4260
|
<>
|
|
2397
4261
|
<CapButton
|
|
2398
4262
|
onClick={handleTestAndPreview}
|
|
@@ -2404,51 +4268,6 @@ const splitTemplateVarString = (str) => {
|
|
|
2404
4268
|
</>
|
|
2405
4269
|
)}
|
|
2406
4270
|
</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
4271
|
</>
|
|
2453
4272
|
);
|
|
2454
4273
|
};
|
|
@@ -2457,23 +4276,57 @@ const splitTemplateVarString = (str) => {
|
|
|
2457
4276
|
<CapSpin spinning={loadingTags || spin}>
|
|
2458
4277
|
{getMainContent()}
|
|
2459
4278
|
</CapSpin>
|
|
4279
|
+
|
|
4280
|
+
{showDltContainer && (() => {
|
|
4281
|
+
const { dltHeader = '', dltContent = '' } = getDltSlideBoxContent() || {};
|
|
4282
|
+
return (
|
|
4283
|
+
<CapSlideBox
|
|
4284
|
+
show={showDltContainer}
|
|
4285
|
+
header={dltHeader}
|
|
4286
|
+
content={dltContent}
|
|
4287
|
+
handleClose={closeDltContainerHandler}
|
|
4288
|
+
size="size-xl"
|
|
4289
|
+
/>
|
|
4290
|
+
);
|
|
4291
|
+
})()}
|
|
4292
|
+
|
|
2460
4293
|
<TestAndPreviewSlidebox
|
|
2461
4294
|
show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
|
|
2462
4295
|
onClose={handleCloseTestAndPreview}
|
|
2463
|
-
formData={
|
|
2464
|
-
content={
|
|
4296
|
+
formData={testPreviewFormData}
|
|
4297
|
+
content={testAndPreviewContent}
|
|
2465
4298
|
currentChannel={RCS}
|
|
4299
|
+
orgUnitId={orgUnitId}
|
|
4300
|
+
rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
|
|
4301
|
+
smsFallbackContent={
|
|
4302
|
+
smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
|
|
4303
|
+
? {
|
|
4304
|
+
templateContent:
|
|
4305
|
+
smsFallbackData.templateContent || smsFallbackData.content || '',
|
|
4306
|
+
templateName: smsFallbackData.templateName || '',
|
|
4307
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
|
|
4308
|
+
? mergeRcsSmsFallbackVarMapLayers(
|
|
4309
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
4310
|
+
smsFallbackData,
|
|
4311
|
+
)
|
|
4312
|
+
: mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
|
|
4313
|
+
}
|
|
4314
|
+
: null
|
|
4315
|
+
}
|
|
4316
|
+
smsRegister={smsRegister}
|
|
2466
4317
|
/>
|
|
2467
4318
|
</>
|
|
2468
4319
|
);
|
|
2469
4320
|
};
|
|
2470
4321
|
|
|
4322
|
+
|
|
2471
4323
|
const mapStateToProps = createStructuredSelector({
|
|
2472
4324
|
rcsData: makeSelectRcs(),
|
|
2473
4325
|
accountData: makeSelectAccount(),
|
|
2474
4326
|
metaEntities: makeSelectMetaEntities(),
|
|
2475
4327
|
loadingTags: isLoadingMetaEntities(),
|
|
2476
4328
|
injectedTags: setInjectedTags(),
|
|
4329
|
+
currentOrgDetails: selectCurrentOrgDetails(),
|
|
2477
4330
|
});
|
|
2478
4331
|
|
|
2479
4332
|
const mapDispatchToProps = (dispatch) => ({
|