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