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