@capillarytech/creatives-library 8.0.329 → 8.0.330-alpha.1
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 +4 -0
- package/package.json +1 -1
- package/utils/commonUtils.js +19 -1
- package/utils/templateVarUtils.js +35 -6
- package/utils/tests/tagValidations.test.js +20 -0
- package/utils/tests/templateVarUtils.test.js +44 -0
- package/v2Components/CapActionButton/constants.js +7 -0
- package/v2Components/CapActionButton/index.js +167 -109
- package/v2Components/CapActionButton/index.scss +157 -6
- package/v2Components/CapActionButton/messages.js +19 -3
- package/v2Components/CapActionButton/tests/index.test.js +41 -17
- package/v2Components/CapTagList/index.js +28 -23
- package/v2Components/CapTagList/style.scss +29 -0
- package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
- package/v2Components/CapTagListWithInput/index.js +4 -0
- package/v2Components/CapWhatsappCTA/index.js +2 -0
- package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +1 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +323 -77
- package/v2Components/CommonTestAndPreview/index.js +49 -57
- package/v2Components/CommonTestAndPreview/messages.js +8 -0
- package/v2Components/CommonTestAndPreview/reducer.js +3 -1
- package/v2Components/CommonTestAndPreview/sagas.js +2 -1
- package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
- package/v2Components/FormBuilder/index.js +1 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +14 -3
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +16 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +143 -28
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +5 -0
- package/v2Components/mockdata.js +1 -0
- package/v2Containers/BeeEditor/index.js +19 -1
- package/v2Containers/CreativesContainer/SlideBoxContent.js +28 -1
- package/v2Containers/CreativesContainer/index.js +9 -3
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -0
- package/v2Containers/Email/index.js +78 -39
- package/v2Containers/Email/reducer.js +2 -2
- package/v2Containers/Email/sagas.js +3 -1
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -2
- package/v2Containers/Email/tests/sagas.test.js +230 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +6 -1
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -1
- package/v2Containers/EmailWrapper/index.js +4 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +2 -0
- package/v2Containers/MobilePush/Edit/index.js +2 -0
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/constants.js +85 -7
- package/v2Containers/Rcs/index.js +1592 -156
- package/v2Containers/Rcs/index.js.rej +1336 -0
- package/v2Containers/Rcs/index.scss +191 -0
- package/v2Containers/Rcs/index.scss.rej +74 -0
- package/v2Containers/Rcs/messages.js +28 -2
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +20 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +69178 -117691
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
- package/v2Containers/Rcs/tests/index.test.js +132 -94
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +67 -0
- package/v2Containers/Rcs/tests/utils.test.js +276 -38
- package/v2Containers/Rcs/utils.js +130 -7
- package/v2Containers/Sms/Edit/index.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +27 -0
- package/v2Containers/SmsTrai/Edit/messages.js +5 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/TagList/index.js +73 -20
- package/v2Containers/TagList/messages.js +4 -0
- package/v2Containers/TagList/tests/TagList.test.js +124 -20
- package/v2Containers/TagList/tests/mockdata.js +17 -0
- package/v2Containers/Templates/_templates.scss +99 -0
- package/v2Containers/Templates/index.js +29 -14
- package/v2Containers/Viber/index.js +3 -0
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
- package/v2Containers/WebPush/Create/index.js +10 -2
- package/v2Containers/Whatsapp/index.js +5 -0
- package/v2Containers/Zalo/index.js +2 -0
|
@@ -21,20 +21,42 @@ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
|
21
21
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
22
22
|
import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
23
23
|
import CapImage from '@capillarytech/cap-ui-library/CapImage';
|
|
24
|
+
import CapCard from '@capillarytech/cap-ui-library/CapCard';
|
|
24
25
|
import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
|
|
25
26
|
import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
|
|
27
|
+
import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
|
|
28
|
+
import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
|
|
29
|
+
import CapMenu from '@capillarytech/cap-ui-library/CapMenu';
|
|
26
30
|
import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
|
|
31
|
+
import CapTooltipWithInfo from '@capillarytech/cap-ui-library/CapTooltipWithInfo';
|
|
27
32
|
import CapError from '@capillarytech/cap-ui-library/CapError';
|
|
33
|
+
import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
|
|
28
34
|
import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
|
|
35
|
+
import CapLink from '@capillarytech/cap-ui-library/CapLink';
|
|
36
|
+
import CapTab from '@capillarytech/cap-ui-library/CapTab';
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
CAP_G01,
|
|
40
|
+
CAP_SPACE_04,
|
|
41
|
+
CAP_SPACE_16,
|
|
42
|
+
CAP_SPACE_24,
|
|
43
|
+
CAP_SPACE_28,
|
|
44
|
+
CAP_SPACE_32,
|
|
45
|
+
CAP_WHITE,
|
|
46
|
+
CAP_SECONDARY,
|
|
47
|
+
} from '@capillarytech/cap-ui-library/styled/variables';
|
|
29
48
|
|
|
30
49
|
import CapVideoUpload from '../../v2Components/CapVideoUpload';
|
|
31
50
|
import * as globalActions from '../Cap/actions';
|
|
32
51
|
import CapActionButton from '../../v2Components/CapActionButton';
|
|
52
|
+
import TemplatePreview from '../../v2Components/TemplatePreview';
|
|
33
53
|
import { makeSelectRcs, makeSelectAccount } from './selectors';
|
|
54
|
+
import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
|
|
34
55
|
import {
|
|
35
56
|
isLoadingMetaEntities,
|
|
36
57
|
makeSelectMetaEntities,
|
|
37
58
|
setInjectedTags,
|
|
59
|
+
selectCurrentOrgDetails,
|
|
38
60
|
} from '../Cap/selectors';
|
|
39
61
|
import * as RcsActions from './actions';
|
|
40
62
|
import { isAiContentBotDisabled } from '../../utils/common';
|
|
@@ -44,10 +66,12 @@ import {
|
|
|
44
66
|
normalizeLibraryLoadedTitleDesc,
|
|
45
67
|
mergeRcsSmsFallBackContentFromDetails,
|
|
46
68
|
mergeRcsSmsFallbackVarMapLayers,
|
|
69
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord,
|
|
47
70
|
pickFirstSmsFallbackTemplateString,
|
|
48
71
|
syncCardVarMappedSemanticsFromSlots,
|
|
49
72
|
hasMeaningfulSmsFallbackShape,
|
|
50
73
|
getLibrarySmsFallbackApiBaselineFromTemplateData,
|
|
74
|
+
pickRcsCardVarMappedEntries,
|
|
51
75
|
} from './rcsLibraryHydrationUtils';
|
|
52
76
|
import {
|
|
53
77
|
RCS,
|
|
@@ -65,11 +89,14 @@ import {
|
|
|
65
89
|
RCS_IMG_SIZE,
|
|
66
90
|
RCS_DLT_MODE,
|
|
67
91
|
CTA,
|
|
92
|
+
AI_CONTENT_BOT_DISABLED,
|
|
68
93
|
RCS_STATUSES,
|
|
69
94
|
TITLE_TEXT,
|
|
70
95
|
MESSAGE_TEXT,
|
|
71
96
|
ALLOWED_EXTENSIONS_VIDEO_REGEX,
|
|
72
97
|
RCS_VIDEO_SIZE,
|
|
98
|
+
TEMPLATE_HEADER_MAX_LENGTH,
|
|
99
|
+
TEMPLATE_MESSAGE_MAX_LENGTH,
|
|
73
100
|
RCS_THUMBNAIL_MIN_SIZE,
|
|
74
101
|
RCS_THUMBNAIL_MAX_SIZE,
|
|
75
102
|
contentType,
|
|
@@ -77,16 +104,30 @@ import {
|
|
|
77
104
|
rcsVarTestRegex,
|
|
78
105
|
RCS_IMAGE_DIMENSIONS,
|
|
79
106
|
RCS_TEXT_MESSAGE_MAX_LENGTH,
|
|
107
|
+
RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
|
|
80
108
|
RCS_RICH_CARD_MAX_LENGTH,
|
|
81
109
|
RCS_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
110
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS,
|
|
111
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
112
|
+
RCS_CAROUSEL_IMG_SIZE,
|
|
113
|
+
RCS_CAROUSEL_VIDEO_SIZE,
|
|
82
114
|
MAX_BUTTONS,
|
|
115
|
+
INITIAL_SUGGESTIONS,
|
|
83
116
|
INITIAL_SUGGESTIONS_DATA_STOP,
|
|
84
117
|
RCS_BUTTON_TYPES,
|
|
118
|
+
titletype,
|
|
119
|
+
descType,
|
|
85
120
|
STANDALONE,
|
|
86
121
|
VERTICAL,
|
|
87
122
|
SMALL,
|
|
88
123
|
MEDIUM,
|
|
89
124
|
RICHCARD,
|
|
125
|
+
HOST_INFOBIP,
|
|
126
|
+
HOST_ICS,
|
|
127
|
+
CAROUSEL_HEIGHT_OPTIONS,
|
|
128
|
+
CAROUSEL_WIDTH_OPTIONS,
|
|
129
|
+
STOP,
|
|
130
|
+
RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
|
|
90
131
|
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
91
132
|
RCS_NUMERIC_VAR_TOKEN_REGEX,
|
|
92
133
|
RCS_TAG_AREA_FIELD_TITLE,
|
|
@@ -118,10 +159,13 @@ import {
|
|
|
118
159
|
getTemplateStatusType,
|
|
119
160
|
normalizeCardVarMapped,
|
|
120
161
|
coalesceCardVarMappedToTemplate,
|
|
162
|
+
getRcsSemanticVarNamesSpanningTitleAndDesc,
|
|
121
163
|
resolveCardVarMappedSlotValue,
|
|
122
164
|
sanitizeCardVarMappedValue,
|
|
123
165
|
} from './utils';
|
|
124
166
|
|
|
167
|
+
|
|
168
|
+
const { Group: CapCheckboxGroup } = CapCheckbox;
|
|
125
169
|
export const Rcs = (props) => {
|
|
126
170
|
const {
|
|
127
171
|
intl,
|
|
@@ -146,6 +190,7 @@ export const Rcs = (props) => {
|
|
|
146
190
|
selectedOfferDetails,
|
|
147
191
|
eventContextTags,
|
|
148
192
|
accountData = {},
|
|
193
|
+
currentOrgDetails,
|
|
149
194
|
// TestAndPreviewSlidebox props
|
|
150
195
|
showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
|
|
151
196
|
handleTestAndPreview: propsHandleTestAndPreview,
|
|
@@ -153,6 +198,24 @@ export const Rcs = (props) => {
|
|
|
153
198
|
} = props || {};
|
|
154
199
|
const { formatMessage } = intl;
|
|
155
200
|
const { TextArea } = CapInput;
|
|
201
|
+
const { CapCustomCardList } = CapCustomCard;
|
|
202
|
+
|
|
203
|
+
// Defensive: React cannot render plain objects as children (crashes with
|
|
204
|
+
// "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
|
|
205
|
+
// can accidentally set an error state to an object (e.g. `{}`).
|
|
206
|
+
const normalizeErrorMessage = (err) => {
|
|
207
|
+
if (!err) return '';
|
|
208
|
+
if (React.isValidElement(err)) return err;
|
|
209
|
+
if (typeof err === 'string') return err;
|
|
210
|
+
if (typeof err === 'number') return String(err);
|
|
211
|
+
if (typeof err === 'object' && typeof err.message === 'string') return err.message;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.stringify(err);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return String(err);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
156
219
|
const [isEditFlow, setEditFlow] = useState(false);
|
|
157
220
|
const isEditLike = isEditFlow || !isFullMode;
|
|
158
221
|
const [tags, updateTags] = useState([]);
|
|
@@ -176,16 +239,28 @@ export const Rcs = (props) => {
|
|
|
176
239
|
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
|
|
177
240
|
const buttonType = RCS_BUTTON_TYPES.NONE;
|
|
178
241
|
const [suggestionError, setSuggestionError] = useState(true);
|
|
242
|
+
const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
|
|
179
243
|
const [templateType, setTemplateType] = useState('text_message');
|
|
180
244
|
const [titleVarMappedData, setTitleVarMappedData] = useState({});
|
|
181
245
|
const [descVarMappedData, setDescVarMappedData] = useState({});
|
|
182
246
|
const [titleTextAreaId, setTitleTextAreaId] = useState();
|
|
183
247
|
const [descTextAreaId, setDescTextAreaId] = useState();
|
|
184
248
|
const [assetList, setAssetList] = useState({});
|
|
249
|
+
const [assetListImage, setAssetListImage] = useState('');
|
|
185
250
|
const [rcsImageSrc, updateRcsImageSrc] = useState('');
|
|
186
251
|
const [rcsVideoSrc, setRcsVideoSrc] = useState({});
|
|
187
252
|
const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
|
|
188
253
|
const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
|
|
254
|
+
// Carousel (UI-only) state
|
|
255
|
+
const [selectedCarousel, setSelectedCarousel] = useState('');
|
|
256
|
+
const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
|
|
257
|
+
const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
|
|
258
|
+
const [carouselData, setCarouselData] = useState([]);
|
|
259
|
+
const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
|
|
260
|
+
const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
|
|
261
|
+
const [carouselResetNonce, setCarouselResetNonce] = useState(0);
|
|
262
|
+
const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
|
|
263
|
+
const [imageError, setImageError] = useState(null);
|
|
189
264
|
const [templateTitleError, setTemplateTitleError] = useState(false);
|
|
190
265
|
const [cardVarMapped, setCardVarMapped] = useState({});
|
|
191
266
|
/** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
|
|
@@ -236,11 +311,21 @@ export const Rcs = (props) => {
|
|
|
236
311
|
// Handle close Test and Preview slidebox
|
|
237
312
|
const handleCloseTestAndPreview = useCallback(() => {
|
|
238
313
|
setShowTestAndPreviewSlidebox(false);
|
|
314
|
+
setIsTestAndPreviewMode(false);
|
|
239
315
|
if (propsHandleCloseTestAndPreview) {
|
|
240
316
|
propsHandleCloseTestAndPreview();
|
|
241
317
|
}
|
|
242
318
|
}, [propsHandleCloseTestAndPreview]);
|
|
243
319
|
|
|
320
|
+
// Helper to get RCS orientation from selectedDimension
|
|
321
|
+
const getRcsOrientation = () => {
|
|
322
|
+
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
323
|
+
if (isMediaTypeImage) {
|
|
324
|
+
return RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
|
|
325
|
+
}
|
|
326
|
+
// For video
|
|
327
|
+
return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
|
|
328
|
+
};
|
|
244
329
|
/** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
|
|
245
330
|
const handleSmsFallbackEditorStateChange = useCallback((patch) => {
|
|
246
331
|
setSmsFallbackData((prev) => {
|
|
@@ -261,6 +346,12 @@ export const Rcs = (props) => {
|
|
|
261
346
|
const [accessToken, setAccessToken] = useState('');
|
|
262
347
|
const [hostName, setHostName] = useState('');
|
|
263
348
|
const [accountName, setAccountName] = useState('');
|
|
349
|
+
const isHostInfoBip = hostName === HOST_INFOBIP;
|
|
350
|
+
const isHostIcs = hostName === HOST_ICS;
|
|
351
|
+
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
|
|
354
|
+
}, [isHostIcs]);
|
|
264
355
|
useEffect(() => {
|
|
265
356
|
const accountObj = accountData.selectedRcsAccount || {};
|
|
266
357
|
if (!isEmpty(accountObj)) {
|
|
@@ -288,6 +379,668 @@ export const Rcs = (props) => {
|
|
|
288
379
|
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
289
380
|
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
290
381
|
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
382
|
+
const isCarouselType = templateType === contentType.carousel;
|
|
383
|
+
|
|
384
|
+
const MAX_RCS_CAROUSEL_ALLOWED = 10;
|
|
385
|
+
// Uploads for RCS are stored in redux under dynamic keys `uploadedAssetData${index}`.
|
|
386
|
+
// Carousel needs per-card indices; otherwise all cards "restore" the last uploaded asset
|
|
387
|
+
// and show the same media/thumbnail.
|
|
388
|
+
const RCS_CAROUSEL_ASSET_INDEX_BASE = 10; // keep away from standalone indices 0 (media) and 1 (thumbnail)
|
|
389
|
+
const getCarouselImageAssetIndex = (cardIndex) =>
|
|
390
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
|
|
391
|
+
const getCarouselVideoAssetIndex = (cardIndex) =>
|
|
392
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 1;
|
|
393
|
+
const getCarouselThumbnailAssetIndex = (cardIndex) =>
|
|
394
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 2;
|
|
395
|
+
const isThumbnailAssetIndex = (index) => {
|
|
396
|
+
if (index === 1) return true; // standalone thumbnail
|
|
397
|
+
if (index >= RCS_CAROUSEL_ASSET_INDEX_BASE) {
|
|
398
|
+
return ((index - RCS_CAROUSEL_ASSET_INDEX_BASE) % 3) === 2; // carousel thumbnail slot
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Carousel dimension key: `${HEIGHT}_${WIDTH}` (e.g., SHORT_SMALL)
|
|
404
|
+
const getCarouselDimensionKey = () => `${selectedCarouselHeight}_${selectedCarouselWidth}`;
|
|
405
|
+
|
|
406
|
+
const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
|
|
407
|
+
setCarouselData((prev = []) => {
|
|
408
|
+
const updated = cloneDeep(prev);
|
|
409
|
+
if (!updated?.[cardIndex]) return updated;
|
|
410
|
+
if (clearImage) updated[cardIndex].imageSrc = '';
|
|
411
|
+
if (clearVideo) updated[cardIndex].videoAsset = {};
|
|
412
|
+
if (clearThumb) updated[cardIndex].thumbnailSrc = '';
|
|
413
|
+
return updated;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
|
|
417
|
+
if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
|
|
418
|
+
if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const resetCarouselMediaForAllCards = () => {
|
|
422
|
+
setCarouselData((prev = []) => {
|
|
423
|
+
const updated = cloneDeep(prev);
|
|
424
|
+
updated.forEach((card) => {
|
|
425
|
+
if (!card) return;
|
|
426
|
+
card.imageSrc = '';
|
|
427
|
+
card.videoAsset = {};
|
|
428
|
+
card.thumbnailSrc = '';
|
|
429
|
+
});
|
|
430
|
+
return updated;
|
|
431
|
+
});
|
|
432
|
+
(carouselData || []).forEach((_, idx) => {
|
|
433
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
|
|
434
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
|
|
435
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
|
|
436
|
+
});
|
|
437
|
+
// Force tab panes to remount after global reset (some tab implementations cache inactive panes)
|
|
438
|
+
setCarouselResetNonce((n) => n + 1);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const RCS_CAROUSEL_INITIAL_CARD = {
|
|
442
|
+
title: '',
|
|
443
|
+
description: '',
|
|
444
|
+
mediaType: RCS_MEDIA_TYPES.IMAGE, // per-card
|
|
445
|
+
imageSrc: '',
|
|
446
|
+
videoAsset: {}, // CapVideoUpload object shape
|
|
447
|
+
thumbnailSrc: '',
|
|
448
|
+
suggestions: [],
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const RCS_CAROUSEL_INITIAL_FIRST_CARD = {
|
|
452
|
+
...RCS_CAROUSEL_INITIAL_CARD,
|
|
453
|
+
suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const ensureFirstCardDefaultPhoneSuggestions = (cards) => {
|
|
457
|
+
const next = cloneDeep(cards || []);
|
|
458
|
+
if (next.length === 0) return next;
|
|
459
|
+
const s = next[0].suggestions;
|
|
460
|
+
if (!Array.isArray(s) || s.length === 0) {
|
|
461
|
+
next[0] = {
|
|
462
|
+
...next[0],
|
|
463
|
+
suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return next;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
|
|
470
|
+
// updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
|
|
471
|
+
// suggestions typed since the last commit (stale closure).
|
|
472
|
+
const handleCarouselValueChange = (carouselIndex, fields) => {
|
|
473
|
+
setCarouselData((prev = []) => {
|
|
474
|
+
const updated = cloneDeep(prev);
|
|
475
|
+
if (!updated[carouselIndex]) return prev;
|
|
476
|
+
fields.forEach(({ fieldName, value }) => {
|
|
477
|
+
updated[carouselIndex][fieldName] = value;
|
|
478
|
+
});
|
|
479
|
+
return updated;
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const updateCarouselErrors = (carouselIndex, patch) => {
|
|
484
|
+
setCarouselErrors((prev = []) => {
|
|
485
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
486
|
+
next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
|
|
487
|
+
return next;
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const deleteCarouselCard = (index) => {
|
|
492
|
+
let nextIdx = 0;
|
|
493
|
+
flushSync(() => {
|
|
494
|
+
setCarouselData((prev = []) => {
|
|
495
|
+
const updated = cloneDeep(prev);
|
|
496
|
+
if (index < 0 || index >= updated.length) return updated;
|
|
497
|
+
updated.splice(index, 1);
|
|
498
|
+
nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
|
|
499
|
+
return ensureFirstCardDefaultPhoneSuggestions(updated);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
setCarouselErrors((prev = []) => {
|
|
503
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
504
|
+
next.splice(index, 1);
|
|
505
|
+
return next;
|
|
506
|
+
});
|
|
507
|
+
setActiveCarouselIndex(`${nextIdx}`);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const carouselButtonTextHasForbiddenChars = (value) => {
|
|
511
|
+
if (!value) return false;
|
|
512
|
+
if (value.includes('[') || value.includes(']')) return true;
|
|
513
|
+
const withoutValidVariables = value.replace(/\{\{[^}]*\}\}/g, '');
|
|
514
|
+
if (withoutValidVariables.includes('{') || withoutValidVariables.includes('}')) return true;
|
|
515
|
+
return false;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const isCompleteSavedCarouselSuggestion = (s) => {
|
|
519
|
+
if (!s || !s.isSaved) return false;
|
|
520
|
+
const text = (s.text || '').trim();
|
|
521
|
+
if (!text || !isValidText(text) || carouselButtonTextHasForbiddenChars(text)) return false;
|
|
522
|
+
if (s.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
|
|
523
|
+
return String(s.phoneNumber || '').length >= 5;
|
|
524
|
+
}
|
|
525
|
+
if (s.type === RCS_BUTTON_TYPES.CTA) {
|
|
526
|
+
const url = String(s.url || '').trim();
|
|
527
|
+
if (!url || url.length > URL_MAX_LENGTH) return false;
|
|
528
|
+
const subtype = s.urlType || RCS_CTA_URL_TYPE.STATIC;
|
|
529
|
+
if (subtype === RCS_CTA_URL_TYPE.DYNAMIC) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
if (!isUrl(url)) return false;
|
|
533
|
+
const varMatches = url.match(invalidVarRegex);
|
|
534
|
+
return !(varMatches && varMatches.length > 0);
|
|
535
|
+
}
|
|
536
|
+
if (s.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const isCarouselCardValid = (card, cardIndex) => {
|
|
543
|
+
if (!card) return false;
|
|
544
|
+
if (!card.title || !card.title.trim()) return false;
|
|
545
|
+
if ((card.title || '').length > TEMPLATE_TITLE_MAX_LENGTH) return false;
|
|
546
|
+
if (!card.description || !card.description.trim()) return false;
|
|
547
|
+
if ((card.description || '').length > RCS_RICH_CARD_MAX_LENGTH) return false;
|
|
548
|
+
let mediaOk = false;
|
|
549
|
+
if (card.mediaType === RCS_MEDIA_TYPES.IMAGE) {
|
|
550
|
+
mediaOk = !!(card.imageSrc && String(card.imageSrc).trim());
|
|
551
|
+
} else if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
|
|
552
|
+
const hasVideo = !!(card.videoAsset && card.videoAsset.videoSrc && String(card.videoAsset.videoSrc).trim());
|
|
553
|
+
const hasThumb = !!(card.thumbnailSrc && String(card.thumbnailSrc).trim());
|
|
554
|
+
mediaOk = hasVideo && hasThumb;
|
|
555
|
+
} else {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
if (!mediaOk) return false;
|
|
559
|
+
if (cardIndex === 0) {
|
|
560
|
+
const sugg = Array.isArray(card.suggestions) ? card.suggestions : [];
|
|
561
|
+
if (!sugg.some(isCompleteSavedCarouselSuggestion)) return false;
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const checkDisableAddCarouselButton = () => {
|
|
567
|
+
const idx = parseInt(activeCarouselIndex, 10);
|
|
568
|
+
const activeCard = carouselData?.[idx];
|
|
569
|
+
return !isCarouselCardValid(activeCard, idx);
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const addCarouselCard = () => {
|
|
573
|
+
let newIndex = 0;
|
|
574
|
+
flushSync(() => {
|
|
575
|
+
setCarouselData((prev = []) => {
|
|
576
|
+
const updated = cloneDeep(prev);
|
|
577
|
+
updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
|
|
578
|
+
newIndex = updated.length - 1;
|
|
579
|
+
return updated;
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
|
|
583
|
+
setActiveCarouselIndex(`${newIndex}`);
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Initialize carousel data when switching to carousel type
|
|
587
|
+
useEffect(() => {
|
|
588
|
+
if (!isCarouselType) return;
|
|
589
|
+
if (!carouselData || carouselData.length === 0) {
|
|
590
|
+
setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
|
|
591
|
+
setCarouselErrors([{}]);
|
|
592
|
+
setActiveCarouselIndex('0');
|
|
593
|
+
}
|
|
594
|
+
}, [isCarouselType]);
|
|
595
|
+
|
|
596
|
+
// keep derived carousel key in sync
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
if (!isCarouselType) return;
|
|
599
|
+
if (!selectedCarouselHeight || !selectedCarouselWidth) return;
|
|
600
|
+
// Required format: HEIGHT_WIDTH
|
|
601
|
+
setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
|
|
602
|
+
}, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
|
|
603
|
+
|
|
604
|
+
const renderCarouselDimensionSelection = () => {
|
|
605
|
+
if (!isCarouselType) return null;
|
|
606
|
+
return (
|
|
607
|
+
<CapRow className="rcs-carousel-dimension-section">
|
|
608
|
+
<CapRow gutter={16} className="rcs-carousel-dimension-row">
|
|
609
|
+
<CapColumn span={12}>
|
|
610
|
+
<CapHeading type="h4" className="rcs-carousel-dimension-label">Card height</CapHeading>
|
|
611
|
+
<CapSelect
|
|
612
|
+
id="rcs-carousel-height-select"
|
|
613
|
+
value={selectedCarouselHeight}
|
|
614
|
+
onChange={(val) => {
|
|
615
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
616
|
+
resetCarouselMediaForAllCards();
|
|
617
|
+
setSelectedCarouselHeight(val);
|
|
618
|
+
}}
|
|
619
|
+
options={CAROUSEL_HEIGHT_OPTIONS}
|
|
620
|
+
disabled={isEditFlow || !isFullMode}
|
|
621
|
+
/>
|
|
622
|
+
</CapColumn>
|
|
623
|
+
<CapColumn span={12}>
|
|
624
|
+
<CapHeading type="h4" className="rcs-carousel-dimension-label">Card width</CapHeading>
|
|
625
|
+
<CapSelect
|
|
626
|
+
id="rcs-carousel-width-select"
|
|
627
|
+
value={selectedCarouselWidth}
|
|
628
|
+
onChange={(val) => {
|
|
629
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
630
|
+
resetCarouselMediaForAllCards();
|
|
631
|
+
setSelectedCarouselWidth(val);
|
|
632
|
+
}}
|
|
633
|
+
options={CAROUSEL_WIDTH_OPTIONS}
|
|
634
|
+
disabled={isEditFlow || !isFullMode}
|
|
635
|
+
/>
|
|
636
|
+
</CapColumn>
|
|
637
|
+
</CapRow>
|
|
638
|
+
{!!selectedCarousel && (
|
|
639
|
+
<CapLabel type="label3" className="rcs-carousel-selected-dimension">
|
|
640
|
+
Selected: {selectedCarousel}
|
|
641
|
+
</CapLabel>
|
|
642
|
+
)}
|
|
643
|
+
</CapRow>
|
|
644
|
+
);
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// Reuse rich-card buttons UI per carousel card
|
|
648
|
+
const renderButtonComponentForCarouselCard = (cardIndex) => {
|
|
649
|
+
const card = carouselData?.[cardIndex] || {};
|
|
650
|
+
const suggestionsForCard = card.suggestions || [];
|
|
651
|
+
return (
|
|
652
|
+
<>
|
|
653
|
+
<CapHeader
|
|
654
|
+
className="rcs-button-cta"
|
|
655
|
+
title={(
|
|
656
|
+
<CapRow type="flex">
|
|
657
|
+
<CapHeading type="h4">
|
|
658
|
+
{formatMessage(messages.btnLabel)}
|
|
659
|
+
</CapHeading>
|
|
660
|
+
</CapRow>
|
|
661
|
+
)}
|
|
662
|
+
description={(
|
|
663
|
+
<CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
|
|
664
|
+
)}
|
|
665
|
+
/>
|
|
666
|
+
<CapActionButton
|
|
667
|
+
buttonType={RCS_BUTTON_TYPES.NONE}
|
|
668
|
+
updateButtonChange={(data, btnIndex) => {
|
|
669
|
+
// Match existing behavior: allow CapActionButton to manage save gating.
|
|
670
|
+
const updated = cloneDeep(suggestionsForCard);
|
|
671
|
+
if (btnIndex === MAX_BUTTONS) {
|
|
672
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: data }]);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
updated[btnIndex] = data;
|
|
676
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
|
|
677
|
+
}}
|
|
678
|
+
deleteButtonHandler={(btnIndex) => {
|
|
679
|
+
if (cardIndex === 0 && btnIndex === 0) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const savedCount = (suggestionsForCard || []).filter((x) => x && x.isSaved).length;
|
|
683
|
+
const target = (suggestionsForCard || []).find((s) => s && s.index === btnIndex);
|
|
684
|
+
if (cardIndex === 0 && target?.isSaved && savedCount <= 1) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const updated = cloneDeep(suggestionsForCard)
|
|
688
|
+
.filter((i) => i.index !== btnIndex)
|
|
689
|
+
.map((i, idx) => ({ ...i, index: idx }));
|
|
690
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
|
|
691
|
+
}}
|
|
692
|
+
suggestions={suggestionsForCard}
|
|
693
|
+
isEditFlow={isEditFlow}
|
|
694
|
+
isFullMode={isFullMode}
|
|
695
|
+
maxButtons={MAX_BUTTONS}
|
|
696
|
+
host={hostName}
|
|
697
|
+
minSavedSuggestions={cardIndex === 0 ? 1 : 0}
|
|
698
|
+
hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
|
|
699
|
+
/>
|
|
700
|
+
</>
|
|
701
|
+
);
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const renderCarouselCardMedia = (cardIndex) => {
|
|
705
|
+
const card = carouselData?.[cardIndex] || {};
|
|
706
|
+
const dimKey = getCarouselDimensionKey();
|
|
707
|
+
|
|
708
|
+
if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
|
|
709
|
+
return (
|
|
710
|
+
<>
|
|
711
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Video</CapHeading>
|
|
712
|
+
<CapVideoUpload
|
|
713
|
+
index={getCarouselVideoAssetIndex(cardIndex)}
|
|
714
|
+
allowedExtensionsRegex={ALLOWED_EXTENSIONS_VIDEO_REGEX}
|
|
715
|
+
videoSize={RCS_CAROUSEL_VIDEO_SIZE}
|
|
716
|
+
isFullMode={isFullMode}
|
|
717
|
+
uploadAsset={uploadRcsVideo}
|
|
718
|
+
uploadedAssetList={card.videoAsset || {}}
|
|
719
|
+
onVideoUploadUpdateAssestList={(_, val) => {
|
|
720
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'videoAsset', value: val }]);
|
|
721
|
+
}}
|
|
722
|
+
videoData={rcsData}
|
|
723
|
+
className="cap-custom-video-upload"
|
|
724
|
+
formClassName={"rcs-video-upload"}
|
|
725
|
+
channel={RCS}
|
|
726
|
+
errorMessage={formatMessage(messages.videoErrorMessage)}
|
|
727
|
+
showVideoNameAndDuration={false}
|
|
728
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
729
|
+
channelSpecificStyle={!isFullMode}
|
|
730
|
+
/>
|
|
731
|
+
|
|
732
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
|
|
733
|
+
<CapImageUpload
|
|
734
|
+
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
735
|
+
imgWidth={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.width}
|
|
736
|
+
imgHeight={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.height}
|
|
737
|
+
imgSize={RCS_THUMBNAIL_MAX_SIZE}
|
|
738
|
+
uploadAsset={uploadRcsImage}
|
|
739
|
+
isFullMode={isFullMode}
|
|
740
|
+
imageSrc={card.thumbnailSrc}
|
|
741
|
+
updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: val }])}
|
|
742
|
+
updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: '' }])}
|
|
743
|
+
minImgSize={RCS_THUMBNAIL_MIN_SIZE}
|
|
744
|
+
index={getCarouselThumbnailAssetIndex(cardIndex)}
|
|
745
|
+
className="cap-custom-image-upload"
|
|
746
|
+
key={`rcs-carousel-thumb-${cardIndex}-${dimKey}`}
|
|
747
|
+
imageData={rcsData}
|
|
748
|
+
channel={RCS}
|
|
749
|
+
channelSpecificStyle={!isFullMode}
|
|
750
|
+
skipDimensionValidation={true}
|
|
751
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
752
|
+
disabled={isEditFlow || !isFullMode}
|
|
753
|
+
/>
|
|
754
|
+
</>
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Default: IMAGE
|
|
759
|
+
return (
|
|
760
|
+
<>
|
|
761
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Image</CapHeading>
|
|
762
|
+
<CapImageUpload
|
|
763
|
+
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
764
|
+
imgWidth={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.width}
|
|
765
|
+
imgHeight={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.height}
|
|
766
|
+
imgSize={RCS_CAROUSEL_IMG_SIZE}
|
|
767
|
+
uploadAsset={uploadRcsImage}
|
|
768
|
+
isFullMode={isFullMode}
|
|
769
|
+
imageSrc={card.imageSrc}
|
|
770
|
+
updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: val }])}
|
|
771
|
+
updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: '' }])}
|
|
772
|
+
index={getCarouselImageAssetIndex(cardIndex)}
|
|
773
|
+
className="cap-custom-image-upload"
|
|
774
|
+
key={`rcs-carousel-image-${cardIndex}-${dimKey}`}
|
|
775
|
+
imageData={rcsData}
|
|
776
|
+
channel={RCS}
|
|
777
|
+
channelSpecificStyle={!isFullMode}
|
|
778
|
+
skipDimensionValidation={true}
|
|
779
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
780
|
+
disabled={isEditFlow || !isFullMode}
|
|
781
|
+
/>
|
|
782
|
+
</>
|
|
783
|
+
);
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const renderCarouselCardButtons = (cardIndex) => {
|
|
787
|
+
return renderButtonComponentForCarouselCard(cardIndex);
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const getCarouselTabPanes = () => {
|
|
791
|
+
return (carouselData || []).map((card, index) => {
|
|
792
|
+
return {
|
|
793
|
+
key: index,
|
|
794
|
+
tab: index + 1,
|
|
795
|
+
content: (
|
|
796
|
+
<CapCard
|
|
797
|
+
title={`Card ${index + 1}`}
|
|
798
|
+
extra={
|
|
799
|
+
!isEditFlow &&
|
|
800
|
+
(carouselData.length === 1 ? (
|
|
801
|
+
<CapTooltip title={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}>
|
|
802
|
+
<span className="button-disabled-tooltip-wrapper rcs-carousel-delete-tooltip-wrap">
|
|
803
|
+
<CapButton
|
|
804
|
+
className="rcs-carousel-card-delete"
|
|
805
|
+
type="flat"
|
|
806
|
+
onClick={() => deleteCarouselCard(index)}
|
|
807
|
+
disabled
|
|
808
|
+
aria-label={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}
|
|
809
|
+
>
|
|
810
|
+
<CapIcon type="delete" />
|
|
811
|
+
</CapButton>
|
|
812
|
+
</span>
|
|
813
|
+
</CapTooltip>
|
|
814
|
+
) : (
|
|
815
|
+
<CapButton
|
|
816
|
+
className="rcs-carousel-card-delete"
|
|
817
|
+
type="flat"
|
|
818
|
+
onClick={() => deleteCarouselCard(index)}
|
|
819
|
+
aria-label={formatMessage(globalMessages.delete)}
|
|
820
|
+
>
|
|
821
|
+
<CapIcon type="delete" />
|
|
822
|
+
</CapButton>
|
|
823
|
+
))
|
|
824
|
+
}
|
|
825
|
+
className="rcs-carousel-card"
|
|
826
|
+
>
|
|
827
|
+
{/* Media selection should be at top of card */}
|
|
828
|
+
<CapRow className="rcs-carousel-media-selection">
|
|
829
|
+
<CapColumn className="rcs-carousel-media-selection-heading">
|
|
830
|
+
<CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
|
|
831
|
+
</CapColumn>
|
|
832
|
+
<CapColumn>
|
|
833
|
+
<CapRadioGroup
|
|
834
|
+
id={`rcs-carousel-media-radio-${index}`}
|
|
835
|
+
options={mediaRadioOptions}
|
|
836
|
+
value={card.mediaType}
|
|
837
|
+
onChange={({ target: { value } }) => {
|
|
838
|
+
// Reset media fields when switching type
|
|
839
|
+
if (value === RCS_MEDIA_TYPES.IMAGE) {
|
|
840
|
+
// Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
|
|
841
|
+
clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
|
|
842
|
+
handleCarouselValueChange(index, [
|
|
843
|
+
{ fieldName: 'mediaType', value },
|
|
844
|
+
{ fieldName: 'videoAsset', value: {} },
|
|
845
|
+
{ fieldName: 'thumbnailSrc', value: '' },
|
|
846
|
+
]);
|
|
847
|
+
} else {
|
|
848
|
+
// Switching to VIDEO: clear image upload so it doesn't auto-restore.
|
|
849
|
+
clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
|
|
850
|
+
handleCarouselValueChange(index, [
|
|
851
|
+
{ fieldName: 'mediaType', value },
|
|
852
|
+
{ fieldName: 'imageSrc', value: '' },
|
|
853
|
+
]);
|
|
854
|
+
}
|
|
855
|
+
}}
|
|
856
|
+
disabled={isEditFlow || !isFullMode}
|
|
857
|
+
className="rcs-radio"
|
|
858
|
+
/>
|
|
859
|
+
</CapColumn>
|
|
860
|
+
</CapRow>
|
|
861
|
+
|
|
862
|
+
<CapRow className="rcs-carousel-media-upload">
|
|
863
|
+
{renderCarouselCardMedia(index)}
|
|
864
|
+
</CapRow>
|
|
865
|
+
|
|
866
|
+
{/* Title after media */}
|
|
867
|
+
<CapRow className="rcs-carousel-card-row">
|
|
868
|
+
<CapHeader
|
|
869
|
+
className="rcs-template-title-label"
|
|
870
|
+
title={<CapHeading type="h4">Card title</CapHeading>}
|
|
871
|
+
suffix={
|
|
872
|
+
(isEditFlow || !isFullMode) ? (
|
|
873
|
+
<TagList
|
|
874
|
+
label={formatMessage(globalMessages.addLabels)}
|
|
875
|
+
onTagSelect={onCarouselTagSelect}
|
|
876
|
+
location={location}
|
|
877
|
+
tags={getRcsTags()}
|
|
878
|
+
onContextChange={handleOnTagsContextChange}
|
|
879
|
+
injectedTags={injectedTags || {}}
|
|
880
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
881
|
+
/>
|
|
882
|
+
) : (
|
|
883
|
+
<CapButton
|
|
884
|
+
data-testid={`rcs-carousel-title-add-var-${index}`}
|
|
885
|
+
type="flat"
|
|
886
|
+
isAddBtn
|
|
887
|
+
onClick={() => appendVarToCarouselField(index, 'title')}
|
|
888
|
+
disabled={!!carouselErrors?.[index]?.title}
|
|
889
|
+
>
|
|
890
|
+
{formatMessage(messages.addVar)}
|
|
891
|
+
</CapButton>
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
/>
|
|
895
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
896
|
+
{(isEditFlow || !isFullMode) ? (
|
|
897
|
+
renderCarouselEditMessage(card.title || '')
|
|
898
|
+
) : (
|
|
899
|
+
<CapInput
|
|
900
|
+
value={card.title || ''}
|
|
901
|
+
placeholder={formatMessage(messages.templateTitlePlaceholder)}
|
|
902
|
+
onChange={({ target: { value } }) => {
|
|
903
|
+
let error = false;
|
|
904
|
+
if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
905
|
+
error = formatMessage(messages.templateHeaderLengthError);
|
|
906
|
+
} else {
|
|
907
|
+
error = variableErrorHandling(value);
|
|
908
|
+
}
|
|
909
|
+
updateCarouselErrors(index, { title: error });
|
|
910
|
+
handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
|
|
911
|
+
}}
|
|
912
|
+
disabled={isEditFlow || !isFullMode}
|
|
913
|
+
errorMessage={carouselErrors?.[index]?.title}
|
|
914
|
+
/>
|
|
915
|
+
)}
|
|
916
|
+
</CapRow>
|
|
917
|
+
</CapRow>
|
|
918
|
+
{!isEditFlow && (
|
|
919
|
+
<CapRow className="rcs-carousel-character-count-row">
|
|
920
|
+
{renderCarouselCharacterCount(
|
|
921
|
+
getCarouselTitleCharacterCount(index),
|
|
922
|
+
getTitleMaxLength(),
|
|
923
|
+
)}
|
|
924
|
+
</CapRow>
|
|
925
|
+
)}
|
|
926
|
+
|
|
927
|
+
{/* Description after title */}
|
|
928
|
+
<CapRow className="rcs-carousel-card-row">
|
|
929
|
+
<CapHeader
|
|
930
|
+
title={<CapHeading type="h4">Card body text</CapHeading>}
|
|
931
|
+
suffix={
|
|
932
|
+
(isEditFlow || !isFullMode) ? (
|
|
933
|
+
<TagList
|
|
934
|
+
label={formatMessage(globalMessages.addLabels)}
|
|
935
|
+
onTagSelect={onCarouselTagSelect}
|
|
936
|
+
location={location}
|
|
937
|
+
tags={getRcsTags()}
|
|
938
|
+
onContextChange={handleOnTagsContextChange}
|
|
939
|
+
injectedTags={injectedTags || {}}
|
|
940
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
941
|
+
/>
|
|
942
|
+
) : (
|
|
943
|
+
<CapButton
|
|
944
|
+
data-testid={`rcs-carousel-desc-add-var-${index}`}
|
|
945
|
+
type="flat"
|
|
946
|
+
isAddBtn
|
|
947
|
+
onClick={() => appendVarToCarouselField(index, 'description')}
|
|
948
|
+
disabled={!!carouselErrors?.[index]?.description}
|
|
949
|
+
>
|
|
950
|
+
{formatMessage(messages.addVar)}
|
|
951
|
+
</CapButton>
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
/>
|
|
955
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
956
|
+
{(isEditFlow || !isFullMode) ? (
|
|
957
|
+
renderCarouselEditMessage(card.description || '')
|
|
958
|
+
) : (
|
|
959
|
+
<TextArea
|
|
960
|
+
autosize={{ minRows: 3, maxRows: 5 }}
|
|
961
|
+
value={card.description || ''}
|
|
962
|
+
placeholder={formatMessage(messages.templateDescPlaceholder)}
|
|
963
|
+
onChange={({ target: { value } }) => {
|
|
964
|
+
let error = false;
|
|
965
|
+
if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
|
|
966
|
+
error = formatMessage(messages.templateMessageLengthError);
|
|
967
|
+
} else {
|
|
968
|
+
error = variableErrorHandling(value);
|
|
969
|
+
}
|
|
970
|
+
updateCarouselErrors(index, { description: error });
|
|
971
|
+
handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
|
|
972
|
+
}}
|
|
973
|
+
disabled={isEditFlow || !isFullMode}
|
|
974
|
+
errorMessage={
|
|
975
|
+
carouselErrors?.[index]?.description && (
|
|
976
|
+
<CapError className="rcs-template-message-error">
|
|
977
|
+
{carouselErrors[index].description}
|
|
978
|
+
</CapError>
|
|
979
|
+
)
|
|
980
|
+
}
|
|
981
|
+
/>
|
|
982
|
+
)}
|
|
983
|
+
</CapRow>
|
|
984
|
+
</CapRow>
|
|
985
|
+
{!isEditFlow && (
|
|
986
|
+
<CapRow className="rcs-carousel-character-count-row">
|
|
987
|
+
{renderCarouselCharacterCount(
|
|
988
|
+
getCarouselDescriptionCharacterCount(index),
|
|
989
|
+
getDescriptionMaxLength(),
|
|
990
|
+
)}
|
|
991
|
+
</CapRow>
|
|
992
|
+
)}
|
|
993
|
+
|
|
994
|
+
<CapDivider className="rcs-carousel-card-divider" />
|
|
995
|
+
{renderCarouselCardButtons(index)}
|
|
996
|
+
</CapCard>
|
|
997
|
+
),
|
|
998
|
+
};
|
|
999
|
+
});
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
const renderCarouselSection = () => {
|
|
1003
|
+
if (!isCarouselType) return null;
|
|
1004
|
+
|
|
1005
|
+
const operations = (
|
|
1006
|
+
<>
|
|
1007
|
+
<CapDivider type="vertical" />
|
|
1008
|
+
<CapButton
|
|
1009
|
+
onClick={addCarouselCard}
|
|
1010
|
+
type="flat"
|
|
1011
|
+
className="add-carousel-content-button"
|
|
1012
|
+
disabled={
|
|
1013
|
+
isEditFlow ||
|
|
1014
|
+
!isFullMode ||
|
|
1015
|
+
MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0) ||
|
|
1016
|
+
checkDisableAddCarouselButton()
|
|
1017
|
+
}
|
|
1018
|
+
>
|
|
1019
|
+
<CapIcon type="plus" />
|
|
1020
|
+
</CapButton>
|
|
1021
|
+
</>
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
return (
|
|
1025
|
+
<CapRow className="rcs-carousel-section">
|
|
1026
|
+
{renderCarouselDimensionSelection()}
|
|
1027
|
+
<CapRow className="rcs-carousel-tab">
|
|
1028
|
+
<CapTab
|
|
1029
|
+
key={`rcs-carousel-tab-${carouselResetNonce}`}
|
|
1030
|
+
defaultActiveKey="0"
|
|
1031
|
+
activeKey={activeCarouselIndex}
|
|
1032
|
+
tabBarExtraContent={operations}
|
|
1033
|
+
onChange={(key) => {
|
|
1034
|
+
setActiveCarouselIndex(`${key}`);
|
|
1035
|
+
// reset focused var when switching cards (as per requirement)
|
|
1036
|
+
setCarouselFocusedVarId('');
|
|
1037
|
+
}}
|
|
1038
|
+
panes={getCarouselTabPanes()}
|
|
1039
|
+
/>
|
|
1040
|
+
</CapRow>
|
|
1041
|
+
</CapRow>
|
|
1042
|
+
);
|
|
1043
|
+
};
|
|
291
1044
|
|
|
292
1045
|
const mediaRadioOptions = [
|
|
293
1046
|
{
|
|
@@ -296,11 +1049,16 @@ export const Rcs = (props) => {
|
|
|
296
1049
|
},
|
|
297
1050
|
{
|
|
298
1051
|
value: RCS_MEDIA_TYPES.VIDEO,
|
|
299
|
-
label: formatMessage(
|
|
1052
|
+
label: formatMessage(
|
|
1053
|
+
templateType === contentType.carousel
|
|
1054
|
+
? messages.carouselMediaVideoOption
|
|
1055
|
+
: messages.mediaVideo,
|
|
1056
|
+
),
|
|
300
1057
|
},
|
|
301
1058
|
];
|
|
302
1059
|
const aiContentBotDisabled = isAiContentBotDisabled();
|
|
303
1060
|
|
|
1061
|
+
|
|
304
1062
|
const updateButtonChange = (data, index) => {
|
|
305
1063
|
if (data && data.text) {
|
|
306
1064
|
const forbiddenError = forbiddenCharactersValidation(data.text);
|
|
@@ -366,10 +1124,16 @@ export const Rcs = (props) => {
|
|
|
366
1124
|
tagModule: getDefaultTags,
|
|
367
1125
|
isFullMode,
|
|
368
1126
|
}) || {};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1127
|
+
const unsupportedTagsLengthCheck =
|
|
1128
|
+
validationResponse?.unsupportedTags?.length > 0;
|
|
1129
|
+
const errorMsg =
|
|
1130
|
+
(unsupportedTagsLengthCheck &&
|
|
1131
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1132
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
1133
|
+
})) ||
|
|
1134
|
+
(validationResponse.isBraceError &&
|
|
1135
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
1136
|
+
false;
|
|
373
1137
|
if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
|
|
374
1138
|
if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
|
|
375
1139
|
};
|
|
@@ -382,10 +1146,60 @@ export const Rcs = (props) => {
|
|
|
382
1146
|
validateResolvedTagsForType(MESSAGE_TEXT);
|
|
383
1147
|
}, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
|
|
384
1148
|
|
|
1149
|
+
useEffect(() => {
|
|
1150
|
+
if (isFullMode || !isCarouselType) return;
|
|
1151
|
+
if (loadingTags || !tags || tags.length === 0) return;
|
|
1152
|
+
(carouselData || []).forEach((card, idx) => {
|
|
1153
|
+
['title', 'description'].forEach((field) => {
|
|
1154
|
+
const templateStr = card?.[field] || '';
|
|
1155
|
+
if (!templateStr) return;
|
|
1156
|
+
const resolved = resolveTemplateWithMap(templateStr);
|
|
1157
|
+
if (!resolved) {
|
|
1158
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
let contentForValidation = resolved;
|
|
1162
|
+
const placeholderTokens = templateStr.match(rcsVarRegex) || [];
|
|
1163
|
+
placeholderTokens.forEach((t) => {
|
|
1164
|
+
const escaped = t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
1165
|
+
contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
|
|
1166
|
+
});
|
|
1167
|
+
if (!contentForValidation.trim()) {
|
|
1168
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const validationResponse = validateTags({
|
|
1172
|
+
content: contentForValidation,
|
|
1173
|
+
tagsParam: tags,
|
|
1174
|
+
injectedTagsParams: injectedTags,
|
|
1175
|
+
location,
|
|
1176
|
+
tagModule: getDefaultTags,
|
|
1177
|
+
eventContextTags,
|
|
1178
|
+
isFullMode,
|
|
1179
|
+
}) || {};
|
|
1180
|
+
const errorMsg =
|
|
1181
|
+
(validationResponse?.unsupportedTags?.length > 0 &&
|
|
1182
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1183
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
1184
|
+
})) ||
|
|
1185
|
+
(validationResponse.isBraceError &&
|
|
1186
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
1187
|
+
false;
|
|
1188
|
+
updateCarouselErrors(idx, { [field]: errorMsg });
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
}, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
|
|
1192
|
+
|
|
385
1193
|
const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
|
|
386
1194
|
|
|
387
1195
|
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
388
1196
|
|
|
1197
|
+
/** Same `{{tag}}` in both title and description must not share one semantic map entry. */
|
|
1198
|
+
const rcsSpanningSemanticVarNames = useMemo(
|
|
1199
|
+
() => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
|
|
1200
|
+
[templateTitle, templateDesc],
|
|
1201
|
+
);
|
|
1202
|
+
|
|
389
1203
|
/** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
|
|
390
1204
|
const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
|
|
391
1205
|
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
@@ -404,6 +1218,10 @@ export const Rcs = (props) => {
|
|
|
404
1218
|
return null;
|
|
405
1219
|
};
|
|
406
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
|
|
1223
|
+
* multi-field variable resolution.
|
|
1224
|
+
*/
|
|
407
1225
|
const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
|
|
408
1226
|
if (!str) return '';
|
|
409
1227
|
const arr = splitTemplateVarStringRcs(str);
|
|
@@ -418,6 +1236,7 @@ export const Rcs = (props) => {
|
|
|
418
1236
|
key,
|
|
419
1237
|
globalSlot,
|
|
420
1238
|
isEditLike,
|
|
1239
|
+
rcsSpanningSemanticVarNames.has(key),
|
|
421
1240
|
);
|
|
422
1241
|
if (isNil(v) || String(v)?.trim?.() === '') return elem;
|
|
423
1242
|
return String(v);
|
|
@@ -426,12 +1245,45 @@ export const Rcs = (props) => {
|
|
|
426
1245
|
}).join('');
|
|
427
1246
|
};
|
|
428
1247
|
|
|
1248
|
+
const buildCarouselCardsForPreview = (cards = []) => (cards || []).map((card = {}) => {
|
|
1249
|
+
const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
|
|
1250
|
+
const videoSrc = card.videoAsset?.videoSrc || '';
|
|
1251
|
+
const resolvedTitle = !isFullMode ? resolveTemplateWithMap(card.title || '') : (card.title || '');
|
|
1252
|
+
const resolvedDesc = !isFullMode ? resolveTemplateWithMap(card.description || '') : (card.description || '');
|
|
1253
|
+
return {
|
|
1254
|
+
mediaType: (card.mediaType || '').toLowerCase(),
|
|
1255
|
+
imageSrc: card.imageSrc || '',
|
|
1256
|
+
videoSrc,
|
|
1257
|
+
videoPreviewImg: videoThumb,
|
|
1258
|
+
title: resolvedTitle,
|
|
1259
|
+
bodyText: resolvedDesc,
|
|
1260
|
+
suggestions: card.suggestions || [],
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
1263
|
+
|
|
429
1264
|
/**
|
|
430
1265
|
* Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
|
|
431
1266
|
* (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
|
|
432
1267
|
* TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
|
|
433
1268
|
*/
|
|
434
1269
|
const getTemplateContent = useCallback(() => {
|
|
1270
|
+
if (templateType === contentType.carousel) {
|
|
1271
|
+
const carouselDimKey = getCarouselDimensionKey();
|
|
1272
|
+
const carouselImgDims =
|
|
1273
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
1274
|
+
const carouselVidDims =
|
|
1275
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
1276
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
1277
|
+
return {
|
|
1278
|
+
carouselData: buildCarouselCardsForPreview(carouselData),
|
|
1279
|
+
carouselPreviewDimensions: {
|
|
1280
|
+
imageWidth: carouselImgDims.width,
|
|
1281
|
+
imageHeight: carouselImgDims.height,
|
|
1282
|
+
videoThumbWidth: carouselVidDims.width,
|
|
1283
|
+
videoThumbHeight: carouselVidDims.height,
|
|
1284
|
+
},
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
435
1287
|
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
436
1288
|
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
437
1289
|
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
@@ -484,21 +1336,42 @@ export const Rcs = (props) => {
|
|
|
484
1336
|
isFullMode,
|
|
485
1337
|
isEditFlow,
|
|
486
1338
|
cardVarMapped,
|
|
1339
|
+
rcsSpanningSemanticVarNames,
|
|
1340
|
+
carouselData,
|
|
1341
|
+
selectedCarouselHeight,
|
|
1342
|
+
selectedCarouselWidth,
|
|
487
1343
|
]);
|
|
488
1344
|
|
|
489
1345
|
const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
|
|
490
1346
|
|
|
491
1347
|
const paramObj = params || {};
|
|
492
1348
|
useEffect(() => {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
1349
|
+
const { id } = paramObj;
|
|
1350
|
+
if (id && isFullMode) {
|
|
1351
|
+
setSpin(true);
|
|
1352
|
+
actions.getTemplateDetails(id, setSpin);
|
|
1353
|
+
} else if (!id && isFullMode) {
|
|
1354
|
+
// Create New: clear standalone media and ALL possible carousel card Redux slots.
|
|
1355
|
+
// Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
|
|
1356
|
+
updateRcsImageSrc('');
|
|
1357
|
+
setRcsVideoSrc({});
|
|
1358
|
+
setRcsThumbnailSrc('');
|
|
1359
|
+
setAssetList({});
|
|
1360
|
+
setEditFlow(false);
|
|
1361
|
+
actions.clearRcsMediaAsset(0);
|
|
1362
|
+
actions.clearRcsMediaAsset(1);
|
|
1363
|
+
for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
|
|
1364
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
|
|
1365
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
|
|
1366
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
|
|
1367
|
+
}
|
|
1368
|
+
setCarouselData([]);
|
|
1369
|
+
setCarouselErrors([]);
|
|
1370
|
+
}
|
|
1371
|
+
return () => {
|
|
1372
|
+
actions.clearEditResponse();
|
|
1373
|
+
};
|
|
1374
|
+
}, [paramObj.id]);
|
|
502
1375
|
|
|
503
1376
|
useEffect(() => {
|
|
504
1377
|
if (!(isEditFlow || !isFullMode)) return;
|
|
@@ -514,7 +1387,7 @@ export const Rcs = (props) => {
|
|
|
514
1387
|
const nextVarMap = {};
|
|
515
1388
|
let varOrdinal = 0;
|
|
516
1389
|
arr.forEach((elem, idx) => {
|
|
517
|
-
//
|
|
1390
|
+
// Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
|
|
518
1391
|
if (rcsVarTestRegex.test(elem)) {
|
|
519
1392
|
const id = `${elem}_${idx}`;
|
|
520
1393
|
const varName = getVarNameFromToken(elem);
|
|
@@ -525,6 +1398,7 @@ export const Rcs = (props) => {
|
|
|
525
1398
|
varName,
|
|
526
1399
|
globalSlot,
|
|
527
1400
|
isEditLike,
|
|
1401
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
528
1402
|
);
|
|
529
1403
|
nextVarMap[id] = mappedValue;
|
|
530
1404
|
}
|
|
@@ -534,14 +1408,13 @@ export const Rcs = (props) => {
|
|
|
534
1408
|
|
|
535
1409
|
initField(templateTitle, setTitleVarMappedData, 0);
|
|
536
1410
|
initField(templateDesc, setDescVarMappedData, titleTokenCount);
|
|
537
|
-
}, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
|
|
1411
|
+
}, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
|
|
538
1412
|
|
|
539
1413
|
useEffect(() => {
|
|
540
1414
|
if (!isEditFlow && isFullMode) {
|
|
541
1415
|
setRcsVideoSrc({});
|
|
542
1416
|
updateRcsImageSrc('');
|
|
543
|
-
|
|
544
|
-
updateRcsThumbnailSrc('');
|
|
1417
|
+
setRcsThumbnailSrc('');
|
|
545
1418
|
setAssetList({});
|
|
546
1419
|
}
|
|
547
1420
|
}, [templateMediaType]);
|
|
@@ -584,6 +1457,7 @@ export const Rcs = (props) => {
|
|
|
584
1457
|
if (mediaType) {
|
|
585
1458
|
setTemplateMediaType(mediaType);
|
|
586
1459
|
}
|
|
1460
|
+
const tempOrientation = cardSettings.cardOrientation;
|
|
587
1461
|
const tempAlignment = cardSettings.mediaAlignment;
|
|
588
1462
|
const tempHeight = mediaData.height;
|
|
589
1463
|
|
|
@@ -633,6 +1507,12 @@ export const Rcs = (props) => {
|
|
|
633
1507
|
details,
|
|
634
1508
|
'versions.base.content.RCS.rcsContent.cardContent[0]',
|
|
635
1509
|
);
|
|
1510
|
+
const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
|
|
1511
|
+
const cardType = (rcsContent?.cardType || '').toString().toLowerCase();
|
|
1512
|
+
|
|
1513
|
+
setEditFlow(true);
|
|
1514
|
+
setTemplateName(details?.name || details?.creativeName || '');
|
|
1515
|
+
|
|
636
1516
|
const cardFromTop = get(details, 'rcsContent.cardContent[0]');
|
|
637
1517
|
const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
|
|
638
1518
|
const cardVarMappedFromCardContent =
|
|
@@ -702,16 +1582,83 @@ export const Rcs = (props) => {
|
|
|
702
1582
|
}
|
|
703
1583
|
return hydratedCardVarMappedResult;
|
|
704
1584
|
});
|
|
1585
|
+
|
|
1586
|
+
if (cardType === contentType.carousel) {
|
|
1587
|
+
|
|
1588
|
+
setTemplateType(contentType.carousel);
|
|
1589
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1590
|
+
const cardSettings = rcsContent?.cardSettings || {};
|
|
1591
|
+
const cardWidth = cardSettings?.cardWidth || SMALL;
|
|
1592
|
+
setSelectedCarouselWidth(cardWidth);
|
|
1593
|
+
|
|
1594
|
+
const cards = Array.isArray(rcsContent?.cardContent) ? rcsContent.cardContent : [];
|
|
1595
|
+
const firstHeight = cards?.[0]?.media?.height || MEDIUM;
|
|
1596
|
+
setSelectedCarouselHeight(firstHeight);
|
|
1597
|
+
setSelectedCarousel(`${firstHeight}_${cardWidth}`);
|
|
1598
|
+
setActiveCarouselIndex('0');
|
|
1599
|
+
|
|
1600
|
+
const hydratedCards = cards.map((c = {}, idx) => {
|
|
1601
|
+
const mediaType = c.mediaType;
|
|
1602
|
+
const media = c.media || {};
|
|
1603
|
+
const mediaUrl = media.mediaUrl || '';
|
|
1604
|
+
const thumbUrl = media.thumbnailUrl || '';
|
|
1605
|
+
const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
|
|
1606
|
+
const suggestions = idx === 0 && rawSuggestions.length === 0
|
|
1607
|
+
? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
|
|
1608
|
+
: rawSuggestions;
|
|
1609
|
+
return {
|
|
1610
|
+
title: c.title || '',
|
|
1611
|
+
description: c.description || '',
|
|
1612
|
+
mediaType,
|
|
1613
|
+
imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
|
|
1614
|
+
videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
|
|
1615
|
+
videoSrc: mediaUrl,
|
|
1616
|
+
previewUrl: thumbUrl,
|
|
1617
|
+
videoThumbnail: thumbUrl,
|
|
1618
|
+
videoName: c?.media?.videoName || '',
|
|
1619
|
+
} : {},
|
|
1620
|
+
thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
|
|
1621
|
+
suggestions,
|
|
1622
|
+
};
|
|
1623
|
+
});
|
|
1624
|
+
setCarouselData(
|
|
1625
|
+
hydratedCards.length > 0
|
|
1626
|
+
? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
|
|
1627
|
+
: [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
|
|
1628
|
+
);
|
|
1629
|
+
setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
|
|
1630
|
+
|
|
1631
|
+
// Status bar uses first card's Status, keep existing behavior.
|
|
1632
|
+
if (isHostInfoBip) {
|
|
1633
|
+
setTemplateStatus('');
|
|
1634
|
+
} else {
|
|
1635
|
+
const firstCard = cards?.[0] || {};
|
|
1636
|
+
const cardForCarouselStatus = {
|
|
1637
|
+
...firstCard,
|
|
1638
|
+
Status:
|
|
1639
|
+
firstCard.Status
|
|
1640
|
+
?? firstCard.status
|
|
1641
|
+
?? firstCard.approvalStatus
|
|
1642
|
+
?? get(details, 'templateStatus')
|
|
1643
|
+
?? get(details, 'approvalStatus')
|
|
1644
|
+
?? get(details, 'creativeStatus')
|
|
1645
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1646
|
+
?? '',
|
|
1647
|
+
};
|
|
1648
|
+
templateStatusHelper(cardForCarouselStatus);
|
|
1649
|
+
}
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
705
1653
|
const mediaType =
|
|
706
1654
|
card0.mediaType
|
|
707
1655
|
|| get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
|
|
708
|
-
if (mediaType === RCS_MEDIA_TYPES.NONE) {
|
|
1656
|
+
if (cardType !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
|
|
709
1657
|
setTemplateType(contentType.text_message);
|
|
710
|
-
} else {
|
|
1658
|
+
} else if (cardType !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
|
|
711
1659
|
setTemplateType(contentType.rich_card);
|
|
712
1660
|
}
|
|
713
|
-
|
|
714
|
-
setTemplateName(details?.name || details?.creativeName || '');
|
|
1661
|
+
|
|
715
1662
|
const loadedTitle = loadedTitleForMap;
|
|
716
1663
|
const loadedDesc = loadedDescForMap;
|
|
717
1664
|
const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
|
|
@@ -740,7 +1687,11 @@ export const Rcs = (props) => {
|
|
|
740
1687
|
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
741
1688
|
?? '',
|
|
742
1689
|
};
|
|
743
|
-
|
|
1690
|
+
if (isHostInfoBip) {
|
|
1691
|
+
setTemplateStatus('');
|
|
1692
|
+
} else {
|
|
1693
|
+
templateStatusHelper(cardForStatus);
|
|
1694
|
+
}
|
|
744
1695
|
const mediaData =
|
|
745
1696
|
card0.media != null && card0.media !== ''
|
|
746
1697
|
? card0.media
|
|
@@ -780,12 +1731,17 @@ export const Rcs = (props) => {
|
|
|
780
1731
|
typeof smsFallbackContent.unicodeValidity === 'boolean'
|
|
781
1732
|
? smsFallbackContent.unicodeValidity
|
|
782
1733
|
: (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
|
|
1734
|
+
const registeredSenderIdsFromApi =
|
|
1735
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
|
|
783
1736
|
const nextSmsState = {
|
|
784
1737
|
templateName: smsFallbackContent.smsTemplateName || '',
|
|
785
1738
|
content: fallbackMessage,
|
|
786
1739
|
templateContent: fallbackMessage,
|
|
787
1740
|
unicodeValidity: unicodeFromApi,
|
|
788
1741
|
...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
|
|
1742
|
+
...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
|
|
1743
|
+
? { registeredSenderIds: registeredSenderIdsFromApi }
|
|
1744
|
+
: {}),
|
|
789
1745
|
};
|
|
790
1746
|
const hydrationKey = JSON.stringify({
|
|
791
1747
|
creativeKey: details._id || details.name || details.creativeName || '',
|
|
@@ -793,6 +1749,10 @@ export const Rcs = (props) => {
|
|
|
793
1749
|
content: nextSmsState.content,
|
|
794
1750
|
unicodeValidity: nextSmsState.unicodeValidity,
|
|
795
1751
|
varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
|
|
1752
|
+
senderIds:
|
|
1753
|
+
Array.isArray(registeredSenderIdsFromApi)
|
|
1754
|
+
? registeredSenderIdsFromApi.join('\u001f')
|
|
1755
|
+
: '',
|
|
796
1756
|
});
|
|
797
1757
|
if (
|
|
798
1758
|
isFullMode
|
|
@@ -806,7 +1766,7 @@ export const Rcs = (props) => {
|
|
|
806
1766
|
setSmsFallbackData(null);
|
|
807
1767
|
}
|
|
808
1768
|
}
|
|
809
|
-
}, [rcsHydrationDetails, isFullMode]);
|
|
1769
|
+
}, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
|
|
810
1770
|
|
|
811
1771
|
useEffect(() => {
|
|
812
1772
|
if (templateType === contentType.text_message) {
|
|
@@ -884,6 +1844,7 @@ export const Rcs = (props) => {
|
|
|
884
1844
|
query.context = getDefaultTags;
|
|
885
1845
|
}
|
|
886
1846
|
fetchTagSchemaIfNewQuery(query);
|
|
1847
|
+
globalActions.fetchSchemaForEntity(query);
|
|
887
1848
|
};
|
|
888
1849
|
|
|
889
1850
|
const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
|
|
@@ -892,58 +1853,87 @@ export const Rcs = (props) => {
|
|
|
892
1853
|
return templateStr.replace(re, `{{${tagName}}}`);
|
|
893
1854
|
};
|
|
894
1855
|
|
|
895
|
-
const onTagSelect = (
|
|
896
|
-
if (!
|
|
897
|
-
const
|
|
898
|
-
if (
|
|
899
|
-
const
|
|
900
|
-
if (
|
|
901
|
-
const
|
|
902
|
-
const
|
|
903
|
-
if (!
|
|
904
|
-
const
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1856
|
+
const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
|
|
1857
|
+
if (!varSegmentCompositeDomId) return;
|
|
1858
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
1859
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
1860
|
+
const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
|
|
1861
|
+
if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
|
|
1862
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
1863
|
+
const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
1864
|
+
if (!semanticOrNumericVarName) return;
|
|
1865
|
+
const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
|
|
1866
|
+
const templateStringForField =
|
|
1867
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
|
|
1868
|
+
const titleOrMessageFieldType =
|
|
1869
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
|
|
1870
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
1871
|
+
varSegmentCompositeDomId,
|
|
1872
|
+
templateStringForField,
|
|
1873
|
+
titleOrMessageFieldType,
|
|
1874
|
+
);
|
|
1875
|
+
const cardVarMappedNumericSlotKey =
|
|
1876
|
+
globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
|
|
1877
|
+
? String(globalVarSlotIndexZeroBased + 1)
|
|
1878
|
+
: null;
|
|
1879
|
+
|
|
1880
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
1881
|
+
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
1882
|
+
if (isNumericPlaceholderSlot) {
|
|
1883
|
+
const existingValueBeforeAppend = (
|
|
1884
|
+
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
1885
|
+
).toString();
|
|
1886
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1887
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
1888
|
+
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
916
1889
|
} else {
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1890
|
+
// Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
|
|
1891
|
+
// "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
|
|
1892
|
+
// read/write the global numeric slot only and drop the shared semantic key.
|
|
1893
|
+
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
1894
|
+
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
1895
|
+
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
1896
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1897
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
1898
|
+
if (cardVarMappedNumericSlotKey) {
|
|
1899
|
+
updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
|
|
926
1900
|
} else {
|
|
927
|
-
|
|
1901
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
928
1902
|
}
|
|
929
1903
|
}
|
|
930
|
-
return
|
|
1904
|
+
return updatedCardVarMapped;
|
|
931
1905
|
});
|
|
932
1906
|
|
|
933
|
-
if (
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1907
|
+
if (
|
|
1908
|
+
isNumericPlaceholderSlot
|
|
1909
|
+
&& (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
|
|
1910
|
+
) {
|
|
1911
|
+
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
1912
|
+
setTemplateTitle((previousTitle) => {
|
|
1913
|
+
const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
1914
|
+
previousTitle || '',
|
|
1915
|
+
semanticOrNumericVarName,
|
|
1916
|
+
selectedTagNameFromPicker,
|
|
1917
|
+
);
|
|
1918
|
+
if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
|
|
1919
|
+
setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
|
|
1920
|
+
// 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
|
|
1921
|
+
setRcsVarSegmentEditorRemountKey((k) => k + 1);
|
|
1922
|
+
return titleAfterReplacingNumericPlaceholder;
|
|
940
1923
|
});
|
|
941
1924
|
} else {
|
|
942
|
-
setTemplateDesc((
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1925
|
+
setTemplateDesc((previousDescription) => {
|
|
1926
|
+
const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
1927
|
+
previousDescription || '',
|
|
1928
|
+
semanticOrNumericVarName,
|
|
1929
|
+
selectedTagNameFromPicker,
|
|
1930
|
+
);
|
|
1931
|
+
if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
|
|
1932
|
+
return previousDescription;
|
|
1933
|
+
}
|
|
1934
|
+
setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
|
|
1935
|
+
setRcsVarSegmentEditorRemountKey((k) => k + 1);
|
|
1936
|
+
return descriptionAfterReplacingNumericPlaceholder;
|
|
947
1937
|
});
|
|
948
1938
|
}
|
|
949
1939
|
}
|
|
@@ -953,6 +1943,31 @@ export const Rcs = (props) => {
|
|
|
953
1943
|
|
|
954
1944
|
const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
|
|
955
1945
|
|
|
1946
|
+
const onCarouselTagSelect = (data) => {
|
|
1947
|
+
if (!carouselFocusedVarId) return;
|
|
1948
|
+
const sep = carouselFocusedVarId.lastIndexOf('_');
|
|
1949
|
+
if (sep === -1) return;
|
|
1950
|
+
const token = carouselFocusedVarId.slice(0, sep);
|
|
1951
|
+
const variableName = getVarNameFromToken(token);
|
|
1952
|
+
if (!variableName) return;
|
|
1953
|
+
setCardVarMapped((prev) => {
|
|
1954
|
+
const base = (prev?.[variableName] ?? '').toString();
|
|
1955
|
+
const nextVal = `${base}{{${data}}}`;
|
|
1956
|
+
return {
|
|
1957
|
+
...(prev || {}),
|
|
1958
|
+
[variableName]: nextVal,
|
|
1959
|
+
};
|
|
1960
|
+
});
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
const onTagSelectFallback = (data) => {
|
|
1964
|
+
const tempMsg = `${fallbackMessage}{{${data}}}`;
|
|
1965
|
+
const error = fallbackMessageErrorHandler(tempMsg);
|
|
1966
|
+
setFallbackMessage(tempMsg);
|
|
1967
|
+
setFallbackMessageError(error);
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
|
|
956
1971
|
//removing optout tag for rcs
|
|
957
1972
|
const getRcsTags = () => {
|
|
958
1973
|
const tempTags = cloneDeep(tags);
|
|
@@ -988,15 +2003,14 @@ export const Rcs = (props) => {
|
|
|
988
2003
|
value: contentType.rich_card,
|
|
989
2004
|
label: formatMessage(messages.richCard),
|
|
990
2005
|
},
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
},
|
|
2006
|
+
...(!isHostInfoBip
|
|
2007
|
+
? [
|
|
2008
|
+
{
|
|
2009
|
+
value: contentType.carousel,
|
|
2010
|
+
label: formatMessage(messages.carousel),
|
|
2011
|
+
},
|
|
2012
|
+
]
|
|
2013
|
+
: []),
|
|
1000
2014
|
];
|
|
1001
2015
|
|
|
1002
2016
|
const onTemplateNameChange = ({ target: { value } }) => {
|
|
@@ -1007,6 +2021,10 @@ export const Rcs = (props) => {
|
|
|
1007
2021
|
|
|
1008
2022
|
const onTemplateTypeChange = ({ target: { value } }) => {
|
|
1009
2023
|
setTemplateType(value);
|
|
2024
|
+
// Carousel has per-card media; keep template-level media type neutral.
|
|
2025
|
+
if (value === contentType.carousel) {
|
|
2026
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
2027
|
+
}
|
|
1010
2028
|
};
|
|
1011
2029
|
|
|
1012
2030
|
|
|
@@ -1027,8 +2045,39 @@ export const Rcs = (props) => {
|
|
|
1027
2045
|
const onTemplateMediaTypeChange = ({ target: { value } }) => {
|
|
1028
2046
|
setTemplateMediaType(value);
|
|
1029
2047
|
};
|
|
1030
|
-
|
|
1031
|
-
|
|
2048
|
+
const renderedRCSEditMessage = (descArray, type) => {
|
|
2049
|
+
const renderArray = [];
|
|
2050
|
+
if (descArray?.length) {
|
|
2051
|
+
descArray.forEach((elem, index) => {
|
|
2052
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
2053
|
+
// Variable input
|
|
2054
|
+
renderArray.push(
|
|
2055
|
+
<TextArea
|
|
2056
|
+
id={`${elem}_${index}`}
|
|
2057
|
+
key={`${elem}_${index}`}
|
|
2058
|
+
placeholder={`enter the value for ${elem}`}
|
|
2059
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2060
|
+
onChange={e => textAreaValueChange(e, type)}
|
|
2061
|
+
value={textAreaValue(index, type)}
|
|
2062
|
+
onFocus={(e) => setTextAreaId(e, type)}
|
|
2063
|
+
/>
|
|
2064
|
+
);
|
|
2065
|
+
} else if (elem) {
|
|
2066
|
+
// Static text
|
|
2067
|
+
renderArray.push(
|
|
2068
|
+
<TextArea
|
|
2069
|
+
key={`static_${index}`}
|
|
2070
|
+
value={elem}
|
|
2071
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2072
|
+
disabled
|
|
2073
|
+
className="rcs-edit-template-message-static-textarea"
|
|
2074
|
+
/>
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
return renderArray;
|
|
2080
|
+
};
|
|
1032
2081
|
const onTemplateTitleChange = ({ target: { value } }) => {
|
|
1033
2082
|
let errorMessage = false;
|
|
1034
2083
|
if (templateType === contentType.rich_card && !value.trim()) {
|
|
@@ -1044,7 +2093,7 @@ export const Rcs = (props) => {
|
|
|
1044
2093
|
|
|
1045
2094
|
const onTemplateDescChange = ({ target: { value } }) => {
|
|
1046
2095
|
let errorMessage = false;
|
|
1047
|
-
if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
|
|
2096
|
+
if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
|
|
1048
2097
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1049
2098
|
} else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
|
|
1050
2099
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
@@ -1057,6 +2106,62 @@ export const Rcs = (props) => {
|
|
|
1057
2106
|
setTemplateDescError(error);
|
|
1058
2107
|
};
|
|
1059
2108
|
|
|
2109
|
+
|
|
2110
|
+
const templateDescErrorHandler = (value) => {
|
|
2111
|
+
let errorMessage = false;
|
|
2112
|
+
const { unsupportedTags, isBraceError } = validateTags({
|
|
2113
|
+
content: value,
|
|
2114
|
+
tagsParam: tags,
|
|
2115
|
+
injectedTagsParams: injectedTags,
|
|
2116
|
+
location,
|
|
2117
|
+
tagModule: getDefaultTags,
|
|
2118
|
+
}) || {};
|
|
2119
|
+
|
|
2120
|
+
const maxLength = templateType === contentType.text_message
|
|
2121
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
|
|
2122
|
+
: RCS_RICH_CARD_MAX_LENGTH;
|
|
2123
|
+
|
|
2124
|
+
if (value === '' && isMediaTypeText) {
|
|
2125
|
+
errorMessage = formatMessage(messages.emptyTemplateDescErrorMessage);
|
|
2126
|
+
} else if (value?.length > maxLength) {
|
|
2127
|
+
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (isBraceError) {
|
|
2131
|
+
errorMessage = formatMessage(globalMessages.unbalanacedCurlyBraces);
|
|
2132
|
+
}
|
|
2133
|
+
return errorMessage;
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
|
|
2137
|
+
const onFallbackMessageChange = ({ target: { value } }) => {
|
|
2138
|
+
const error = fallbackMessageErrorHandler(value);
|
|
2139
|
+
setFallbackMessage(value);
|
|
2140
|
+
setFallbackMessageError(error);
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2143
|
+
const fallbackMessageErrorHandler = (value) => {
|
|
2144
|
+
let errorMessage = false;
|
|
2145
|
+
const { unsupportedTags } = validateTags({
|
|
2146
|
+
content: value,
|
|
2147
|
+
tagsParam: tags,
|
|
2148
|
+
injectedTagsParams: injectedTags,
|
|
2149
|
+
location,
|
|
2150
|
+
tagModule: getDefaultTags,
|
|
2151
|
+
}) || {};
|
|
2152
|
+
if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
|
|
2153
|
+
errorMessage = formatMessage(messages.fallbackMsgLenError);
|
|
2154
|
+
} else if (unsupportedTags?.length > 0) {
|
|
2155
|
+
errorMessage = formatMessage(
|
|
2156
|
+
globalMessages.unsupportedTagsValidationError,
|
|
2157
|
+
{
|
|
2158
|
+
unsupportedTags,
|
|
2159
|
+
},
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
return errorMessage;
|
|
2163
|
+
};
|
|
2164
|
+
|
|
1060
2165
|
// Check for forbidden characters: square brackets [] and single curly braces {}
|
|
1061
2166
|
const forbiddenCharactersValidation = (value) => {
|
|
1062
2167
|
if (!value) return false;
|
|
@@ -1101,7 +2206,8 @@ export const Rcs = (props) => {
|
|
|
1101
2206
|
if(!isFullMode){
|
|
1102
2207
|
return false;
|
|
1103
2208
|
}
|
|
1104
|
-
|
|
2209
|
+
// Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
|
|
2210
|
+
if (!/^[\w.]+$/.test(paramName)) {
|
|
1105
2211
|
return formatMessage(messages.unknownCharactersError);
|
|
1106
2212
|
}
|
|
1107
2213
|
}
|
|
@@ -1166,6 +2272,117 @@ const onTitleAddVar = () => {
|
|
|
1166
2272
|
setTemplateTitleError(error);
|
|
1167
2273
|
};
|
|
1168
2274
|
|
|
2275
|
+
// Carousel: global variables across the whole carousel (all cards, title+body)
|
|
2276
|
+
const getNextCarouselVarToken = () => {
|
|
2277
|
+
const nums = [];
|
|
2278
|
+
(carouselData || []).forEach((c = {}) => {
|
|
2279
|
+
const s1 = (c.title || '').match(/\{\{(\d+)\}\}/g) || [];
|
|
2280
|
+
const s2 = (c.description || '').match(/\{\{(\d+)\}\}/g) || [];
|
|
2281
|
+
[...s1, ...s2].forEach((tok) => {
|
|
2282
|
+
const n = parseInt((tok.match(/\d+/) || [])[0], 10);
|
|
2283
|
+
if (!Number.isNaN(n)) nums.push(n);
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
const existing = new Set(nums);
|
|
2287
|
+
let nextNumber = 1;
|
|
2288
|
+
while (existing.has(nextNumber)) nextNumber++;
|
|
2289
|
+
if (nextNumber > 19) return '';
|
|
2290
|
+
return `{{${nextNumber}}}`;
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
const appendVarToCarouselField = (cardIndex, fieldName) => {
|
|
2294
|
+
const token = getNextCarouselVarToken();
|
|
2295
|
+
if (!token) return;
|
|
2296
|
+
setCarouselData((prev = []) => {
|
|
2297
|
+
const updated = cloneDeep(prev);
|
|
2298
|
+
if (!updated[cardIndex]) return prev;
|
|
2299
|
+
const current = (updated[cardIndex][fieldName] || '').toString();
|
|
2300
|
+
updated[cardIndex][fieldName] = `${current}${token}`;
|
|
2301
|
+
return updated;
|
|
2302
|
+
});
|
|
2303
|
+
};
|
|
2304
|
+
|
|
2305
|
+
const textAreaValue = (idValue, type) => {
|
|
2306
|
+
if (idValue >= 0) {
|
|
2307
|
+
const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
2308
|
+
const templateArr = splitTemplateVarString(templateStr);
|
|
2309
|
+
const token = templateArr?.[idValue] || "";
|
|
2310
|
+
if (token && rcsVarTestRegex.test(token)) {
|
|
2311
|
+
const varName = getVarNameFromToken(token);
|
|
2312
|
+
return (cardVarMapped?.[varName] ?? '').toString();
|
|
2313
|
+
}
|
|
2314
|
+
return "";
|
|
2315
|
+
}
|
|
2316
|
+
return "";
|
|
2317
|
+
};
|
|
2318
|
+
|
|
2319
|
+
// Carousel: render variable-value editor for a given template string (title/description).
|
|
2320
|
+
// This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
|
|
2321
|
+
const renderCarouselEditMessage = (templateStr) => {
|
|
2322
|
+
const renderArray = [];
|
|
2323
|
+
const templateArr = splitTemplateVarString(templateStr);
|
|
2324
|
+
if (templateArr?.length) {
|
|
2325
|
+
templateArr.forEach((elem, index) => {
|
|
2326
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
2327
|
+
const varName = getVarNameFromToken(elem);
|
|
2328
|
+
renderArray.push(
|
|
2329
|
+
<div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
|
|
2330
|
+
<TextArea
|
|
2331
|
+
id={`${elem}_${index}`}
|
|
2332
|
+
placeholder={`enter the value for ${elem}`}
|
|
2333
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2334
|
+
onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
|
|
2335
|
+
value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
|
|
2336
|
+
onFocus={(e) => {
|
|
2337
|
+
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
2338
|
+
setCarouselFocusedVarId(id);
|
|
2339
|
+
}}
|
|
2340
|
+
/>
|
|
2341
|
+
</div>
|
|
2342
|
+
);
|
|
2343
|
+
} else if (elem) {
|
|
2344
|
+
renderArray.push(
|
|
2345
|
+
<CapHeading
|
|
2346
|
+
key={`static_${index}`}
|
|
2347
|
+
type="h4"
|
|
2348
|
+
className="rcs-edit-template-message-split"
|
|
2349
|
+
>
|
|
2350
|
+
{elem}
|
|
2351
|
+
</CapHeading>
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
2357
|
+
};
|
|
2358
|
+
|
|
2359
|
+
const textAreaValueChange = (e, type) => {
|
|
2360
|
+
const value = e?.target?.value ?? '';
|
|
2361
|
+
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
2362
|
+
if (!id) return;
|
|
2363
|
+
const sep = id.lastIndexOf('_');
|
|
2364
|
+
if (sep === -1) return;
|
|
2365
|
+
const isInvalidValue = value?.trim() === "";
|
|
2366
|
+
const token = id.slice(0, sep);
|
|
2367
|
+
const variableName = getVarNameFromToken(token);
|
|
2368
|
+
|
|
2369
|
+
if (variableName) {
|
|
2370
|
+
setCardVarMapped((prev) => ({
|
|
2371
|
+
...prev,
|
|
2372
|
+
[variableName]: isInvalidValue ? "" : value,
|
|
2373
|
+
}));
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
|
|
2377
|
+
const setTextAreaId = (e, type) => {
|
|
2378
|
+
// VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
|
|
2379
|
+
// have an `.target.id` shape. Support both.
|
|
2380
|
+
const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
|
|
2381
|
+
if (!id) return;
|
|
2382
|
+
if (type === TITLE_TEXT) setTitleTextAreaId(id);
|
|
2383
|
+
else setDescTextAreaId(id);
|
|
2384
|
+
};
|
|
2385
|
+
|
|
1169
2386
|
const renderButtonComponent = () => {
|
|
1170
2387
|
return (
|
|
1171
2388
|
<>
|
|
@@ -1191,7 +2408,8 @@ const onTitleAddVar = () => {
|
|
|
1191
2408
|
isEditFlow={isEditFlow}
|
|
1192
2409
|
isFullMode={isFullMode}
|
|
1193
2410
|
maxButtons={MAX_BUTTONS}
|
|
1194
|
-
|
|
2411
|
+
host={hostName}
|
|
2412
|
+
/>
|
|
1195
2413
|
</>
|
|
1196
2414
|
);
|
|
1197
2415
|
};
|
|
@@ -1214,6 +2432,7 @@ const onTitleAddVar = () => {
|
|
|
1214
2432
|
varName,
|
|
1215
2433
|
globalSlot,
|
|
1216
2434
|
isEditLike,
|
|
2435
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
1217
2436
|
);
|
|
1218
2437
|
}
|
|
1219
2438
|
});
|
|
@@ -1222,42 +2441,37 @@ const onTitleAddVar = () => {
|
|
|
1222
2441
|
|
|
1223
2442
|
const titleVarSegmentValueMapById = useMemo(
|
|
1224
2443
|
() => getRcsValueMap(templateTitle, TITLE_TEXT),
|
|
1225
|
-
[templateTitle, cardVarMapped, isEditFlow, isFullMode],
|
|
2444
|
+
[templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
1226
2445
|
);
|
|
1227
2446
|
const descriptionVarSegmentValueMapById = useMemo(
|
|
1228
2447
|
() => getRcsValueMap(templateDesc, MESSAGE_TEXT),
|
|
1229
|
-
[templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode],
|
|
2448
|
+
[templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
1230
2449
|
);
|
|
1231
2450
|
|
|
1232
|
-
const handleRcsVarChange = (
|
|
1233
|
-
const
|
|
1234
|
-
if (
|
|
1235
|
-
const
|
|
1236
|
-
const variableName = getVarNameFromToken(
|
|
2451
|
+
const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
|
|
2452
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
2453
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
2454
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
2455
|
+
const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
1237
2456
|
if (variableName === undefined || variableName === null || variableName === '') return;
|
|
1238
2457
|
const isInvalidValue = value?.trim() === '';
|
|
1239
2458
|
const coercedSlotValue = isInvalidValue ? '' : value;
|
|
1240
|
-
const
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
if (variableName !== numericKey) {
|
|
1255
|
-
delete nextVarMap[variableName];
|
|
1256
|
-
}
|
|
1257
|
-
} else {
|
|
1258
|
-
nextVarMap[variableName] = coercedSlotValue;
|
|
2459
|
+
const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
2460
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
2461
|
+
varSegmentCompositeDomId,
|
|
2462
|
+
templateStringForField,
|
|
2463
|
+
type,
|
|
2464
|
+
);
|
|
2465
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
2466
|
+
const updatedCardVarMapped = { ...previousCardVarMapped };
|
|
2467
|
+
// Remove stale semantic key: keeping it causes every other slot sharing the same
|
|
2468
|
+
// variable name (e.g. {{adv}} in both title and description) to read the same value
|
|
2469
|
+
// via the semantic-key fallback in resolveCardVarMappedSlotValue.
|
|
2470
|
+
delete updatedCardVarMapped[variableName];
|
|
2471
|
+
if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
|
|
2472
|
+
updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
|
|
1259
2473
|
}
|
|
1260
|
-
return
|
|
2474
|
+
return updatedCardVarMapped;
|
|
1261
2475
|
});
|
|
1262
2476
|
};
|
|
1263
2477
|
|
|
@@ -1278,6 +2492,7 @@ const onTitleAddVar = () => {
|
|
|
1278
2492
|
}
|
|
1279
2493
|
suffix={
|
|
1280
2494
|
<>
|
|
2495
|
+
|
|
1281
2496
|
{(isEditFlow || !isFullMode) ? (
|
|
1282
2497
|
<TagList
|
|
1283
2498
|
label={formatMessage(globalMessages.addLabels)}
|
|
@@ -1294,15 +2509,16 @@ const onTitleAddVar = () => {
|
|
|
1294
2509
|
type="flat"
|
|
1295
2510
|
isAddBtn
|
|
1296
2511
|
onClick={onTitleAddVar}
|
|
1297
|
-
disabled={
|
|
2512
|
+
disabled={templateTitleError}
|
|
1298
2513
|
>
|
|
1299
2514
|
{formatMessage(messages.addVar)}
|
|
1300
2515
|
</CapButton>
|
|
1301
|
-
|
|
2516
|
+
)}
|
|
1302
2517
|
</>
|
|
1303
|
-
}
|
|
2518
|
+
}
|
|
1304
2519
|
/>
|
|
1305
|
-
|
|
2520
|
+
|
|
2521
|
+
{(isEditFlow || !isFullMode) ? (
|
|
1306
2522
|
<VarSegmentMessageEditor
|
|
1307
2523
|
key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
1308
2524
|
templateString={templateTitle}
|
|
@@ -1336,7 +2552,7 @@ const onTitleAddVar = () => {
|
|
|
1336
2552
|
)}
|
|
1337
2553
|
{!isEditFlow && isFullMode && renderTitleCharacterCount()}
|
|
1338
2554
|
</>
|
|
1339
|
-
|
|
2555
|
+
)}
|
|
1340
2556
|
|
|
1341
2557
|
{/* Template Message */}
|
|
1342
2558
|
<CapRow id="rcs-template-message-label">
|
|
@@ -1374,9 +2590,10 @@ const onTitleAddVar = () => {
|
|
|
1374
2590
|
/>
|
|
1375
2591
|
</CapRow>
|
|
1376
2592
|
<CapRow className="rcs-create-template-message-input">
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
2593
|
+
{/* 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. */}
|
|
2594
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
2595
|
+
{(isEditFlow || !isFullMode)?
|
|
2596
|
+
(
|
|
1380
2597
|
<VarSegmentMessageEditor
|
|
1381
2598
|
key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
1382
2599
|
templateString={templateDesc}
|
|
@@ -1425,7 +2642,7 @@ const onTitleAddVar = () => {
|
|
|
1425
2642
|
</>
|
|
1426
2643
|
)
|
|
1427
2644
|
}
|
|
1428
|
-
</
|
|
2645
|
+
</CapRow>
|
|
1429
2646
|
{(isEditFlow || !isFullMode) && templateDescError && (
|
|
1430
2647
|
<CapError className="rcs-template-message-error">
|
|
1431
2648
|
{templateDescError}
|
|
@@ -1447,7 +2664,8 @@ const onTitleAddVar = () => {
|
|
|
1447
2664
|
/>
|
|
1448
2665
|
)}
|
|
1449
2666
|
</CapRow>
|
|
1450
|
-
{
|
|
2667
|
+
{((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
|
|
2668
|
+
renderButtonComponent()}
|
|
1451
2669
|
</>
|
|
1452
2670
|
|
|
1453
2671
|
);
|
|
@@ -1467,7 +2685,7 @@ const onTitleAddVar = () => {
|
|
|
1467
2685
|
// Get max length for description based on template type
|
|
1468
2686
|
const getDescriptionMaxLength = () => {
|
|
1469
2687
|
return templateType === contentType.text_message
|
|
1470
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
|
|
2688
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
|
|
1471
2689
|
: RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
|
|
1472
2690
|
};
|
|
1473
2691
|
|
|
@@ -1493,6 +2711,63 @@ const onTitleAddVar = () => {
|
|
|
1493
2711
|
);
|
|
1494
2712
|
};
|
|
1495
2713
|
|
|
2714
|
+
const rcsDltCardDeleteHandler = () => {
|
|
2715
|
+
closeDltContainerHandler();
|
|
2716
|
+
setDltEditData({});
|
|
2717
|
+
setFallbackMessage('');
|
|
2718
|
+
setFallbackMessageError(false);
|
|
2719
|
+
setShowDltCard(false);
|
|
2720
|
+
};
|
|
2721
|
+
|
|
2722
|
+
const dltFallbackListingPreviewhandler = (data) => {
|
|
2723
|
+
const {
|
|
2724
|
+
'updated-sms-editor': updatedSmsEditor = [],
|
|
2725
|
+
'sms-editor': smsEditor = '',
|
|
2726
|
+
} = data.versions.base || {};
|
|
2727
|
+
setFallbackPreviewmode(true);
|
|
2728
|
+
setDltPreviewData(
|
|
2729
|
+
updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
|
|
2730
|
+
);
|
|
2731
|
+
};
|
|
2732
|
+
|
|
2733
|
+
const getDltContentCardList = (content, channel) => {
|
|
2734
|
+
const extra = [
|
|
2735
|
+
<CapIcon
|
|
2736
|
+
type="edit"
|
|
2737
|
+
style={{ marginRight: '8px' }}
|
|
2738
|
+
onClick={() => rcsDltEditSelectHandler(dltEditData)}
|
|
2739
|
+
/>,
|
|
2740
|
+
<CapDropdown
|
|
2741
|
+
overlay={(
|
|
2742
|
+
<CapMenu>
|
|
2743
|
+
<>
|
|
2744
|
+
<CapMenu.Item
|
|
2745
|
+
className="ant-dropdown-menu-item"
|
|
2746
|
+
onClick={() => setFallbackPreviewmode(true)}
|
|
2747
|
+
>
|
|
2748
|
+
{formatMessage(globalMessages.preview)}
|
|
2749
|
+
</CapMenu.Item>
|
|
2750
|
+
<CapMenu.Item
|
|
2751
|
+
className="ant-dropdown-menu-item"
|
|
2752
|
+
onClick={rcsDltCardDeleteHandler}
|
|
2753
|
+
>
|
|
2754
|
+
{formatMessage(globalMessages.delete)}
|
|
2755
|
+
</CapMenu.Item>
|
|
2756
|
+
</>
|
|
2757
|
+
</CapMenu>
|
|
2758
|
+
)}
|
|
2759
|
+
>
|
|
2760
|
+
<CapIcon type="more" />
|
|
2761
|
+
</CapDropdown>,
|
|
2762
|
+
];
|
|
2763
|
+
return {
|
|
2764
|
+
title: channel,
|
|
2765
|
+
content,
|
|
2766
|
+
cardType: channel,
|
|
2767
|
+
extra,
|
|
2768
|
+
};
|
|
2769
|
+
};
|
|
2770
|
+
|
|
1496
2771
|
// Render character count for description/message
|
|
1497
2772
|
const renderDescriptionCharacterCount = (className = "rcs-character-count") => {
|
|
1498
2773
|
const currentLength = getDescriptionCharacterCount();
|
|
@@ -1508,11 +2783,31 @@ const onTitleAddVar = () => {
|
|
|
1508
2783
|
);
|
|
1509
2784
|
};
|
|
1510
2785
|
|
|
2786
|
+
// Carousel: per-card character counts (same limits as rich card)
|
|
2787
|
+
const getCarouselTitleCharacterCount = (cardIndex) => {
|
|
2788
|
+
const t = carouselData?.[cardIndex]?.title || '';
|
|
2789
|
+
return t ? t.length : 0;
|
|
2790
|
+
};
|
|
2791
|
+
|
|
2792
|
+
const getCarouselDescriptionCharacterCount = (cardIndex) => {
|
|
2793
|
+
const d = carouselData?.[cardIndex]?.description || '';
|
|
2794
|
+
return d ? d.length : 0;
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
const renderCarouselCharacterCount = (currentLength, maxLength, className = "rcs-character-count") => (
|
|
2798
|
+
<CapLabel type="label1" className={className}>
|
|
2799
|
+
{formatMessage(messages.templateMessageLength, {
|
|
2800
|
+
currentLength,
|
|
2801
|
+
maxLength,
|
|
2802
|
+
})}
|
|
2803
|
+
</CapLabel>
|
|
2804
|
+
);
|
|
2805
|
+
|
|
1511
2806
|
// Check if any RCS variables contain tags (similar to Zalo hasTag logic)
|
|
1512
2807
|
const hasTag = () => {
|
|
1513
2808
|
// Check cardVarMapped values for tags
|
|
1514
2809
|
if (cardVarMapped && Object.keys(cardVarMapped).length > 0) {
|
|
1515
|
-
const hasTagInMapped = Object.values(cardVarMapped).some(
|
|
2810
|
+
const hasTagInMapped = Object.values(cardVarMapped).some(value =>
|
|
1516
2811
|
isTagIncluded(value)
|
|
1517
2812
|
);
|
|
1518
2813
|
if (hasTagInMapped) return true;
|
|
@@ -1530,6 +2825,14 @@ const onTitleAddVar = () => {
|
|
|
1530
2825
|
return false;
|
|
1531
2826
|
};
|
|
1532
2827
|
|
|
2828
|
+
//adding creative dlt fallback sms handlers
|
|
2829
|
+
const addDltMsgHandler = () => {
|
|
2830
|
+
setShowDltContainer(true);
|
|
2831
|
+
setDltMode(RCS_DLT_MODE.TEMPLATES);
|
|
2832
|
+
setDltEditData({});
|
|
2833
|
+
setFallbackMessage('');
|
|
2834
|
+
};
|
|
2835
|
+
|
|
1533
2836
|
const closeDltContainerHandler = () => {
|
|
1534
2837
|
setShowDltContainer(false);
|
|
1535
2838
|
setDltMode('');
|
|
@@ -1589,6 +2892,7 @@ const onTitleAddVar = () => {
|
|
|
1589
2892
|
isFullMode={isFullMode}
|
|
1590
2893
|
isDltFromRcs
|
|
1591
2894
|
onSelectTemplate={rcsDltEditSelectHandler}
|
|
2895
|
+
handlePeviewTemplate={dltFallbackListingPreviewhandler}
|
|
1592
2896
|
/>
|
|
1593
2897
|
);
|
|
1594
2898
|
} else if (dltMode === RCS_DLT_MODE.EDIT) {
|
|
@@ -1635,7 +2939,8 @@ const onTitleAddVar = () => {
|
|
|
1635
2939
|
);
|
|
1636
2940
|
|
|
1637
2941
|
const uploadRcsImage = useCallback((file, type, fileParams, index) => {
|
|
1638
|
-
|
|
2942
|
+
setImageError(null);
|
|
2943
|
+
const isRcsThumbnail = isThumbnailAssetIndex(index);
|
|
1639
2944
|
actions.uploadRcsAsset(file, type, {
|
|
1640
2945
|
isRcsThumbnail,
|
|
1641
2946
|
...fileParams,
|
|
@@ -1646,6 +2951,7 @@ const onTitleAddVar = () => {
|
|
|
1646
2951
|
|
|
1647
2952
|
const setUpdateRcsImageSrc = useCallback(
|
|
1648
2953
|
(val) => {
|
|
2954
|
+
setAssetListImage(val);
|
|
1649
2955
|
updateRcsImageSrc(val);
|
|
1650
2956
|
actions.clearRcsMediaAsset(0);
|
|
1651
2957
|
},
|
|
@@ -1672,8 +2978,6 @@ const onTitleAddVar = () => {
|
|
|
1672
2978
|
const updateOnRcsImageReUpload = useCallback(() => {
|
|
1673
2979
|
setUpdateRcsImageSrc('');
|
|
1674
2980
|
}, []);
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
2981
|
const uploadRcsVideo = (file, type, fileParams) => {
|
|
1678
2982
|
actions.uploadRcsAsset(file, type, {
|
|
1679
2983
|
...fileParams,
|
|
@@ -1705,13 +3009,13 @@ const onTitleAddVar = () => {
|
|
|
1705
3009
|
updateRcsThumbnailSrc('');
|
|
1706
3010
|
};
|
|
1707
3011
|
|
|
1708
|
-
|
|
3012
|
+
const renderThumbnailComponent = () => {
|
|
1709
3013
|
const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
1710
3014
|
return !isEditFlow && (
|
|
1711
3015
|
<>
|
|
1712
3016
|
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
|
|
1713
3017
|
<CapImageUpload
|
|
1714
|
-
|
|
3018
|
+
style={{ paddingTop: '20px' }}
|
|
1715
3019
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1716
3020
|
imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
|
|
1717
3021
|
imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
|
|
@@ -1723,11 +3027,13 @@ const onTitleAddVar = () => {
|
|
|
1723
3027
|
updateOnReUpload={updateOnRcsThumbnailReUpload}
|
|
1724
3028
|
minImgSize={RCS_THUMBNAIL_MIN_SIZE}
|
|
1725
3029
|
index={1}
|
|
3030
|
+
className="cap-custom-image-upload"
|
|
1726
3031
|
key={`rcs-uploaded-image-${currentDimension}`}
|
|
1727
3032
|
imageData={thumbnailData}
|
|
1728
3033
|
channel={RCS}
|
|
1729
3034
|
channelSpecificStyle={!isFullMode}
|
|
1730
3035
|
skipDimensionValidation={true}
|
|
3036
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
1731
3037
|
/>
|
|
1732
3038
|
</>
|
|
1733
3039
|
)
|
|
@@ -1767,6 +3073,7 @@ const onTitleAddVar = () => {
|
|
|
1767
3073
|
</div>
|
|
1768
3074
|
) : (
|
|
1769
3075
|
<CapImageUpload
|
|
3076
|
+
style={{ paddingTop: '20px' }}
|
|
1770
3077
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1771
3078
|
imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
|
|
1772
3079
|
imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
|
|
@@ -1788,7 +3095,7 @@ const onTitleAddVar = () => {
|
|
|
1788
3095
|
|
|
1789
3096
|
</>
|
|
1790
3097
|
);
|
|
1791
|
-
|
|
3098
|
+
}
|
|
1792
3099
|
|
|
1793
3100
|
const renderVideoComponent = () => {
|
|
1794
3101
|
const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
@@ -1865,6 +3172,36 @@ const onTitleAddVar = () => {
|
|
|
1865
3172
|
};
|
|
1866
3173
|
|
|
1867
3174
|
const getRcsPreview = () => {
|
|
3175
|
+
|
|
3176
|
+
if (templateType === contentType.carousel) {
|
|
3177
|
+
const cardsForPreview = buildCarouselCardsForPreview(carouselData);
|
|
3178
|
+
const carouselDimKey = getCarouselDimensionKey();
|
|
3179
|
+
const carouselImgDims =
|
|
3180
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
3181
|
+
const carouselVidDims =
|
|
3182
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
3183
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
3184
|
+
// Debug log for embedded/library mode preview payload (carousel)
|
|
3185
|
+
// eslint-disable-next-line no-console
|
|
3186
|
+
return (
|
|
3187
|
+
<UnifiedPreview
|
|
3188
|
+
channel={RCS}
|
|
3189
|
+
content={{
|
|
3190
|
+
carouselData: cardsForPreview,
|
|
3191
|
+
carouselPreviewDimensions: {
|
|
3192
|
+
imageWidth: carouselImgDims.width,
|
|
3193
|
+
imageHeight: carouselImgDims.height,
|
|
3194
|
+
videoThumbWidth: carouselVidDims.width,
|
|
3195
|
+
videoThumbHeight: carouselVidDims.height,
|
|
3196
|
+
},
|
|
3197
|
+
}}
|
|
3198
|
+
device={ANDROID}
|
|
3199
|
+
showDeviceToggle={false}
|
|
3200
|
+
showHeader={false}
|
|
3201
|
+
formatMessage={formatMessage}
|
|
3202
|
+
/>
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
1868
3205
|
|
|
1869
3206
|
const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
|
|
1870
3207
|
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
@@ -1990,23 +3327,25 @@ const onTitleAddVar = () => {
|
|
|
1990
3327
|
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
1991
3328
|
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
1992
3329
|
];
|
|
3330
|
+
const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
|
|
3331
|
+
cardVarMapped,
|
|
3332
|
+
);
|
|
3333
|
+
// Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
|
|
3334
|
+
// semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
|
|
3335
|
+
// and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
|
|
1993
3336
|
const persistedSlotVarMap = {};
|
|
1994
|
-
const seenSemanticVarNames = new Set();
|
|
1995
3337
|
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
1996
3338
|
const varName = getVarNameFromToken(token);
|
|
1997
3339
|
if (!varName) return;
|
|
1998
3340
|
const resolvedRawValue = resolveCardVarMappedSlotValue(
|
|
1999
|
-
|
|
3341
|
+
cardVarMappedForRcsCardOnly,
|
|
2000
3342
|
varName,
|
|
2001
3343
|
slotIndexZeroBased,
|
|
2002
3344
|
isSlotMappingMode,
|
|
3345
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
2003
3346
|
);
|
|
2004
3347
|
const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
|
|
2005
3348
|
persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
|
|
2006
|
-
if (!seenSemanticVarNames.has(varName)) {
|
|
2007
|
-
seenSemanticVarNames.add(varName);
|
|
2008
|
-
persistedSlotVarMap[varName] = sanitizedSlotValue;
|
|
2009
|
-
}
|
|
2010
3349
|
});
|
|
2011
3350
|
return { cardVarMapped: persistedSlotVarMap };
|
|
2012
3351
|
})()),
|
|
@@ -2023,6 +3362,53 @@ const onTitleAddVar = () => {
|
|
|
2023
3362
|
|| smsFallbackForPayload.message
|
|
2024
3363
|
|| smsFallbackForPayload.smsContent
|
|
2025
3364
|
|| '';
|
|
3365
|
+
/**
|
|
3366
|
+
* Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
|
|
3367
|
+
* Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
|
|
3368
|
+
*/
|
|
3369
|
+
const m = smsFallbackMerged || {};
|
|
3370
|
+
const tcSibling = m.templateConfigs && typeof m.templateConfigs === 'object'
|
|
3371
|
+
? m.templateConfigs
|
|
3372
|
+
: {};
|
|
3373
|
+
const smsFallbackTemplateId =
|
|
3374
|
+
(m.smsTemplateId != null && String(m.smsTemplateId).trim() !== ''
|
|
3375
|
+
? String(m.smsTemplateId)
|
|
3376
|
+
: '')
|
|
3377
|
+
|| (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
|
|
3378
|
+
? String(tcSibling.templateId)
|
|
3379
|
+
: '');
|
|
3380
|
+
const smsFallbackTemplateStr =
|
|
3381
|
+
pickFirstSmsFallbackTemplateString(m)
|
|
3382
|
+
|| (typeof m.templateContent === 'string' ? m.templateContent : '')
|
|
3383
|
+
|| (typeof tcSibling.template === 'string' ? tcSibling.template : '')
|
|
3384
|
+
|| '';
|
|
3385
|
+
const smsFallbackTemplateName =
|
|
3386
|
+
m.templateName
|
|
3387
|
+
|| m.smsTemplateName
|
|
3388
|
+
|| tcSibling.templateName
|
|
3389
|
+
|| tcSibling.name
|
|
3390
|
+
|| '';
|
|
3391
|
+
const registeredSenderIdsForPayload = Array.isArray(m.registeredSenderIds)
|
|
3392
|
+
? m.registeredSenderIds
|
|
3393
|
+
: Array.isArray(tcSibling.registeredSenderIds)
|
|
3394
|
+
? tcSibling.registeredSenderIds
|
|
3395
|
+
: Array.isArray(tcSibling.header)
|
|
3396
|
+
? tcSibling.header
|
|
3397
|
+
: null;
|
|
3398
|
+
const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
|
|
3399
|
+
const smsFallbackTemplateConfigs =
|
|
3400
|
+
smsFallbackTemplateId || hasRegisteredSenderIds
|
|
3401
|
+
? {
|
|
3402
|
+
...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
|
|
3403
|
+
...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
|
|
3404
|
+
...(smsFallbackTemplateName && {
|
|
3405
|
+
templateName: smsFallbackTemplateName,
|
|
3406
|
+
}),
|
|
3407
|
+
...(hasRegisteredSenderIds && {
|
|
3408
|
+
registeredSenderIds: registeredSenderIdsForPayload,
|
|
3409
|
+
}),
|
|
3410
|
+
}
|
|
3411
|
+
: null;
|
|
2026
3412
|
return {
|
|
2027
3413
|
smsFallBackContent: {
|
|
2028
3414
|
smsTemplateName: smsFallbackForPayload.templateName || '',
|
|
@@ -2036,6 +3422,9 @@ const onTitleAddVar = () => {
|
|
|
2036
3422
|
&& Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
|
|
2037
3423
|
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
|
|
2038
3424
|
}),
|
|
3425
|
+
...(smsFallbackTemplateConfigs && {
|
|
3426
|
+
templateConfigs: smsFallbackTemplateConfigs,
|
|
3427
|
+
}),
|
|
2039
3428
|
},
|
|
2040
3429
|
};
|
|
2041
3430
|
})()),
|
|
@@ -2058,13 +3447,36 @@ const onTitleAddVar = () => {
|
|
|
2058
3447
|
(wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
|
|
2059
3448
|
? String(wecrmAccountId)
|
|
2060
3449
|
: accountId;
|
|
2061
|
-
const
|
|
3450
|
+
const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
|
|
3451
|
+
let rcsForTest = {
|
|
2062
3452
|
...rcs,
|
|
2063
3453
|
rcsContent: {
|
|
2064
3454
|
...rcs.rcsContent,
|
|
2065
3455
|
...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
|
|
2066
3456
|
},
|
|
2067
3457
|
};
|
|
3458
|
+
/** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
|
|
3459
|
+
if (isSlotMappingModeForPreview) {
|
|
3460
|
+
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
3461
|
+
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
3462
|
+
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
3463
|
+
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
3464
|
+
templateTitle,
|
|
3465
|
+
templateDesc,
|
|
3466
|
+
rcsVarRegex,
|
|
3467
|
+
);
|
|
3468
|
+
rcsForTest = {
|
|
3469
|
+
...rcsForTest,
|
|
3470
|
+
rcsContent: {
|
|
3471
|
+
...rcsForTest.rcsContent,
|
|
3472
|
+
cardContent: [
|
|
3473
|
+
{ ...cardContent[0], cardVarMapped: fullCardVarMapped },
|
|
3474
|
+
...cardContent.slice(1),
|
|
3475
|
+
],
|
|
3476
|
+
},
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
2068
3480
|
const out = {
|
|
2069
3481
|
versions: {
|
|
2070
3482
|
base: {
|
|
@@ -2112,7 +3524,7 @@ const onTitleAddVar = () => {
|
|
|
2112
3524
|
* with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
|
|
2113
3525
|
* miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
|
|
2114
3526
|
*/
|
|
2115
|
-
|
|
3527
|
+
const librarySmsFallbackMergedForValidation = useMemo(() => {
|
|
2116
3528
|
if (isFullMode) {
|
|
2117
3529
|
return smsFallbackData;
|
|
2118
3530
|
}
|
|
@@ -2126,6 +3538,8 @@ const onTitleAddVar = () => {
|
|
|
2126
3538
|
};
|
|
2127
3539
|
}, [isFullMode, templateData, smsFallbackData]);
|
|
2128
3540
|
|
|
3541
|
+
|
|
3542
|
+
|
|
2129
3543
|
const actionCallback = ({ errorMessage, resp }, isEdit) => {
|
|
2130
3544
|
// eslint-disable-next-line no-undef
|
|
2131
3545
|
const error = errorMessage?.message || errorMessage;
|
|
@@ -2235,7 +3649,13 @@ const onTitleAddVar = () => {
|
|
|
2235
3649
|
return true;
|
|
2236
3650
|
}
|
|
2237
3651
|
return orderedVarNames.some((name, globalIdx) => {
|
|
2238
|
-
const v = resolveCardVarMappedSlotValue(
|
|
3652
|
+
const v = resolveCardVarMappedSlotValue(
|
|
3653
|
+
cardVarMapped,
|
|
3654
|
+
name,
|
|
3655
|
+
globalIdx,
|
|
3656
|
+
true,
|
|
3657
|
+
rcsSpanningSemanticVarNames.has(name),
|
|
3658
|
+
);
|
|
2239
3659
|
const s = v == null ? '' : String(v);
|
|
2240
3660
|
return s.trim() === '';
|
|
2241
3661
|
});
|
|
@@ -2266,7 +3686,7 @@ const onTitleAddVar = () => {
|
|
|
2266
3686
|
return true;
|
|
2267
3687
|
}
|
|
2268
3688
|
|
|
2269
|
-
if (isMediaTypeVideo && (rcsVideoSrc.videoSrc
|
|
3689
|
+
if (isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2270
3690
|
return true;
|
|
2271
3691
|
}
|
|
2272
3692
|
if (buttonType.includes(CTA)) {
|
|
@@ -2291,7 +3711,7 @@ const onTitleAddVar = () => {
|
|
|
2291
3711
|
};
|
|
2292
3712
|
|
|
2293
3713
|
const isEditDisableDone = () => {
|
|
2294
|
-
if (templateStatus !== RCS_STATUSES.approved) {
|
|
3714
|
+
if (!isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
|
|
2295
3715
|
return true;
|
|
2296
3716
|
}
|
|
2297
3717
|
|
|
@@ -2385,7 +3805,6 @@ const onTitleAddVar = () => {
|
|
|
2385
3805
|
// Slideboxes are rendered outside the page-level spinner to avoid
|
|
2386
3806
|
// stacking/blur issues during initial loads.
|
|
2387
3807
|
if (showDltContainer) return null;
|
|
2388
|
-
|
|
2389
3808
|
return (
|
|
2390
3809
|
<>
|
|
2391
3810
|
{templateStatus !== '' && (
|
|
@@ -2395,7 +3814,7 @@ const onTitleAddVar = () => {
|
|
|
2395
3814
|
{formatMessage(messages.templateStatusLabel)}
|
|
2396
3815
|
</CapLabel>
|
|
2397
3816
|
|
|
2398
|
-
{templateStatus && (
|
|
3817
|
+
{!isHostInfoBip && templateStatus && (
|
|
2399
3818
|
<CapAlert
|
|
2400
3819
|
message={getTemplateStatusMessage()}
|
|
2401
3820
|
type={getTemplateStatusType(templateStatus)}
|
|
@@ -2434,6 +3853,7 @@ const onTitleAddVar = () => {
|
|
|
2434
3853
|
)
|
|
2435
3854
|
)}
|
|
2436
3855
|
{renderLabel('templateTypeLabel')}
|
|
3856
|
+
|
|
2437
3857
|
<CapRadioGroup
|
|
2438
3858
|
id="select-rcs-template-type"
|
|
2439
3859
|
options={TEMPLATE_TYPE_OPTIONS}
|
|
@@ -2442,23 +3862,29 @@ const onTitleAddVar = () => {
|
|
|
2442
3862
|
disabled={(isEditFlow || !isFullMode)}
|
|
2443
3863
|
/>
|
|
2444
3864
|
|
|
2445
|
-
{
|
|
2446
|
-
|
|
3865
|
+
{templateType === contentType.carousel ? (
|
|
3866
|
+
renderCarouselSection()
|
|
3867
|
+
) : (
|
|
2447
3868
|
<>
|
|
2448
|
-
{
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
3869
|
+
{/* Show media only for rich_card */}
|
|
3870
|
+
{templateType === contentType.rich_card && (
|
|
3871
|
+
<>
|
|
3872
|
+
{renderLabel('mediaLabel')}
|
|
3873
|
+
<CapRadioGroup
|
|
3874
|
+
options={mediaRadioOptions || []}
|
|
3875
|
+
value={templateMediaType}
|
|
3876
|
+
onChange={onTemplateMediaTypeChange}
|
|
3877
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
3878
|
+
className="rcs-radio"
|
|
3879
|
+
/>
|
|
3880
|
+
<div className="rcs-container-image">
|
|
3881
|
+
{getMediaBasedComponent()}
|
|
3882
|
+
</div>
|
|
3883
|
+
</>
|
|
3884
|
+
)}
|
|
3885
|
+
{renderTextComponent()}
|
|
2459
3886
|
</>
|
|
2460
3887
|
)}
|
|
2461
|
-
{renderTextComponent()}
|
|
2462
3888
|
<CapDivider className="rcs-fallback-section-divider" />
|
|
2463
3889
|
{renderFallBackSmsComponent()}
|
|
2464
3890
|
<div className="rcs-scroll-div" />
|
|
@@ -2480,7 +3906,9 @@ const onTitleAddVar = () => {
|
|
|
2480
3906
|
disabled={isDisableDone()}
|
|
2481
3907
|
className="rcs-done-btn"
|
|
2482
3908
|
>
|
|
2483
|
-
<FormattedMessage
|
|
3909
|
+
<FormattedMessage
|
|
3910
|
+
{...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
|
|
3911
|
+
/>
|
|
2484
3912
|
</CapButton>
|
|
2485
3913
|
</div>
|
|
2486
3914
|
<CapTooltip
|
|
@@ -2554,13 +3982,19 @@ const onTitleAddVar = () => {
|
|
|
2554
3982
|
content={testAndPreviewContent}
|
|
2555
3983
|
currentChannel={RCS}
|
|
2556
3984
|
orgUnitId={orgUnitId}
|
|
3985
|
+
rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
|
|
2557
3986
|
smsFallbackContent={
|
|
2558
3987
|
smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
|
|
2559
3988
|
? {
|
|
2560
3989
|
templateContent:
|
|
2561
3990
|
smsFallbackData.templateContent || smsFallbackData.content || '',
|
|
2562
3991
|
templateName: smsFallbackData.templateName || '',
|
|
2563
|
-
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
|
|
3992
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
|
|
3993
|
+
? mergeRcsSmsFallbackVarMapLayers(
|
|
3994
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
3995
|
+
smsFallbackData,
|
|
3996
|
+
)
|
|
3997
|
+
: mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
|
|
2564
3998
|
}
|
|
2565
3999
|
: null
|
|
2566
4000
|
}
|
|
@@ -2570,12 +4004,14 @@ const onTitleAddVar = () => {
|
|
|
2570
4004
|
);
|
|
2571
4005
|
};
|
|
2572
4006
|
|
|
4007
|
+
|
|
2573
4008
|
const mapStateToProps = createStructuredSelector({
|
|
2574
4009
|
rcsData: makeSelectRcs(),
|
|
2575
4010
|
accountData: makeSelectAccount(),
|
|
2576
4011
|
metaEntities: makeSelectMetaEntities(),
|
|
2577
4012
|
loadingTags: isLoadingMetaEntities(),
|
|
2578
4013
|
injectedTags: setInjectedTags(),
|
|
4014
|
+
currentOrgDetails: selectCurrentOrgDetails(),
|
|
2579
4015
|
});
|
|
2580
4016
|
|
|
2581
4017
|
const mapDispatchToProps = (dispatch) => ({
|