@capillarytech/creatives-library 8.0.345-alpha.13 → 8.0.345-alpha.15
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/package.json +1 -1
- package/services/api.js +0 -20
- package/services/tests/api.test.js +13 -59
- package/utils/commonUtils.js +19 -1
- package/utils/rcsPayloadUtils.js +92 -0
- package/utils/templateVarUtils.js +201 -0
- package/utils/tests/templateVarUtils.test.js +204 -0
- package/v2Components/CapActionButton/constants.js +7 -0
- package/v2Components/CapActionButton/index.js +167 -109
- package/v2Components/CapActionButton/index.scss +157 -6
- package/v2Components/CapActionButton/messages.js +19 -3
- package/v2Components/CapActionButton/tests/index.test.js +41 -17
- package/v2Components/CapCustomSkeleton/index.js +1 -1
- package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
- package/v2Components/CapTagList/index.js +10 -0
- package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
- package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
- package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
- package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
- 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/RcsPreviewContent.js +160 -15
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
- package/v2Components/CommonTestAndPreview/constants.js +38 -2
- package/v2Components/CommonTestAndPreview/index.js +676 -186
- package/v2Components/CommonTestAndPreview/messages.js +49 -3
- package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
- package/v2Components/CommonTestAndPreview/sagas.js +15 -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/RcsPreviewContent.test.js +281 -283
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
- package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
- package/v2Components/FormBuilder/index.js +8 -10
- package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
- package/v2Components/SmsFallback/constants.js +73 -0
- package/v2Components/SmsFallback/index.js +955 -0
- package/v2Components/SmsFallback/index.scss +265 -0
- package/v2Components/SmsFallback/messages.js +78 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +118 -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 +197 -0
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
- package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
- package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +143 -28
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
- package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
- package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
- package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
- package/v2Components/VarSegmentMessageEditor/index.js +125 -0
- package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
- package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +11 -4
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
- package/v2Containers/CreativesContainer/constants.js +9 -0
- package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
- package/v2Containers/CreativesContainer/index.js +300 -108
- package/v2Containers/CreativesContainer/index.scss +51 -1
- package/v2Containers/CreativesContainer/messages.js +0 -4
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -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 -18
- 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/Rcs/constants.js +119 -8
- package/v2Containers/Rcs/index.js +2379 -807
- package/v2Containers/Rcs/index.js.rej +1336 -0
- package/v2Containers/Rcs/index.scss +276 -6
- package/v2Containers/Rcs/index.scss.rej +74 -0
- package/v2Containers/Rcs/messages.js +38 -3
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
- 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 +100 -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 +636 -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/ChannelTypeIllustration.js +6 -23
- package/v2Containers/Templates/TemplatesActionBar.js +101 -0
- package/v2Containers/Templates/_templates.scss +181 -126
- package/v2Containers/Templates/actions.js +11 -36
- package/v2Containers/Templates/constants.js +2 -23
- package/v2Containers/Templates/index.js +142 -333
- package/v2Containers/Templates/messages.js +0 -68
- package/v2Containers/Templates/reducer.js +0 -68
- package/v2Containers/Templates/sagas.js +55 -98
- package/v2Containers/Templates/selectors.js +0 -12
- package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +0 -12
- package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1042 -1256
- package/v2Containers/Templates/tests/index.test.js +0 -6
- package/v2Containers/Templates/tests/reducer.test.js +0 -178
- package/v2Containers/Templates/tests/sagas.test.js +200 -436
- package/v2Containers/Templates/tests/selector.test.js +0 -32
- package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
- package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
- package/v2Containers/TemplatesV2/index.js +86 -23
- package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
- package/v2Containers/Whatsapp/index.js +3 -20
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
- package/v2Containers/Assets/images/archive_Empty_Illustration.svg +0 -9
|
@@ -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,7 @@ 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
37
|
|
|
38
38
|
import {
|
|
39
39
|
CAP_G01,
|
|
@@ -49,17 +49,30 @@ import {
|
|
|
49
49
|
import CapVideoUpload from '../../v2Components/CapVideoUpload';
|
|
50
50
|
import * as globalActions from '../Cap/actions';
|
|
51
51
|
import CapActionButton from '../../v2Components/CapActionButton';
|
|
52
|
+
import TemplatePreview from '../../v2Components/TemplatePreview';
|
|
52
53
|
import { makeSelectRcs, makeSelectAccount } from './selectors';
|
|
53
54
|
import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
|
|
54
55
|
import {
|
|
55
56
|
isLoadingMetaEntities,
|
|
56
57
|
makeSelectMetaEntities,
|
|
57
58
|
setInjectedTags,
|
|
59
|
+
selectCurrentOrgDetails,
|
|
58
60
|
} from '../Cap/selectors';
|
|
59
61
|
import * as RcsActions from './actions';
|
|
60
62
|
import { isAiContentBotDisabled } from '../../utils/common';
|
|
61
63
|
import * as TemplatesActions from '../Templates/actions';
|
|
62
64
|
import './index.scss';
|
|
65
|
+
import {
|
|
66
|
+
normalizeLibraryLoadedTitleDesc,
|
|
67
|
+
mergeRcsSmsFallBackContentFromDetails,
|
|
68
|
+
mergeRcsSmsFallbackVarMapLayers,
|
|
69
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord,
|
|
70
|
+
pickFirstSmsFallbackTemplateString,
|
|
71
|
+
syncCardVarMappedSemanticsFromSlots,
|
|
72
|
+
hasMeaningfulSmsFallbackShape,
|
|
73
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData,
|
|
74
|
+
pickRcsCardVarMappedEntries,
|
|
75
|
+
} from './rcsLibraryHydrationUtils';
|
|
63
76
|
import {
|
|
64
77
|
RCS,
|
|
65
78
|
SMS,
|
|
@@ -76,6 +89,7 @@ import {
|
|
|
76
89
|
RCS_IMG_SIZE,
|
|
77
90
|
RCS_DLT_MODE,
|
|
78
91
|
CTA,
|
|
92
|
+
AI_CONTENT_BOT_DISABLED,
|
|
79
93
|
RCS_STATUSES,
|
|
80
94
|
TITLE_TEXT,
|
|
81
95
|
MESSAGE_TEXT,
|
|
@@ -90,9 +104,15 @@ import {
|
|
|
90
104
|
rcsVarTestRegex,
|
|
91
105
|
RCS_IMAGE_DIMENSIONS,
|
|
92
106
|
RCS_TEXT_MESSAGE_MAX_LENGTH,
|
|
107
|
+
RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
|
|
93
108
|
RCS_RICH_CARD_MAX_LENGTH,
|
|
94
109
|
RCS_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
110
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS,
|
|
111
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
112
|
+
RCS_CAROUSEL_IMG_SIZE,
|
|
113
|
+
RCS_CAROUSEL_VIDEO_SIZE,
|
|
95
114
|
MAX_BUTTONS,
|
|
115
|
+
INITIAL_SUGGESTIONS,
|
|
96
116
|
INITIAL_SUGGESTIONS_DATA_STOP,
|
|
97
117
|
RCS_BUTTON_TYPES,
|
|
98
118
|
titletype,
|
|
@@ -102,25 +122,47 @@ import {
|
|
|
102
122
|
SMALL,
|
|
103
123
|
MEDIUM,
|
|
104
124
|
RICHCARD,
|
|
125
|
+
HOST_INFOBIP,
|
|
126
|
+
HOST_ICS,
|
|
127
|
+
CAROUSEL_HEIGHT_OPTIONS,
|
|
128
|
+
CAROUSEL_WIDTH_OPTIONS,
|
|
129
|
+
STOP,
|
|
130
|
+
RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
|
|
131
|
+
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
132
|
+
RCS_NUMERIC_VAR_TOKEN_REGEX,
|
|
133
|
+
RCS_TAG_AREA_FIELD_TITLE,
|
|
134
|
+
RCS_TAG_AREA_FIELD_DESC,
|
|
105
135
|
} from './constants';
|
|
106
136
|
import globalMessages from '../Cap/messages';
|
|
107
137
|
import messages from './messages';
|
|
108
138
|
import creativesMessages from '../CreativesContainer/messages';
|
|
109
139
|
import withCreatives from '../../hoc/withCreatives';
|
|
110
140
|
import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
|
|
111
|
-
import
|
|
141
|
+
import VarSegmentMessageEditor from '../../v2Components/VarSegmentMessageEditor';
|
|
142
|
+
import { ANDROID, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
|
|
112
143
|
import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
|
|
144
|
+
import { splitTemplateVarString } from '../../utils/templateVarUtils';
|
|
113
145
|
import CapImageUpload from '../../v2Components/CapImageUpload';
|
|
114
|
-
import addCreativesIcon from '../Assets/images/addCreativesIllustration.svg';
|
|
115
146
|
import Templates from '../Templates';
|
|
116
147
|
import SmsTraiEdit from '../SmsTrai/Edit';
|
|
148
|
+
import SmsFallback from '../../v2Components/SmsFallback';
|
|
149
|
+
import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
|
|
117
150
|
import TagList from '../TagList';
|
|
118
151
|
import { validateTags } from '../../utils/tagValidations';
|
|
119
|
-
import {
|
|
152
|
+
import { isTraiDLTEnable } from '../../utils/common';
|
|
120
153
|
import { isTagIncluded } from '../../utils/commonUtils';
|
|
121
154
|
import injectReducer from '../../utils/injectReducer';
|
|
122
155
|
import v2RcsReducer from './reducer';
|
|
123
|
-
import {
|
|
156
|
+
import {
|
|
157
|
+
areAllRcsSmsFallbackVarSlotsFilled,
|
|
158
|
+
buildRcsNumericMustachePlaceholderRegex,
|
|
159
|
+
getTemplateStatusType,
|
|
160
|
+
normalizeCardVarMapped,
|
|
161
|
+
coalesceCardVarMappedToTemplate,
|
|
162
|
+
getRcsSemanticVarNamesSpanningTitleAndDesc,
|
|
163
|
+
resolveCardVarMappedSlotValue,
|
|
164
|
+
sanitizeCardVarMappedValue,
|
|
165
|
+
} from './utils';
|
|
124
166
|
|
|
125
167
|
|
|
126
168
|
const { Group: CapCheckboxGroup } = CapCheckbox;
|
|
@@ -137,19 +179,18 @@ export const Rcs = (props) => {
|
|
|
137
179
|
templatesActions,
|
|
138
180
|
globalActions,
|
|
139
181
|
location,
|
|
140
|
-
handleClose,
|
|
141
182
|
getDefaultTags,
|
|
142
183
|
supportedTags,
|
|
143
184
|
metaEntities,
|
|
144
185
|
injectedTags,
|
|
145
186
|
loadingTags,
|
|
146
187
|
getFormData,
|
|
147
|
-
isDltEnabled,
|
|
148
188
|
smsRegister,
|
|
189
|
+
orgUnitId,
|
|
149
190
|
selectedOfferDetails,
|
|
150
191
|
eventContextTags,
|
|
151
|
-
waitEventContextTags,
|
|
152
192
|
accountData = {},
|
|
193
|
+
currentOrgDetails,
|
|
153
194
|
// TestAndPreviewSlidebox props
|
|
154
195
|
showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
|
|
155
196
|
handleTestAndPreview: propsHandleTestAndPreview,
|
|
@@ -158,7 +199,25 @@ export const Rcs = (props) => {
|
|
|
158
199
|
const { formatMessage } = intl;
|
|
159
200
|
const { TextArea } = CapInput;
|
|
160
201
|
const { CapCustomCardList } = CapCustomCard;
|
|
202
|
+
|
|
203
|
+
// Defensive: React cannot render plain objects as children (crashes with
|
|
204
|
+
// "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
|
|
205
|
+
// can accidentally set an error state to an object (e.g. `{}`).
|
|
206
|
+
const normalizeErrorMessage = (err) => {
|
|
207
|
+
if (!err) return '';
|
|
208
|
+
if (React.isValidElement(err)) return err;
|
|
209
|
+
if (typeof err === 'string') return err;
|
|
210
|
+
if (typeof err === 'number') return String(err);
|
|
211
|
+
if (typeof err === 'object' && typeof err.message === 'string') return err.message;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.stringify(err);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return String(err);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
161
219
|
const [isEditFlow, setEditFlow] = useState(false);
|
|
220
|
+
const isEditLike = isEditFlow || !isFullMode;
|
|
162
221
|
const [tags, updateTags] = useState([]);
|
|
163
222
|
const [spin, setSpin] = useState(false);
|
|
164
223
|
//template
|
|
@@ -167,33 +226,21 @@ export const Rcs = (props) => {
|
|
|
167
226
|
const [templateMediaType, setTemplateMediaType] = useState(
|
|
168
227
|
RCS_MEDIA_TYPES.NONE,
|
|
169
228
|
);
|
|
170
|
-
const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
|
|
171
229
|
const [templateTitle, setTemplateTitle] = useState('');
|
|
172
230
|
const [templateDesc, setTemplateDesc] = useState('');
|
|
173
231
|
const [templateDescError, setTemplateDescError] = useState(false);
|
|
174
232
|
const [templateStatus, setTemplateStatus] = useState('');
|
|
175
|
-
const [templateDate, setTemplateDate] = useState('');
|
|
176
|
-
//fallback
|
|
177
|
-
const [fallbackMessage, setFallbackMessage] = useState('');
|
|
178
|
-
const [fallbackMessageError, setFallbackMessageError] = useState(false);
|
|
179
233
|
//fallback dlt
|
|
180
234
|
const [showDltContainer, setShowDltContainer] = useState(false);
|
|
181
235
|
const [dltMode, setDltMode] = useState('');
|
|
182
236
|
const [dltEditData, setDltEditData] = useState({});
|
|
183
|
-
|
|
184
|
-
const [
|
|
185
|
-
const [dltPreviewData, setDltPreviewData] = useState('');
|
|
237
|
+
/** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
|
|
238
|
+
const [smsFallbackData, setSmsFallbackData] = useState(undefined);
|
|
186
239
|
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
|
|
187
|
-
const
|
|
240
|
+
const buttonType = RCS_BUTTON_TYPES.NONE;
|
|
188
241
|
const [suggestionError, setSuggestionError] = useState(true);
|
|
242
|
+
const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
|
|
189
243
|
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
244
|
const [titleVarMappedData, setTitleVarMappedData] = useState({});
|
|
198
245
|
const [descVarMappedData, setDescVarMappedData] = useState({});
|
|
199
246
|
const [titleTextAreaId, setTitleTextAreaId] = useState();
|
|
@@ -204,75 +251,58 @@ export const Rcs = (props) => {
|
|
|
204
251
|
const [rcsVideoSrc, setRcsVideoSrc] = useState({});
|
|
205
252
|
const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
|
|
206
253
|
const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
|
|
207
|
-
|
|
254
|
+
// Carousel (UI-only) state
|
|
255
|
+
const [selectedCarousel, setSelectedCarousel] = useState('');
|
|
256
|
+
const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
|
|
257
|
+
const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
|
|
258
|
+
const [carouselData, setCarouselData] = useState([]);
|
|
259
|
+
const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
|
|
260
|
+
const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
|
|
261
|
+
const [carouselResetNonce, setCarouselResetNonce] = useState(0);
|
|
262
|
+
const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
|
|
263
|
+
const [imageError, setImageError] = useState(null);
|
|
208
264
|
const [templateTitleError, setTemplateTitleError] = useState(false);
|
|
209
265
|
const [cardVarMapped, setCardVarMapped] = useState({});
|
|
266
|
+
/** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
|
|
267
|
+
const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
|
|
268
|
+
const lastHydratedRcsCardVarSignatureRef = useRef(null);
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
|
|
272
|
+
* without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
|
|
273
|
+
* fallback card / content stopped appearing until re-selected.
|
|
274
|
+
*/
|
|
275
|
+
const rcsHydrationDetails = useMemo(
|
|
276
|
+
() => (isFullMode ? rcsData?.templateDetails : templateData),
|
|
277
|
+
[isFullMode, rcsData?.templateDetails, templateData],
|
|
278
|
+
);
|
|
210
279
|
|
|
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;
|
|
280
|
+
/** 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'). */
|
|
281
|
+
const lastTagSchemaQueryKeyRef = useRef(null);
|
|
282
|
+
/**
|
|
283
|
+
* Library: parent often passes a new `templateData` object reference every render. Re-applying the same
|
|
284
|
+
* SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
|
|
285
|
+
*/
|
|
286
|
+
const lastSmsFallbackHydrationKeyRef = useRef(null);
|
|
287
|
+
|
|
288
|
+
const fetchTagSchemaIfNewQuery = useCallback(
|
|
289
|
+
(query) => {
|
|
290
|
+
const key = JSON.stringify(query);
|
|
291
|
+
if (lastTagSchemaQueryKeyRef.current === key) {
|
|
292
|
+
return;
|
|
242
293
|
}
|
|
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
|
-
};
|
|
294
|
+
lastTagSchemaQueryKeyRef.current = key;
|
|
295
|
+
globalActions.fetchSchemaForEntity(query);
|
|
296
|
+
},
|
|
297
|
+
[globalActions],
|
|
298
|
+
);
|
|
259
299
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
templateMediaType,
|
|
263
|
-
templateTitle,
|
|
264
|
-
templateDesc,
|
|
265
|
-
rcsImageSrc,
|
|
266
|
-
rcsVideoSrc,
|
|
267
|
-
rcsThumbnailSrc,
|
|
268
|
-
suggestions,
|
|
269
|
-
selectedDimension,
|
|
270
|
-
]);
|
|
300
|
+
// TestAndPreviewSlidebox state
|
|
301
|
+
const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
|
|
271
302
|
|
|
272
303
|
// Handle Test and Preview button click
|
|
273
304
|
const handleTestAndPreview = useCallback(() => {
|
|
274
305
|
setShowTestAndPreviewSlidebox(true);
|
|
275
|
-
setIsTestAndPreviewMode(true);
|
|
276
306
|
if (propsHandleTestAndPreview) {
|
|
277
307
|
propsHandleTestAndPreview();
|
|
278
308
|
}
|
|
@@ -296,31 +326,721 @@ export const Rcs = (props) => {
|
|
|
296
326
|
// For video
|
|
297
327
|
return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
|
|
298
328
|
};
|
|
329
|
+
/** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
|
|
330
|
+
const handleSmsFallbackEditorStateChange = useCallback((patch) => {
|
|
331
|
+
setSmsFallbackData((prev) => {
|
|
332
|
+
if (!patch || typeof patch !== 'object') return prev;
|
|
333
|
+
// Bail out when no template has been selected yet — SmsTraiEdit fires
|
|
334
|
+
// onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
|
|
335
|
+
// which would create a non-null smsFallbackData from nothing and cause
|
|
336
|
+
// the card to appear as "Untitled creative" before any save.
|
|
337
|
+
if (!prev) return prev;
|
|
338
|
+
return { ...prev, ...patch };
|
|
339
|
+
});
|
|
340
|
+
}, []);
|
|
299
341
|
|
|
342
|
+
/** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
|
|
300
343
|
const [accountId, setAccountId] = useState('');
|
|
344
|
+
/** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
|
|
345
|
+
const [wecrmAccountId, setWecrmAccountId] = useState('');
|
|
301
346
|
const [accessToken, setAccessToken] = useState('');
|
|
302
347
|
const [hostName, setHostName] = useState('');
|
|
303
348
|
const [accountName, setAccountName] = useState('');
|
|
304
|
-
const
|
|
349
|
+
const isHostInfoBip = hostName === HOST_INFOBIP;
|
|
350
|
+
const isHostIcs = hostName === HOST_ICS;
|
|
351
|
+
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
|
|
354
|
+
}, [isHostIcs]);
|
|
305
355
|
useEffect(() => {
|
|
306
356
|
const accountObj = accountData.selectedRcsAccount || {};
|
|
307
357
|
if (!isEmpty(accountObj)) {
|
|
308
358
|
const {
|
|
359
|
+
id: wecrmId,
|
|
309
360
|
sourceAccountIdentifier = '',
|
|
310
361
|
configs = {},
|
|
311
362
|
} = accountObj;
|
|
312
|
-
|
|
313
363
|
setAccountId(sourceAccountIdentifier);
|
|
364
|
+
setWecrmAccountId(
|
|
365
|
+
wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
|
|
366
|
+
);
|
|
314
367
|
setAccessToken(configs.accessToken || '');
|
|
315
368
|
setHostName(accountObj.hostName || '');
|
|
316
369
|
setAccountName(accountObj.name || '');
|
|
317
|
-
|
|
370
|
+
} else {
|
|
371
|
+
setAccountId('');
|
|
372
|
+
setWecrmAccountId('');
|
|
373
|
+
setAccessToken('');
|
|
374
|
+
setHostName('');
|
|
375
|
+
setAccountName('');
|
|
318
376
|
}
|
|
319
377
|
}, [accountData.selectedRcsAccount]);
|
|
320
378
|
|
|
321
379
|
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
322
380
|
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
323
381
|
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
382
|
+
const isCarouselType = templateType === contentType.carousel;
|
|
383
|
+
|
|
384
|
+
const MAX_RCS_CAROUSEL_ALLOWED = 10;
|
|
385
|
+
// Uploads for RCS are stored in redux under dynamic keys `uploadedAssetData${index}`.
|
|
386
|
+
// Carousel needs per-card indices; otherwise all cards "restore" the last uploaded asset
|
|
387
|
+
// and show the same media/thumbnail.
|
|
388
|
+
const RCS_CAROUSEL_ASSET_INDEX_BASE = 10; // keep away from standalone indices 0 (media) and 1 (thumbnail)
|
|
389
|
+
const getCarouselImageAssetIndex = (cardIndex) =>
|
|
390
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
|
|
391
|
+
const getCarouselVideoAssetIndex = (cardIndex) =>
|
|
392
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 1;
|
|
393
|
+
const getCarouselThumbnailAssetIndex = (cardIndex) =>
|
|
394
|
+
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 2;
|
|
395
|
+
const isThumbnailAssetIndex = (index) => {
|
|
396
|
+
if (index === 1) return true; // standalone thumbnail
|
|
397
|
+
if (index >= RCS_CAROUSEL_ASSET_INDEX_BASE) {
|
|
398
|
+
return ((index - RCS_CAROUSEL_ASSET_INDEX_BASE) % 3) === 2; // carousel thumbnail slot
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Carousel dimension key: `${HEIGHT}_${WIDTH}` (e.g., SHORT_SMALL)
|
|
404
|
+
const getCarouselDimensionKey = () => `${selectedCarouselHeight}_${selectedCarouselWidth}`;
|
|
405
|
+
|
|
406
|
+
const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
|
|
407
|
+
setCarouselData((prev = []) => {
|
|
408
|
+
const updated = cloneDeep(prev);
|
|
409
|
+
if (!updated?.[cardIndex]) return updated;
|
|
410
|
+
if (clearImage) updated[cardIndex].imageSrc = '';
|
|
411
|
+
if (clearVideo) updated[cardIndex].videoAsset = {};
|
|
412
|
+
if (clearThumb) updated[cardIndex].thumbnailSrc = '';
|
|
413
|
+
return updated;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
|
|
417
|
+
if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
|
|
418
|
+
if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const resetCarouselMediaForAllCards = () => {
|
|
422
|
+
setCarouselData((prev = []) => {
|
|
423
|
+
const updated = cloneDeep(prev);
|
|
424
|
+
updated.forEach((card) => {
|
|
425
|
+
if (!card) return;
|
|
426
|
+
card.imageSrc = '';
|
|
427
|
+
card.videoAsset = {};
|
|
428
|
+
card.thumbnailSrc = '';
|
|
429
|
+
});
|
|
430
|
+
return updated;
|
|
431
|
+
});
|
|
432
|
+
(carouselData || []).forEach((_, idx) => {
|
|
433
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
|
|
434
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
|
|
435
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
|
|
436
|
+
});
|
|
437
|
+
// Force tab panes to remount after global reset (some tab implementations cache inactive panes)
|
|
438
|
+
setCarouselResetNonce((n) => n + 1);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const RCS_CAROUSEL_INITIAL_CARD = {
|
|
442
|
+
title: '',
|
|
443
|
+
description: '',
|
|
444
|
+
mediaType: RCS_MEDIA_TYPES.IMAGE, // per-card
|
|
445
|
+
imageSrc: '',
|
|
446
|
+
videoAsset: {}, // CapVideoUpload object shape
|
|
447
|
+
thumbnailSrc: '',
|
|
448
|
+
suggestions: [],
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const RCS_CAROUSEL_INITIAL_FIRST_CARD = {
|
|
452
|
+
...RCS_CAROUSEL_INITIAL_CARD,
|
|
453
|
+
suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const ensureFirstCardDefaultPhoneSuggestions = (cards) => {
|
|
457
|
+
const next = cloneDeep(cards || []);
|
|
458
|
+
if (next.length === 0) return next;
|
|
459
|
+
const s = next[0].suggestions;
|
|
460
|
+
if (!Array.isArray(s) || s.length === 0) {
|
|
461
|
+
next[0] = {
|
|
462
|
+
...next[0],
|
|
463
|
+
suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return next;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
|
|
470
|
+
// updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
|
|
471
|
+
// suggestions typed since the last commit (stale closure).
|
|
472
|
+
const handleCarouselValueChange = (carouselIndex, fields) => {
|
|
473
|
+
setCarouselData((prev = []) => {
|
|
474
|
+
const updated = cloneDeep(prev);
|
|
475
|
+
if (!updated[carouselIndex]) return prev;
|
|
476
|
+
fields.forEach(({ fieldName, value }) => {
|
|
477
|
+
updated[carouselIndex][fieldName] = value;
|
|
478
|
+
});
|
|
479
|
+
return updated;
|
|
480
|
+
});
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const updateCarouselErrors = (carouselIndex, patch) => {
|
|
484
|
+
setCarouselErrors((prev = []) => {
|
|
485
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
486
|
+
next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
|
|
487
|
+
return next;
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const deleteCarouselCard = (index) => {
|
|
492
|
+
let nextIdx = 0;
|
|
493
|
+
flushSync(() => {
|
|
494
|
+
setCarouselData((prev = []) => {
|
|
495
|
+
const updated = cloneDeep(prev);
|
|
496
|
+
if (index < 0 || index >= updated.length) return updated;
|
|
497
|
+
updated.splice(index, 1);
|
|
498
|
+
nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
|
|
499
|
+
return ensureFirstCardDefaultPhoneSuggestions(updated);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
setCarouselErrors((prev = []) => {
|
|
503
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
504
|
+
next.splice(index, 1);
|
|
505
|
+
return next;
|
|
506
|
+
});
|
|
507
|
+
setActiveCarouselIndex(`${nextIdx}`);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const carouselButtonTextHasForbiddenChars = (value) => {
|
|
511
|
+
if (!value) return false;
|
|
512
|
+
if (value.includes('[') || value.includes(']')) return true;
|
|
513
|
+
const withoutValidVariables = value.replace(/\{\{[^}]*\}\}/g, '');
|
|
514
|
+
if (withoutValidVariables.includes('{') || withoutValidVariables.includes('}')) return true;
|
|
515
|
+
return false;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const isCompleteSavedCarouselSuggestion = (s) => {
|
|
519
|
+
if (!s || !s.isSaved) return false;
|
|
520
|
+
const text = (s.text || '').trim();
|
|
521
|
+
if (!text || !isValidText(text) || carouselButtonTextHasForbiddenChars(text)) return false;
|
|
522
|
+
if (s.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
|
|
523
|
+
return String(s.phoneNumber || '').length >= 5;
|
|
524
|
+
}
|
|
525
|
+
if (s.type === RCS_BUTTON_TYPES.CTA) {
|
|
526
|
+
const url = String(s.url || '').trim();
|
|
527
|
+
if (!url || url.length > URL_MAX_LENGTH) return false;
|
|
528
|
+
const subtype = s.urlType || RCS_CTA_URL_TYPE.STATIC;
|
|
529
|
+
if (subtype === RCS_CTA_URL_TYPE.DYNAMIC) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
if (!isUrl(url)) return false;
|
|
533
|
+
const varMatches = url.match(invalidVarRegex);
|
|
534
|
+
return !(varMatches && varMatches.length > 0);
|
|
535
|
+
}
|
|
536
|
+
if (s.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const isCarouselCardValid = (card, cardIndex) => {
|
|
543
|
+
if (!card) return false;
|
|
544
|
+
if (!card.title || !card.title.trim()) return false;
|
|
545
|
+
if ((card.title || '').length > TEMPLATE_TITLE_MAX_LENGTH) return false;
|
|
546
|
+
if (!card.description || !card.description.trim()) return false;
|
|
547
|
+
if ((card.description || '').length > RCS_RICH_CARD_MAX_LENGTH) return false;
|
|
548
|
+
let mediaOk = false;
|
|
549
|
+
if (card.mediaType === RCS_MEDIA_TYPES.IMAGE) {
|
|
550
|
+
mediaOk = !!(card.imageSrc && String(card.imageSrc).trim());
|
|
551
|
+
} else if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
|
|
552
|
+
const hasVideo = !!(card.videoAsset && card.videoAsset.videoSrc && String(card.videoAsset.videoSrc).trim());
|
|
553
|
+
const hasThumb = !!(card.thumbnailSrc && String(card.thumbnailSrc).trim());
|
|
554
|
+
mediaOk = hasVideo && hasThumb;
|
|
555
|
+
} else {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
if (!mediaOk) return false;
|
|
559
|
+
if (cardIndex === 0) {
|
|
560
|
+
const sugg = Array.isArray(card.suggestions) ? card.suggestions : [];
|
|
561
|
+
if (!sugg.some(isCompleteSavedCarouselSuggestion)) return false;
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const checkDisableAddCarouselButton = () => {
|
|
567
|
+
const idx = parseInt(activeCarouselIndex, 10);
|
|
568
|
+
const activeCard = carouselData?.[idx];
|
|
569
|
+
return !isCarouselCardValid(activeCard, idx);
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const addCarouselCard = () => {
|
|
573
|
+
let newIndex = 0;
|
|
574
|
+
flushSync(() => {
|
|
575
|
+
setCarouselData((prev = []) => {
|
|
576
|
+
const updated = cloneDeep(prev);
|
|
577
|
+
updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
|
|
578
|
+
newIndex = updated.length - 1;
|
|
579
|
+
return updated;
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
|
|
583
|
+
setActiveCarouselIndex(`${newIndex}`);
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Initialize carousel data when switching to carousel type
|
|
587
|
+
useEffect(() => {
|
|
588
|
+
if (!isCarouselType) return;
|
|
589
|
+
if (!carouselData || carouselData.length === 0) {
|
|
590
|
+
setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
|
|
591
|
+
setCarouselErrors([{}]);
|
|
592
|
+
setActiveCarouselIndex('0');
|
|
593
|
+
}
|
|
594
|
+
}, [isCarouselType]);
|
|
595
|
+
|
|
596
|
+
// keep derived carousel key in sync
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
if (!isCarouselType) return;
|
|
599
|
+
if (!selectedCarouselHeight || !selectedCarouselWidth) return;
|
|
600
|
+
// Required format: HEIGHT_WIDTH
|
|
601
|
+
setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
|
|
602
|
+
}, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
|
|
603
|
+
|
|
604
|
+
const renderCarouselDimensionSelection = () => {
|
|
605
|
+
if (!isCarouselType) return null;
|
|
606
|
+
return (
|
|
607
|
+
<CapRow className="rcs-carousel-dimension-section">
|
|
608
|
+
<CapRow gutter={16} className="rcs-carousel-dimension-row">
|
|
609
|
+
<CapColumn span={12}>
|
|
610
|
+
<CapHeading type="h4" className="rcs-carousel-dimension-label">Card height</CapHeading>
|
|
611
|
+
<CapSelect
|
|
612
|
+
id="rcs-carousel-height-select"
|
|
613
|
+
value={selectedCarouselHeight}
|
|
614
|
+
onChange={(val) => {
|
|
615
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
616
|
+
resetCarouselMediaForAllCards();
|
|
617
|
+
setSelectedCarouselHeight(val);
|
|
618
|
+
}}
|
|
619
|
+
options={CAROUSEL_HEIGHT_OPTIONS}
|
|
620
|
+
disabled={isEditFlow || !isFullMode}
|
|
621
|
+
/>
|
|
622
|
+
</CapColumn>
|
|
623
|
+
<CapColumn span={12}>
|
|
624
|
+
<CapHeading type="h4" className="rcs-carousel-dimension-label">Card width</CapHeading>
|
|
625
|
+
<CapSelect
|
|
626
|
+
id="rcs-carousel-width-select"
|
|
627
|
+
value={selectedCarouselWidth}
|
|
628
|
+
onChange={(val) => {
|
|
629
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
630
|
+
resetCarouselMediaForAllCards();
|
|
631
|
+
setSelectedCarouselWidth(val);
|
|
632
|
+
}}
|
|
633
|
+
options={CAROUSEL_WIDTH_OPTIONS}
|
|
634
|
+
disabled={isEditFlow || !isFullMode}
|
|
635
|
+
/>
|
|
636
|
+
</CapColumn>
|
|
637
|
+
</CapRow>
|
|
638
|
+
{!!selectedCarousel && (
|
|
639
|
+
<CapLabel type="label3" className="rcs-carousel-selected-dimension">
|
|
640
|
+
Selected: {selectedCarousel}
|
|
641
|
+
</CapLabel>
|
|
642
|
+
)}
|
|
643
|
+
</CapRow>
|
|
644
|
+
);
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// Reuse rich-card buttons UI per carousel card
|
|
648
|
+
const renderButtonComponentForCarouselCard = (cardIndex) => {
|
|
649
|
+
const card = carouselData?.[cardIndex] || {};
|
|
650
|
+
const suggestionsForCard = card.suggestions || [];
|
|
651
|
+
return (
|
|
652
|
+
<>
|
|
653
|
+
<CapHeader
|
|
654
|
+
className="rcs-button-cta"
|
|
655
|
+
title={(
|
|
656
|
+
<CapRow type="flex">
|
|
657
|
+
<CapHeading type="h4">
|
|
658
|
+
{formatMessage(messages.btnLabel)}
|
|
659
|
+
</CapHeading>
|
|
660
|
+
</CapRow>
|
|
661
|
+
)}
|
|
662
|
+
description={(
|
|
663
|
+
<CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
|
|
664
|
+
)}
|
|
665
|
+
/>
|
|
666
|
+
<CapActionButton
|
|
667
|
+
buttonType={RCS_BUTTON_TYPES.NONE}
|
|
668
|
+
updateButtonChange={(data, btnIndex) => {
|
|
669
|
+
// Match existing behavior: allow CapActionButton to manage save gating.
|
|
670
|
+
const updated = cloneDeep(suggestionsForCard);
|
|
671
|
+
if (btnIndex === MAX_BUTTONS) {
|
|
672
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: data }]);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
updated[btnIndex] = data;
|
|
676
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
|
|
677
|
+
}}
|
|
678
|
+
deleteButtonHandler={(btnIndex) => {
|
|
679
|
+
if (cardIndex === 0 && btnIndex === 0) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const savedCount = (suggestionsForCard || []).filter((x) => x && x.isSaved).length;
|
|
683
|
+
const target = (suggestionsForCard || []).find((s) => s && s.index === btnIndex);
|
|
684
|
+
if (cardIndex === 0 && target?.isSaved && savedCount <= 1) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const updated = cloneDeep(suggestionsForCard)
|
|
688
|
+
.filter((i) => i.index !== btnIndex)
|
|
689
|
+
.map((i, idx) => ({ ...i, index: idx }));
|
|
690
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'suggestions', value: updated }]);
|
|
691
|
+
}}
|
|
692
|
+
suggestions={suggestionsForCard}
|
|
693
|
+
isEditFlow={isEditFlow}
|
|
694
|
+
isFullMode={isFullMode}
|
|
695
|
+
maxButtons={MAX_BUTTONS}
|
|
696
|
+
host={hostName}
|
|
697
|
+
minSavedSuggestions={cardIndex === 0 ? 1 : 0}
|
|
698
|
+
hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
|
|
699
|
+
/>
|
|
700
|
+
</>
|
|
701
|
+
);
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const renderCarouselCardMedia = (cardIndex) => {
|
|
705
|
+
const card = carouselData?.[cardIndex] || {};
|
|
706
|
+
const dimKey = getCarouselDimensionKey();
|
|
707
|
+
|
|
708
|
+
if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
|
|
709
|
+
return (
|
|
710
|
+
<>
|
|
711
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Video</CapHeading>
|
|
712
|
+
<CapVideoUpload
|
|
713
|
+
index={getCarouselVideoAssetIndex(cardIndex)}
|
|
714
|
+
allowedExtensionsRegex={ALLOWED_EXTENSIONS_VIDEO_REGEX}
|
|
715
|
+
videoSize={RCS_CAROUSEL_VIDEO_SIZE}
|
|
716
|
+
isFullMode={isFullMode}
|
|
717
|
+
uploadAsset={uploadRcsVideo}
|
|
718
|
+
uploadedAssetList={card.videoAsset || {}}
|
|
719
|
+
onVideoUploadUpdateAssestList={(_, val) => {
|
|
720
|
+
handleCarouselValueChange(cardIndex, [{ fieldName: 'videoAsset', value: val }]);
|
|
721
|
+
}}
|
|
722
|
+
videoData={rcsData}
|
|
723
|
+
className="cap-custom-video-upload"
|
|
724
|
+
formClassName={"rcs-video-upload"}
|
|
725
|
+
channel={RCS}
|
|
726
|
+
errorMessage={formatMessage(messages.videoErrorMessage)}
|
|
727
|
+
showVideoNameAndDuration={false}
|
|
728
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
729
|
+
channelSpecificStyle={!isFullMode}
|
|
730
|
+
/>
|
|
731
|
+
|
|
732
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
|
|
733
|
+
<CapImageUpload
|
|
734
|
+
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
735
|
+
imgWidth={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.width}
|
|
736
|
+
imgHeight={RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS?.[dimKey]?.height}
|
|
737
|
+
imgSize={RCS_THUMBNAIL_MAX_SIZE}
|
|
738
|
+
uploadAsset={uploadRcsImage}
|
|
739
|
+
isFullMode={isFullMode}
|
|
740
|
+
imageSrc={card.thumbnailSrc}
|
|
741
|
+
updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: val }])}
|
|
742
|
+
updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'thumbnailSrc', value: '' }])}
|
|
743
|
+
minImgSize={RCS_THUMBNAIL_MIN_SIZE}
|
|
744
|
+
index={getCarouselThumbnailAssetIndex(cardIndex)}
|
|
745
|
+
className="cap-custom-image-upload"
|
|
746
|
+
key={`rcs-carousel-thumb-${cardIndex}-${dimKey}`}
|
|
747
|
+
imageData={rcsData}
|
|
748
|
+
channel={RCS}
|
|
749
|
+
channelSpecificStyle={!isFullMode}
|
|
750
|
+
skipDimensionValidation={true}
|
|
751
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
752
|
+
disabled={isEditFlow || !isFullMode}
|
|
753
|
+
/>
|
|
754
|
+
</>
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Default: IMAGE
|
|
759
|
+
return (
|
|
760
|
+
<>
|
|
761
|
+
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Image</CapHeading>
|
|
762
|
+
<CapImageUpload
|
|
763
|
+
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
764
|
+
imgWidth={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.width}
|
|
765
|
+
imgHeight={RCS_CAROUSEL_IMAGE_DIMENSIONS?.[dimKey]?.height}
|
|
766
|
+
imgSize={RCS_CAROUSEL_IMG_SIZE}
|
|
767
|
+
uploadAsset={uploadRcsImage}
|
|
768
|
+
isFullMode={isFullMode}
|
|
769
|
+
imageSrc={card.imageSrc}
|
|
770
|
+
updateImageSrc={(val) => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: val }])}
|
|
771
|
+
updateOnReUpload={() => handleCarouselValueChange(cardIndex, [{ fieldName: 'imageSrc', value: '' }])}
|
|
772
|
+
index={getCarouselImageAssetIndex(cardIndex)}
|
|
773
|
+
className="cap-custom-image-upload"
|
|
774
|
+
key={`rcs-carousel-image-${cardIndex}-${dimKey}`}
|
|
775
|
+
imageData={rcsData}
|
|
776
|
+
channel={RCS}
|
|
777
|
+
channelSpecificStyle={!isFullMode}
|
|
778
|
+
skipDimensionValidation={true}
|
|
779
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
780
|
+
disabled={isEditFlow || !isFullMode}
|
|
781
|
+
/>
|
|
782
|
+
</>
|
|
783
|
+
);
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const renderCarouselCardButtons = (cardIndex) => {
|
|
787
|
+
return renderButtonComponentForCarouselCard(cardIndex);
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const getCarouselTabPanes = () => {
|
|
791
|
+
return (carouselData || []).map((card, index) => {
|
|
792
|
+
return {
|
|
793
|
+
key: index,
|
|
794
|
+
tab: index + 1,
|
|
795
|
+
content: (
|
|
796
|
+
<CapCard
|
|
797
|
+
title={`Card ${index + 1}`}
|
|
798
|
+
extra={
|
|
799
|
+
!isEditFlow &&
|
|
800
|
+
(carouselData.length === 1 ? (
|
|
801
|
+
<CapTooltip title={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}>
|
|
802
|
+
<span className="button-disabled-tooltip-wrapper rcs-carousel-delete-tooltip-wrap">
|
|
803
|
+
<CapButton
|
|
804
|
+
className="rcs-carousel-card-delete"
|
|
805
|
+
type="flat"
|
|
806
|
+
onClick={() => deleteCarouselCard(index)}
|
|
807
|
+
disabled
|
|
808
|
+
aria-label={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}
|
|
809
|
+
>
|
|
810
|
+
<CapIcon type="delete" />
|
|
811
|
+
</CapButton>
|
|
812
|
+
</span>
|
|
813
|
+
</CapTooltip>
|
|
814
|
+
) : (
|
|
815
|
+
<CapButton
|
|
816
|
+
className="rcs-carousel-card-delete"
|
|
817
|
+
type="flat"
|
|
818
|
+
onClick={() => deleteCarouselCard(index)}
|
|
819
|
+
aria-label={formatMessage(globalMessages.delete)}
|
|
820
|
+
>
|
|
821
|
+
<CapIcon type="delete" />
|
|
822
|
+
</CapButton>
|
|
823
|
+
))
|
|
824
|
+
}
|
|
825
|
+
className="rcs-carousel-card"
|
|
826
|
+
>
|
|
827
|
+
{/* Media selection should be at top of card */}
|
|
828
|
+
<CapRow className="rcs-carousel-media-selection">
|
|
829
|
+
<CapColumn className="rcs-carousel-media-selection-heading">
|
|
830
|
+
<CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
|
|
831
|
+
</CapColumn>
|
|
832
|
+
<CapColumn>
|
|
833
|
+
<CapRadioGroup
|
|
834
|
+
id={`rcs-carousel-media-radio-${index}`}
|
|
835
|
+
options={mediaRadioOptions}
|
|
836
|
+
value={card.mediaType}
|
|
837
|
+
onChange={({ target: { value } }) => {
|
|
838
|
+
// Reset media fields when switching type
|
|
839
|
+
if (value === RCS_MEDIA_TYPES.IMAGE) {
|
|
840
|
+
// Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
|
|
841
|
+
clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
|
|
842
|
+
handleCarouselValueChange(index, [
|
|
843
|
+
{ fieldName: 'mediaType', value },
|
|
844
|
+
{ fieldName: 'videoAsset', value: {} },
|
|
845
|
+
{ fieldName: 'thumbnailSrc', value: '' },
|
|
846
|
+
]);
|
|
847
|
+
} else {
|
|
848
|
+
// Switching to VIDEO: clear image upload so it doesn't auto-restore.
|
|
849
|
+
clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
|
|
850
|
+
handleCarouselValueChange(index, [
|
|
851
|
+
{ fieldName: 'mediaType', value },
|
|
852
|
+
{ fieldName: 'imageSrc', value: '' },
|
|
853
|
+
]);
|
|
854
|
+
}
|
|
855
|
+
}}
|
|
856
|
+
disabled={isEditFlow || !isFullMode}
|
|
857
|
+
className="rcs-radio"
|
|
858
|
+
/>
|
|
859
|
+
</CapColumn>
|
|
860
|
+
</CapRow>
|
|
861
|
+
|
|
862
|
+
<CapRow className="rcs-carousel-media-upload">
|
|
863
|
+
{renderCarouselCardMedia(index)}
|
|
864
|
+
</CapRow>
|
|
865
|
+
|
|
866
|
+
{/* Title after media */}
|
|
867
|
+
<CapRow className="rcs-carousel-card-row">
|
|
868
|
+
<CapHeader
|
|
869
|
+
className="rcs-template-title-label"
|
|
870
|
+
title={<CapHeading type="h4">Card title</CapHeading>}
|
|
871
|
+
suffix={
|
|
872
|
+
(isEditFlow || !isFullMode) ? (
|
|
873
|
+
<TagList
|
|
874
|
+
label={formatMessage(globalMessages.addLabels)}
|
|
875
|
+
onTagSelect={onCarouselTagSelect}
|
|
876
|
+
location={location}
|
|
877
|
+
tags={getRcsTags()}
|
|
878
|
+
onContextChange={handleOnTagsContextChange}
|
|
879
|
+
injectedTags={injectedTags || {}}
|
|
880
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
881
|
+
/>
|
|
882
|
+
) : (
|
|
883
|
+
<CapButton
|
|
884
|
+
data-testid={`rcs-carousel-title-add-var-${index}`}
|
|
885
|
+
type="flat"
|
|
886
|
+
isAddBtn
|
|
887
|
+
onClick={() => appendVarToCarouselField(index, 'title')}
|
|
888
|
+
disabled={!!carouselErrors?.[index]?.title}
|
|
889
|
+
>
|
|
890
|
+
{formatMessage(messages.addVar)}
|
|
891
|
+
</CapButton>
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
/>
|
|
895
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
896
|
+
{(isEditFlow || !isFullMode) ? (
|
|
897
|
+
renderCarouselEditMessage(card.title || '')
|
|
898
|
+
) : (
|
|
899
|
+
<CapInput
|
|
900
|
+
value={card.title || ''}
|
|
901
|
+
placeholder={formatMessage(messages.templateTitlePlaceholder)}
|
|
902
|
+
onChange={({ target: { value } }) => {
|
|
903
|
+
let error = false;
|
|
904
|
+
if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
905
|
+
error = formatMessage(messages.templateHeaderLengthError);
|
|
906
|
+
} else {
|
|
907
|
+
error = variableErrorHandling(value);
|
|
908
|
+
}
|
|
909
|
+
updateCarouselErrors(index, { title: error });
|
|
910
|
+
handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
|
|
911
|
+
}}
|
|
912
|
+
disabled={isEditFlow || !isFullMode}
|
|
913
|
+
errorMessage={carouselErrors?.[index]?.title}
|
|
914
|
+
/>
|
|
915
|
+
)}
|
|
916
|
+
</CapRow>
|
|
917
|
+
</CapRow>
|
|
918
|
+
{!isEditFlow && (
|
|
919
|
+
<CapRow className="rcs-carousel-character-count-row">
|
|
920
|
+
{renderCarouselCharacterCount(
|
|
921
|
+
getCarouselTitleCharacterCount(index),
|
|
922
|
+
getTitleMaxLength(),
|
|
923
|
+
)}
|
|
924
|
+
</CapRow>
|
|
925
|
+
)}
|
|
926
|
+
|
|
927
|
+
{/* Description after title */}
|
|
928
|
+
<CapRow className="rcs-carousel-card-row">
|
|
929
|
+
<CapHeader
|
|
930
|
+
title={<CapHeading type="h4">Card body text</CapHeading>}
|
|
931
|
+
suffix={
|
|
932
|
+
(isEditFlow || !isFullMode) ? (
|
|
933
|
+
<TagList
|
|
934
|
+
label={formatMessage(globalMessages.addLabels)}
|
|
935
|
+
onTagSelect={onCarouselTagSelect}
|
|
936
|
+
location={location}
|
|
937
|
+
tags={getRcsTags()}
|
|
938
|
+
onContextChange={handleOnTagsContextChange}
|
|
939
|
+
injectedTags={injectedTags || {}}
|
|
940
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
941
|
+
/>
|
|
942
|
+
) : (
|
|
943
|
+
<CapButton
|
|
944
|
+
data-testid={`rcs-carousel-desc-add-var-${index}`}
|
|
945
|
+
type="flat"
|
|
946
|
+
isAddBtn
|
|
947
|
+
onClick={() => appendVarToCarouselField(index, 'description')}
|
|
948
|
+
disabled={!!carouselErrors?.[index]?.description}
|
|
949
|
+
>
|
|
950
|
+
{formatMessage(messages.addVar)}
|
|
951
|
+
</CapButton>
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
/>
|
|
955
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
956
|
+
{(isEditFlow || !isFullMode) ? (
|
|
957
|
+
renderCarouselEditMessage(card.description || '')
|
|
958
|
+
) : (
|
|
959
|
+
<TextArea
|
|
960
|
+
autosize={{ minRows: 3, maxRows: 5 }}
|
|
961
|
+
value={card.description || ''}
|
|
962
|
+
placeholder={formatMessage(messages.templateDescPlaceholder)}
|
|
963
|
+
onChange={({ target: { value } }) => {
|
|
964
|
+
let error = false;
|
|
965
|
+
if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
|
|
966
|
+
error = formatMessage(messages.templateMessageLengthError);
|
|
967
|
+
} else {
|
|
968
|
+
error = variableErrorHandling(value);
|
|
969
|
+
}
|
|
970
|
+
updateCarouselErrors(index, { description: error });
|
|
971
|
+
handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
|
|
972
|
+
}}
|
|
973
|
+
disabled={isEditFlow || !isFullMode}
|
|
974
|
+
errorMessage={
|
|
975
|
+
carouselErrors?.[index]?.description && (
|
|
976
|
+
<CapError className="rcs-template-message-error">
|
|
977
|
+
{carouselErrors[index].description}
|
|
978
|
+
</CapError>
|
|
979
|
+
)
|
|
980
|
+
}
|
|
981
|
+
/>
|
|
982
|
+
)}
|
|
983
|
+
</CapRow>
|
|
984
|
+
</CapRow>
|
|
985
|
+
{!isEditFlow && (
|
|
986
|
+
<CapRow className="rcs-carousel-character-count-row">
|
|
987
|
+
{renderCarouselCharacterCount(
|
|
988
|
+
getCarouselDescriptionCharacterCount(index),
|
|
989
|
+
getDescriptionMaxLength(),
|
|
990
|
+
)}
|
|
991
|
+
</CapRow>
|
|
992
|
+
)}
|
|
993
|
+
|
|
994
|
+
<CapDivider className="rcs-carousel-card-divider" />
|
|
995
|
+
{renderCarouselCardButtons(index)}
|
|
996
|
+
</CapCard>
|
|
997
|
+
),
|
|
998
|
+
};
|
|
999
|
+
});
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
const renderCarouselSection = () => {
|
|
1003
|
+
if (!isCarouselType) return null;
|
|
1004
|
+
|
|
1005
|
+
const operations = (
|
|
1006
|
+
<>
|
|
1007
|
+
<CapDivider type="vertical" />
|
|
1008
|
+
<CapButton
|
|
1009
|
+
onClick={addCarouselCard}
|
|
1010
|
+
type="flat"
|
|
1011
|
+
className="add-carousel-content-button"
|
|
1012
|
+
disabled={
|
|
1013
|
+
isEditFlow ||
|
|
1014
|
+
!isFullMode ||
|
|
1015
|
+
MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0) ||
|
|
1016
|
+
checkDisableAddCarouselButton()
|
|
1017
|
+
}
|
|
1018
|
+
>
|
|
1019
|
+
<CapIcon type="plus" />
|
|
1020
|
+
</CapButton>
|
|
1021
|
+
</>
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
return (
|
|
1025
|
+
<CapRow className="rcs-carousel-section">
|
|
1026
|
+
{renderCarouselDimensionSelection()}
|
|
1027
|
+
<CapRow className="rcs-carousel-tab">
|
|
1028
|
+
<CapTab
|
|
1029
|
+
key={`rcs-carousel-tab-${carouselResetNonce}`}
|
|
1030
|
+
defaultActiveKey="0"
|
|
1031
|
+
activeKey={activeCarouselIndex}
|
|
1032
|
+
tabBarExtraContent={operations}
|
|
1033
|
+
onChange={(key) => {
|
|
1034
|
+
setActiveCarouselIndex(`${key}`);
|
|
1035
|
+
// reset focused var when switching cards (as per requirement)
|
|
1036
|
+
setCarouselFocusedVarId('');
|
|
1037
|
+
}}
|
|
1038
|
+
panes={getCarouselTabPanes()}
|
|
1039
|
+
/>
|
|
1040
|
+
</CapRow>
|
|
1041
|
+
</CapRow>
|
|
1042
|
+
);
|
|
1043
|
+
};
|
|
324
1044
|
|
|
325
1045
|
const mediaRadioOptions = [
|
|
326
1046
|
{
|
|
@@ -329,11 +1049,16 @@ export const Rcs = (props) => {
|
|
|
329
1049
|
},
|
|
330
1050
|
{
|
|
331
1051
|
value: RCS_MEDIA_TYPES.VIDEO,
|
|
332
|
-
label: formatMessage(
|
|
1052
|
+
label: formatMessage(
|
|
1053
|
+
templateType === contentType.carousel
|
|
1054
|
+
? messages.carouselMediaVideoOption
|
|
1055
|
+
: messages.mediaVideo,
|
|
1056
|
+
),
|
|
333
1057
|
},
|
|
334
1058
|
];
|
|
335
1059
|
const aiContentBotDisabled = isAiContentBotDisabled();
|
|
336
1060
|
|
|
1061
|
+
|
|
337
1062
|
const updateButtonChange = (data, index) => {
|
|
338
1063
|
if (data && data.text) {
|
|
339
1064
|
const forbiddenError = forbiddenCharactersValidation(data.text);
|
|
@@ -370,7 +1095,9 @@ export const Rcs = (props) => {
|
|
|
370
1095
|
if (isFullMode) return;
|
|
371
1096
|
if (loadingTags || !tags || tags.length === 0) return;
|
|
372
1097
|
const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
373
|
-
const
|
|
1098
|
+
const slotOffset =
|
|
1099
|
+
type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
|
|
1100
|
+
const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
|
|
374
1101
|
if (!resolved) {
|
|
375
1102
|
if (type === TITLE_TEXT) setTemplateTitleError(false);
|
|
376
1103
|
if (type === MESSAGE_TEXT) setTemplateDescError(false);
|
|
@@ -397,10 +1124,16 @@ export const Rcs = (props) => {
|
|
|
397
1124
|
tagModule: getDefaultTags,
|
|
398
1125
|
isFullMode,
|
|
399
1126
|
}) || {};
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1127
|
+
const unsupportedTagsLengthCheck =
|
|
1128
|
+
validationResponse?.unsupportedTags?.length > 0;
|
|
1129
|
+
const errorMsg =
|
|
1130
|
+
(unsupportedTagsLengthCheck &&
|
|
1131
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1132
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
1133
|
+
})) ||
|
|
1134
|
+
(validationResponse.isBraceError &&
|
|
1135
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
1136
|
+
false;
|
|
404
1137
|
if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
|
|
405
1138
|
if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
|
|
406
1139
|
};
|
|
@@ -413,15 +1146,98 @@ export const Rcs = (props) => {
|
|
|
413
1146
|
validateResolvedTagsForType(MESSAGE_TEXT);
|
|
414
1147
|
}, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
|
|
415
1148
|
|
|
1149
|
+
useEffect(() => {
|
|
1150
|
+
if (isFullMode || !isCarouselType) return;
|
|
1151
|
+
if (loadingTags || !tags || tags.length === 0) return;
|
|
1152
|
+
(carouselData || []).forEach((card, idx) => {
|
|
1153
|
+
['title', 'description'].forEach((field) => {
|
|
1154
|
+
const templateStr = card?.[field] || '';
|
|
1155
|
+
if (!templateStr) return;
|
|
1156
|
+
const resolved = resolveTemplateWithMap(templateStr);
|
|
1157
|
+
if (!resolved) {
|
|
1158
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
let contentForValidation = resolved;
|
|
1162
|
+
const placeholderTokens = templateStr.match(rcsVarRegex) || [];
|
|
1163
|
+
placeholderTokens.forEach((t) => {
|
|
1164
|
+
const escaped = t.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
1165
|
+
contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
|
|
1166
|
+
});
|
|
1167
|
+
if (!contentForValidation.trim()) {
|
|
1168
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const validationResponse = validateTags({
|
|
1172
|
+
content: contentForValidation,
|
|
1173
|
+
tagsParam: tags,
|
|
1174
|
+
injectedTagsParams: injectedTags,
|
|
1175
|
+
location,
|
|
1176
|
+
tagModule: getDefaultTags,
|
|
1177
|
+
eventContextTags,
|
|
1178
|
+
isFullMode,
|
|
1179
|
+
}) || {};
|
|
1180
|
+
const errorMsg =
|
|
1181
|
+
(validationResponse?.unsupportedTags?.length > 0 &&
|
|
1182
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1183
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
1184
|
+
})) ||
|
|
1185
|
+
(validationResponse.isBraceError &&
|
|
1186
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
1187
|
+
false;
|
|
1188
|
+
updateCarouselErrors(idx, { [field]: errorMsg });
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
}, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
|
|
1192
|
+
|
|
416
1193
|
const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
|
|
417
1194
|
|
|
418
|
-
const
|
|
1195
|
+
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
1196
|
+
|
|
1197
|
+
/** Same `{{tag}}` in both title and description must not share one semantic map entry. */
|
|
1198
|
+
const rcsSpanningSemanticVarNames = useMemo(
|
|
1199
|
+
() => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
|
|
1200
|
+
[templateTitle, templateDesc],
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
/** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
|
|
1204
|
+
const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
|
|
1205
|
+
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
1206
|
+
const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
|
|
1207
|
+
const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
|
|
1208
|
+
let varOrdinal = 0;
|
|
1209
|
+
for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
|
|
1210
|
+
const segmentToken = templateSegments[segmentIndexInField];
|
|
1211
|
+
if (rcsVarTestRegex.test(segmentToken)) {
|
|
1212
|
+
if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
|
|
1213
|
+
return offset + varOrdinal;
|
|
1214
|
+
}
|
|
1215
|
+
varOrdinal += 1;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return null;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
|
|
1223
|
+
* multi-field variable resolution.
|
|
1224
|
+
*/
|
|
1225
|
+
const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
|
|
419
1226
|
if (!str) return '';
|
|
420
|
-
const arr =
|
|
1227
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
1228
|
+
let varOrdinal = 0;
|
|
421
1229
|
return arr.map((elem) => {
|
|
422
1230
|
if (rcsVarTestRegex.test(elem)) {
|
|
423
1231
|
const key = getVarNameFromToken(elem);
|
|
424
|
-
const
|
|
1232
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
1233
|
+
varOrdinal += 1;
|
|
1234
|
+
const v = resolveCardVarMappedSlotValue(
|
|
1235
|
+
cardVarMapped,
|
|
1236
|
+
key,
|
|
1237
|
+
globalSlot,
|
|
1238
|
+
isEditLike,
|
|
1239
|
+
rcsSpanningSemanticVarNames.has(key),
|
|
1240
|
+
);
|
|
425
1241
|
if (isNil(v) || String(v)?.trim?.() === '') return elem;
|
|
426
1242
|
return String(v);
|
|
427
1243
|
}
|
|
@@ -429,39 +1245,128 @@ export const Rcs = (props) => {
|
|
|
429
1245
|
}).join('');
|
|
430
1246
|
};
|
|
431
1247
|
|
|
1248
|
+
const buildCarouselCardsForPreview = (cards = []) => (cards || []).map((card = {}) => {
|
|
1249
|
+
const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
|
|
1250
|
+
const videoSrc = card.videoAsset?.videoSrc || '';
|
|
1251
|
+
const resolvedTitle = !isFullMode ? resolveTemplateWithMap(card.title || '') : (card.title || '');
|
|
1252
|
+
const resolvedDesc = !isFullMode ? resolveTemplateWithMap(card.description || '') : (card.description || '');
|
|
1253
|
+
return {
|
|
1254
|
+
mediaType: (card.mediaType || '').toLowerCase(),
|
|
1255
|
+
imageSrc: card.imageSrc || '',
|
|
1256
|
+
videoSrc,
|
|
1257
|
+
videoPreviewImg: videoThumb,
|
|
1258
|
+
title: resolvedTitle,
|
|
1259
|
+
bodyText: resolvedDesc,
|
|
1260
|
+
suggestions: card.suggestions || [],
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
432
1263
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1264
|
+
/**
|
|
1265
|
+
* Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
|
|
1266
|
+
* (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
|
|
1267
|
+
* TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
|
|
1268
|
+
*/
|
|
1269
|
+
const getTemplateContent = useCallback(() => {
|
|
1270
|
+
if (templateType === contentType.carousel) {
|
|
1271
|
+
const carouselDimKey = getCarouselDimensionKey();
|
|
1272
|
+
const carouselImgDims =
|
|
1273
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
1274
|
+
const carouselVidDims =
|
|
1275
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
1276
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
1277
|
+
return {
|
|
1278
|
+
carouselData: buildCarouselCardsForPreview(carouselData),
|
|
1279
|
+
carouselPreviewDimensions: {
|
|
1280
|
+
imageWidth: carouselImgDims.width,
|
|
1281
|
+
imageHeight: carouselImgDims.height,
|
|
1282
|
+
videoThumbWidth: carouselVidDims.width,
|
|
1283
|
+
videoThumbHeight: carouselVidDims.height,
|
|
1284
|
+
},
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
1288
|
+
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
1289
|
+
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
1290
|
+
|
|
1291
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
1292
|
+
const titleVarCountForResolve = isMediaTypeText
|
|
1293
|
+
? 0
|
|
1294
|
+
: ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
|
|
1295
|
+
const resolvedTitle = isMediaTypeText
|
|
1296
|
+
? ''
|
|
1297
|
+
: (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
|
|
1298
|
+
const resolvedDesc = isSlotMappingMode
|
|
1299
|
+
? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
|
|
1300
|
+
: templateDesc;
|
|
1301
|
+
|
|
1302
|
+
const mediaPreview = {};
|
|
1303
|
+
if (isMediaTypeImage && rcsImageSrc) {
|
|
1304
|
+
mediaPreview.rcsImageSrc = rcsImageSrc;
|
|
1305
|
+
}
|
|
1306
|
+
if (isMediaTypeVideo && !isMediaTypeText) {
|
|
1307
|
+
if (rcsThumbnailSrc) {
|
|
1308
|
+
mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
|
|
1309
|
+
} else if (rcsVideoSrc?.videoSrc) {
|
|
1310
|
+
mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
|
|
1311
|
+
}
|
|
1312
|
+
if (rcsThumbnailSrc) {
|
|
1313
|
+
mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
453
1316
|
|
|
1317
|
+
const contentObj = {
|
|
1318
|
+
templateHeader: resolvedTitle,
|
|
1319
|
+
templateMessage: resolvedDesc,
|
|
1320
|
+
...mediaPreview,
|
|
1321
|
+
...(suggestions.length > 0 && {
|
|
1322
|
+
suggestions: suggestions,
|
|
1323
|
+
}),
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
return contentObj;
|
|
1327
|
+
}, [
|
|
1328
|
+
templateMediaType,
|
|
1329
|
+
templateTitle,
|
|
1330
|
+
templateDesc,
|
|
1331
|
+
rcsImageSrc,
|
|
1332
|
+
rcsVideoSrc,
|
|
1333
|
+
rcsThumbnailSrc,
|
|
1334
|
+
suggestions,
|
|
1335
|
+
selectedDimension,
|
|
1336
|
+
isFullMode,
|
|
1337
|
+
isEditFlow,
|
|
1338
|
+
cardVarMapped,
|
|
1339
|
+
rcsSpanningSemanticVarNames,
|
|
1340
|
+
carouselData,
|
|
1341
|
+
selectedCarouselHeight,
|
|
1342
|
+
selectedCarouselWidth,
|
|
1343
|
+
]);
|
|
1344
|
+
|
|
1345
|
+
const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
|
|
454
1346
|
|
|
455
|
-
const RcsLabel = styled.div`
|
|
456
|
-
display: flex;
|
|
457
|
-
margin-top: 20px;
|
|
458
|
-
`;
|
|
459
1347
|
const paramObj = params || {};
|
|
460
1348
|
useEffect(() => {
|
|
461
1349
|
const { id } = paramObj;
|
|
462
1350
|
if (id && isFullMode) {
|
|
463
1351
|
setSpin(true);
|
|
464
1352
|
actions.getTemplateDetails(id, setSpin);
|
|
1353
|
+
} else if (!id && isFullMode) {
|
|
1354
|
+
// Create New: clear standalone media and ALL possible carousel card Redux slots.
|
|
1355
|
+
// Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
|
|
1356
|
+
updateRcsImageSrc('');
|
|
1357
|
+
setRcsVideoSrc({});
|
|
1358
|
+
setRcsThumbnailSrc('');
|
|
1359
|
+
setAssetList({});
|
|
1360
|
+
setEditFlow(false);
|
|
1361
|
+
actions.clearRcsMediaAsset(0);
|
|
1362
|
+
actions.clearRcsMediaAsset(1);
|
|
1363
|
+
for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
|
|
1364
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
|
|
1365
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
|
|
1366
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
|
|
1367
|
+
}
|
|
1368
|
+
setCarouselData([]);
|
|
1369
|
+
setCarouselErrors([]);
|
|
465
1370
|
}
|
|
466
1371
|
return () => {
|
|
467
1372
|
actions.clearEditResponse();
|
|
@@ -469,67 +1374,79 @@ export const Rcs = (props) => {
|
|
|
469
1374
|
}, [paramObj.id]);
|
|
470
1375
|
|
|
471
1376
|
useEffect(() => {
|
|
472
|
-
|
|
1377
|
+
if (!(isEditFlow || !isFullMode)) return;
|
|
473
1378
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1379
|
+
const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
|
|
1380
|
+
|
|
1381
|
+
const initField = (targetString, setVarMap, slotOffset) => {
|
|
1382
|
+
const arr = splitTemplateVarStringRcs(targetString);
|
|
1383
|
+
if (!arr?.length) {
|
|
1384
|
+
setVarMap({});
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
const nextVarMap = {};
|
|
1388
|
+
let varOrdinal = 0;
|
|
1389
|
+
arr.forEach((elem, idx) => {
|
|
1390
|
+
// Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
|
|
1391
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
1392
|
+
const id = `${elem}_${idx}`;
|
|
1393
|
+
const varName = getVarNameFromToken(elem);
|
|
1394
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
1395
|
+
varOrdinal += 1;
|
|
1396
|
+
const mappedValue = resolveCardVarMappedSlotValue(
|
|
1397
|
+
cardVarMapped,
|
|
1398
|
+
varName,
|
|
1399
|
+
globalSlot,
|
|
1400
|
+
isEditLike,
|
|
1401
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
1402
|
+
);
|
|
1403
|
+
nextVarMap[id] = mappedValue;
|
|
480
1404
|
}
|
|
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
|
-
};
|
|
1405
|
+
});
|
|
1406
|
+
setVarMap(nextVarMap);
|
|
1407
|
+
};
|
|
500
1408
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1409
|
+
initField(templateTitle, setTitleVarMappedData, 0);
|
|
1410
|
+
initField(templateDesc, setDescVarMappedData, titleTokenCount);
|
|
1411
|
+
}, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
|
|
1412
|
+
|
|
1413
|
+
useEffect(() => {
|
|
1414
|
+
if (!isEditFlow && isFullMode) {
|
|
507
1415
|
setRcsVideoSrc({});
|
|
508
1416
|
updateRcsImageSrc('');
|
|
509
|
-
|
|
510
|
-
updateRcsThumbnailSrc('');
|
|
1417
|
+
setRcsThumbnailSrc('');
|
|
511
1418
|
setAssetList({});
|
|
512
|
-
|
|
513
|
-
|
|
1419
|
+
}
|
|
1420
|
+
}, [templateMediaType]);
|
|
514
1421
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1422
|
+
/** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
|
|
1423
|
+
const templateStatusHelper = (cardContentFirst) => {
|
|
1424
|
+
const raw =
|
|
1425
|
+
cardContentFirst?.Status
|
|
1426
|
+
?? cardContentFirst?.status
|
|
1427
|
+
?? cardContentFirst?.approvalStatus
|
|
1428
|
+
?? '';
|
|
1429
|
+
const status = typeof raw === 'string' ? raw.trim() : String(raw);
|
|
1430
|
+
const n = status.toLowerCase();
|
|
1431
|
+
switch (n) {
|
|
1432
|
+
case 'approved':
|
|
519
1433
|
setTemplateStatus(RCS_STATUSES.approved);
|
|
520
1434
|
break;
|
|
521
|
-
case
|
|
1435
|
+
case 'pending':
|
|
522
1436
|
setTemplateStatus(RCS_STATUSES.pending);
|
|
523
1437
|
break;
|
|
524
|
-
case
|
|
1438
|
+
case 'awaitingapproval':
|
|
525
1439
|
setTemplateStatus(RCS_STATUSES.awaitingApproval);
|
|
526
1440
|
break;
|
|
527
|
-
case
|
|
1441
|
+
case 'unavailable':
|
|
528
1442
|
setTemplateStatus(RCS_STATUSES.unavailable);
|
|
529
1443
|
break;
|
|
530
|
-
case
|
|
1444
|
+
case 'rejected':
|
|
531
1445
|
setTemplateStatus(RCS_STATUSES.rejected);
|
|
532
1446
|
break;
|
|
1447
|
+
case 'created':
|
|
1448
|
+
setTemplateStatus(RCS_STATUSES.created);
|
|
1449
|
+
break;
|
|
533
1450
|
default:
|
|
534
1451
|
setTemplateStatus(status);
|
|
535
1452
|
break;
|
|
@@ -583,48 +1500,283 @@ export const Rcs = (props) => {
|
|
|
583
1500
|
};
|
|
584
1501
|
|
|
585
1502
|
useEffect(() => {
|
|
586
|
-
const details =
|
|
1503
|
+
const details = rcsHydrationDetails;
|
|
587
1504
|
if (details && Object.keys(details).length > 0) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1505
|
+
// Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
|
|
1506
|
+
const cardFromVersions = get(
|
|
1507
|
+
details,
|
|
1508
|
+
'versions.base.content.RCS.rcsContent.cardContent[0]',
|
|
1509
|
+
);
|
|
1510
|
+
const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
|
|
1511
|
+
const cardType = (rcsContent?.cardType || '').toString().toLowerCase();
|
|
1512
|
+
|
|
1513
|
+
setEditFlow(true);
|
|
1514
|
+
setTemplateName(details?.name || details?.creativeName || '');
|
|
1515
|
+
|
|
1516
|
+
const cardFromTop = get(details, 'rcsContent.cardContent[0]');
|
|
1517
|
+
const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
|
|
1518
|
+
const cardVarMappedFromCardContent =
|
|
1519
|
+
card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
|
|
1520
|
+
? card0.cardVarMapped
|
|
1521
|
+
: {};
|
|
1522
|
+
const cardVarMappedFromRootMirror =
|
|
1523
|
+
details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
|
|
1524
|
+
? details.rcsCardVarMapped
|
|
1525
|
+
: {};
|
|
1526
|
+
// Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
|
|
1527
|
+
// nested versions.cardContent[0].cardVarMapped is dropped on reload.
|
|
1528
|
+
const mergedCardVarMappedFromPayload = {
|
|
1529
|
+
...cardVarMappedFromRootMirror,
|
|
1530
|
+
...cardVarMappedFromCardContent,
|
|
1531
|
+
};
|
|
1532
|
+
const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
|
|
1533
|
+
const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
|
|
1534
|
+
const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
|
|
1535
|
+
Object.keys(mergedCardVarMappedFromPayload)
|
|
1536
|
+
.sort()
|
|
1537
|
+
.reduce((accumulator, mapKey) => {
|
|
1538
|
+
accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
|
|
1539
|
+
return accumulator;
|
|
1540
|
+
}, {}),
|
|
1541
|
+
)}`;
|
|
1542
|
+
if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
|
|
1543
|
+
lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
|
|
1544
|
+
setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
|
|
1545
|
+
}
|
|
1546
|
+
const tokenListForMap = [
|
|
1547
|
+
...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
|
|
1548
|
+
...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
|
|
1549
|
+
];
|
|
1550
|
+
const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
|
|
1551
|
+
// Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
|
|
1552
|
+
// getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
|
|
1553
|
+
const cardVarMappedBeforeCoalesce = isFullMode
|
|
1554
|
+
? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
|
|
1555
|
+
: { ...mergedCardVarMappedFromPayload };
|
|
1556
|
+
const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
|
|
1557
|
+
cardVarMappedBeforeCoalesce,
|
|
1558
|
+
loadedTitleForMap,
|
|
1559
|
+
loadedDescForMap,
|
|
1560
|
+
rcsVarRegex,
|
|
1561
|
+
);
|
|
1562
|
+
const cardVarMappedAfterNumericSlotSync = !isFullMode
|
|
1563
|
+
? syncCardVarMappedSemanticsFromSlots(
|
|
1564
|
+
cardVarMappedAfterCoalesce,
|
|
1565
|
+
loadedTitleForMap,
|
|
1566
|
+
loadedDescForMap,
|
|
1567
|
+
rcsVarRegex,
|
|
1568
|
+
)
|
|
1569
|
+
: cardVarMappedAfterCoalesce;
|
|
1570
|
+
const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
|
|
1571
|
+
// Pre-populate variable/tag mappings while opening an existing template in edit flows
|
|
1572
|
+
setCardVarMapped((previousVarMapState) => {
|
|
1573
|
+
const previousVarMap = previousVarMapState ?? {};
|
|
1574
|
+
if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
|
|
1575
|
+
const previousVarMapKeys = Object.keys(previousVarMap);
|
|
1576
|
+
const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
|
|
1577
|
+
if (previousVarMapKeys.length === nextVarMapKeys.length) {
|
|
1578
|
+
const allSlotValuesMatchPrevious = previousVarMapKeys.every(
|
|
1579
|
+
(key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
|
|
1580
|
+
);
|
|
1581
|
+
if (allSlotValuesMatchPrevious) return previousVarMapState;
|
|
1582
|
+
}
|
|
1583
|
+
return hydratedCardVarMappedResult;
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
if (cardType === contentType.carousel) {
|
|
1587
|
+
|
|
1588
|
+
setTemplateType(contentType.carousel);
|
|
1589
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1590
|
+
const cardSettings = rcsContent?.cardSettings || {};
|
|
1591
|
+
const cardWidth = cardSettings?.cardWidth || SMALL;
|
|
1592
|
+
setSelectedCarouselWidth(cardWidth);
|
|
1593
|
+
|
|
1594
|
+
const cards = Array.isArray(rcsContent?.cardContent) ? rcsContent.cardContent : [];
|
|
1595
|
+
const firstHeight = cards?.[0]?.media?.height || MEDIUM;
|
|
1596
|
+
setSelectedCarouselHeight(firstHeight);
|
|
1597
|
+
setSelectedCarousel(`${firstHeight}_${cardWidth}`);
|
|
1598
|
+
setActiveCarouselIndex('0');
|
|
1599
|
+
|
|
1600
|
+
const hydratedCards = cards.map((c = {}, idx) => {
|
|
1601
|
+
const mediaType = c.mediaType;
|
|
1602
|
+
const media = c.media || {};
|
|
1603
|
+
const mediaUrl = media.mediaUrl || '';
|
|
1604
|
+
const thumbUrl = media.thumbnailUrl || '';
|
|
1605
|
+
const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
|
|
1606
|
+
const suggestions = idx === 0 && rawSuggestions.length === 0
|
|
1607
|
+
? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
|
|
1608
|
+
: rawSuggestions;
|
|
1609
|
+
return {
|
|
1610
|
+
title: c.title || '',
|
|
1611
|
+
description: c.description || '',
|
|
1612
|
+
mediaType,
|
|
1613
|
+
imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
|
|
1614
|
+
videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
|
|
1615
|
+
videoSrc: mediaUrl,
|
|
1616
|
+
previewUrl: thumbUrl,
|
|
1617
|
+
videoThumbnail: thumbUrl,
|
|
1618
|
+
videoName: c?.media?.videoName || '',
|
|
1619
|
+
} : {},
|
|
1620
|
+
thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
|
|
1621
|
+
suggestions,
|
|
1622
|
+
};
|
|
1623
|
+
});
|
|
1624
|
+
setCarouselData(
|
|
1625
|
+
hydratedCards.length > 0
|
|
1626
|
+
? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
|
|
1627
|
+
: [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
|
|
1628
|
+
);
|
|
1629
|
+
setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
|
|
1630
|
+
|
|
1631
|
+
// Status bar uses first card's Status, keep existing behavior.
|
|
1632
|
+
if (isHostInfoBip) {
|
|
1633
|
+
setTemplateStatus('');
|
|
1634
|
+
} else {
|
|
1635
|
+
const firstCard = cards?.[0] || {};
|
|
1636
|
+
const cardForCarouselStatus = {
|
|
1637
|
+
...firstCard,
|
|
1638
|
+
Status:
|
|
1639
|
+
firstCard.Status
|
|
1640
|
+
?? firstCard.status
|
|
1641
|
+
?? firstCard.approvalStatus
|
|
1642
|
+
?? get(details, 'templateStatus')
|
|
1643
|
+
?? get(details, 'approvalStatus')
|
|
1644
|
+
?? get(details, 'creativeStatus')
|
|
1645
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1646
|
+
?? '',
|
|
1647
|
+
};
|
|
1648
|
+
templateStatusHelper(cardForCarouselStatus);
|
|
1649
|
+
}
|
|
1650
|
+
return;
|
|
591
1651
|
}
|
|
592
|
-
|
|
593
|
-
|
|
1652
|
+
|
|
1653
|
+
const mediaType =
|
|
1654
|
+
card0.mediaType
|
|
1655
|
+
|| get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
|
|
1656
|
+
if (cardType !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
|
|
594
1657
|
setTemplateType(contentType.text_message);
|
|
595
|
-
} else {
|
|
1658
|
+
} else if (cardType !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
|
|
596
1659
|
setTemplateType(contentType.rich_card);
|
|
597
1660
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1661
|
+
|
|
1662
|
+
const loadedTitle = loadedTitleForMap;
|
|
1663
|
+
const loadedDesc = loadedDescForMap;
|
|
1664
|
+
const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
|
|
1665
|
+
loadedTitle,
|
|
1666
|
+
loadedDesc,
|
|
1667
|
+
isFullMode,
|
|
1668
|
+
cardVarMappedAfterHydration: hydratedCardVarMappedResult,
|
|
1669
|
+
rcsVarRegex,
|
|
1670
|
+
});
|
|
605
1671
|
setTemplateTitle(normalizedTitle);
|
|
606
1672
|
setTemplateDesc(normalizedDesc);
|
|
607
|
-
setSuggestions(
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1673
|
+
setSuggestions(
|
|
1674
|
+
Array.isArray(card0.suggestions)
|
|
1675
|
+
? card0.suggestions
|
|
1676
|
+
: get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
|
|
1677
|
+
);
|
|
1678
|
+
const cardForStatus = {
|
|
1679
|
+
...card0,
|
|
1680
|
+
Status:
|
|
1681
|
+
card0.Status
|
|
1682
|
+
?? card0.status
|
|
1683
|
+
?? card0.approvalStatus
|
|
1684
|
+
?? get(details, 'templateStatus')
|
|
1685
|
+
?? get(details, 'approvalStatus')
|
|
1686
|
+
?? get(details, 'creativeStatus')
|
|
1687
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1688
|
+
?? '',
|
|
1689
|
+
};
|
|
1690
|
+
if (isHostInfoBip) {
|
|
1691
|
+
setTemplateStatus('');
|
|
1692
|
+
} else {
|
|
1693
|
+
templateStatusHelper(cardForStatus);
|
|
1694
|
+
}
|
|
1695
|
+
const mediaData =
|
|
1696
|
+
card0.media != null && card0.media !== ''
|
|
1697
|
+
? card0.media
|
|
1698
|
+
: get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
|
|
1699
|
+
const cardSettings =
|
|
1700
|
+
get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
|
|
1701
|
+
|| get(details, 'rcsContent.cardSettings', '');
|
|
611
1702
|
setMediaData(mediaData, mediaType, cardSettings);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1703
|
+
|
|
1704
|
+
const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
|
|
1705
|
+
const base = get(smsFallbackContent, 'versions.base', {});
|
|
1706
|
+
const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
|
|
1707
|
+
const smsEditor = base['sms-editor'];
|
|
1708
|
+
const fromNested = Array.isArray(updatedEditor)
|
|
1709
|
+
? updatedEditor.join('')
|
|
1710
|
+
: (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
|
|
1711
|
+
const fallbackMessage = smsFallbackContent.smsContent
|
|
1712
|
+
|| smsFallbackContent.smsTemplateContent
|
|
1713
|
+
|| smsFallbackContent.message
|
|
1714
|
+
|| fromNested
|
|
1715
|
+
|| '';
|
|
1716
|
+
const varMappedFromPayload = smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {};
|
|
1717
|
+
const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
|
|
1718
|
+
const hasFallbackPayload =
|
|
1719
|
+
smsFallbackContent
|
|
1720
|
+
&& Object.keys(smsFallbackContent).length > 0
|
|
1721
|
+
&& (
|
|
1722
|
+
!!smsFallbackContent.smsTemplateName
|
|
1723
|
+
|| !!fallbackMessage
|
|
1724
|
+
|| hasVarMapped
|
|
1725
|
+
);
|
|
1726
|
+
if (hasFallbackPayload) {
|
|
1727
|
+
if (!fallbackMessage && !hasVarMapped && process.env.NODE_ENV !== 'production') {
|
|
1728
|
+
console.warn('[RCS SMS Fallback] No message text found in API response. Inspect shape:', smsFallbackContent);
|
|
1729
|
+
}
|
|
1730
|
+
const unicodeFromApi =
|
|
1731
|
+
typeof smsFallbackContent.unicodeValidity === 'boolean'
|
|
1732
|
+
? smsFallbackContent.unicodeValidity
|
|
1733
|
+
: (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
|
|
1734
|
+
const registeredSenderIdsFromApi =
|
|
1735
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
|
|
1736
|
+
const nextSmsState = {
|
|
1737
|
+
templateName: smsFallbackContent.smsTemplateName || '',
|
|
1738
|
+
content: fallbackMessage,
|
|
1739
|
+
templateContent: fallbackMessage,
|
|
1740
|
+
unicodeValidity: unicodeFromApi,
|
|
1741
|
+
...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
|
|
1742
|
+
...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
|
|
1743
|
+
? { registeredSenderIds: registeredSenderIdsFromApi }
|
|
1744
|
+
: {}),
|
|
1745
|
+
};
|
|
1746
|
+
const hydrationKey = JSON.stringify({
|
|
1747
|
+
creativeKey: details._id || details.name || details.creativeName || '',
|
|
1748
|
+
templateName: nextSmsState.templateName,
|
|
1749
|
+
content: nextSmsState.content,
|
|
1750
|
+
unicodeValidity: nextSmsState.unicodeValidity,
|
|
1751
|
+
varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
|
|
1752
|
+
senderIds:
|
|
1753
|
+
Array.isArray(registeredSenderIdsFromApi)
|
|
1754
|
+
? registeredSenderIdsFromApi.join('\u001f')
|
|
1755
|
+
: '',
|
|
1756
|
+
});
|
|
1757
|
+
if (
|
|
1758
|
+
isFullMode
|
|
1759
|
+
|| lastSmsFallbackHydrationKeyRef.current !== hydrationKey
|
|
1760
|
+
) {
|
|
1761
|
+
lastSmsFallbackHydrationKeyRef.current = hydrationKey;
|
|
1762
|
+
setSmsFallbackData(nextSmsState);
|
|
1763
|
+
}
|
|
1764
|
+
} else if (isFullMode || lastSmsFallbackHydrationKeyRef.current !== '__EMPTY__') {
|
|
1765
|
+
lastSmsFallbackHydrationKeyRef.current = '__EMPTY__';
|
|
1766
|
+
setSmsFallbackData(null);
|
|
615
1767
|
}
|
|
616
1768
|
}
|
|
617
|
-
}, [
|
|
618
|
-
|
|
1769
|
+
}, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
|
|
619
1770
|
|
|
620
1771
|
useEffect(() => {
|
|
621
1772
|
if (templateType === contentType.text_message) {
|
|
622
1773
|
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
623
|
-
|
|
624
|
-
|
|
1774
|
+
// Full-mode create only: switching to plain text clears draft title/media. Never clear when
|
|
1775
|
+
// hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
|
|
625
1776
|
if (!isEditFlow && isFullMode) {
|
|
1777
|
+
setTemplateTitle('');
|
|
1778
|
+
setTemplateTitleError('');
|
|
626
1779
|
setUpdateRcsImageSrc('');
|
|
627
|
-
setUpdateRcsVideoSrc({});
|
|
628
1780
|
setRcsVideoSrc({});
|
|
629
1781
|
setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
|
|
630
1782
|
}
|
|
@@ -651,7 +1803,8 @@ export const Rcs = (props) => {
|
|
|
651
1803
|
if (!showDltContainer) {
|
|
652
1804
|
const { type, module } = location.query || {};
|
|
653
1805
|
const isEmbedded = type === EMBEDDED;
|
|
654
|
-
|
|
1806
|
+
// Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
|
|
1807
|
+
const context = isEmbedded ? module : 'outbound';
|
|
655
1808
|
const embedded = isEmbedded ? type : FULL;
|
|
656
1809
|
const query = {
|
|
657
1810
|
layout: SMS,
|
|
@@ -662,9 +1815,9 @@ export const Rcs = (props) => {
|
|
|
662
1815
|
if (getDefaultTags) {
|
|
663
1816
|
query.context = getDefaultTags;
|
|
664
1817
|
}
|
|
665
|
-
|
|
1818
|
+
fetchTagSchemaIfNewQuery(query);
|
|
666
1819
|
}
|
|
667
|
-
}, [showDltContainer]);
|
|
1820
|
+
}, [showDltContainer, fetchTagSchemaIfNewQuery]);
|
|
668
1821
|
|
|
669
1822
|
useEffect(() => {
|
|
670
1823
|
let tag = get(metaEntities, `tags.standard`, []);
|
|
@@ -687,16 +1840,114 @@ export const Rcs = (props) => {
|
|
|
687
1840
|
context,
|
|
688
1841
|
embedded,
|
|
689
1842
|
};
|
|
1843
|
+
if (getDefaultTags) {
|
|
1844
|
+
query.context = getDefaultTags;
|
|
1845
|
+
}
|
|
1846
|
+
fetchTagSchemaIfNewQuery(query);
|
|
690
1847
|
globalActions.fetchSchemaForEntity(query);
|
|
691
1848
|
};
|
|
692
1849
|
|
|
693
|
-
const
|
|
694
|
-
if (!
|
|
695
|
-
const
|
|
1850
|
+
const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
|
|
1851
|
+
if (!templateStr || !numericVarName || !tagName) return templateStr;
|
|
1852
|
+
const re = buildRcsNumericMustachePlaceholderRegex(numericVarName);
|
|
1853
|
+
return templateStr.replace(re, `{{${tagName}}}`);
|
|
1854
|
+
};
|
|
1855
|
+
|
|
1856
|
+
const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
|
|
1857
|
+
if (!varSegmentCompositeDomId) return;
|
|
1858
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
1859
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
1860
|
+
const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
|
|
1861
|
+
if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
|
|
1862
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
1863
|
+
const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
1864
|
+
if (!semanticOrNumericVarName) return;
|
|
1865
|
+
const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
|
|
1866
|
+
const templateStringForField =
|
|
1867
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
|
|
1868
|
+
const titleOrMessageFieldType =
|
|
1869
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
|
|
1870
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
1871
|
+
varSegmentCompositeDomId,
|
|
1872
|
+
templateStringForField,
|
|
1873
|
+
titleOrMessageFieldType,
|
|
1874
|
+
);
|
|
1875
|
+
const cardVarMappedNumericSlotKey =
|
|
1876
|
+
globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
|
|
1877
|
+
? String(globalVarSlotIndexZeroBased + 1)
|
|
1878
|
+
: null;
|
|
1879
|
+
|
|
1880
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
1881
|
+
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
1882
|
+
if (isNumericPlaceholderSlot) {
|
|
1883
|
+
const existingValueBeforeAppend = (
|
|
1884
|
+
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
1885
|
+
).toString();
|
|
1886
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1887
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
1888
|
+
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
1889
|
+
} else {
|
|
1890
|
+
// Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
|
|
1891
|
+
// "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
|
|
1892
|
+
// read/write the global numeric slot only and drop the shared semantic key.
|
|
1893
|
+
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
1894
|
+
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
1895
|
+
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
1896
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1897
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
1898
|
+
if (cardVarMappedNumericSlotKey) {
|
|
1899
|
+
updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
|
|
1900
|
+
} else {
|
|
1901
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return updatedCardVarMapped;
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
if (
|
|
1908
|
+
isNumericPlaceholderSlot
|
|
1909
|
+
&& (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
|
|
1910
|
+
) {
|
|
1911
|
+
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
1912
|
+
setTemplateTitle((previousTitle) => {
|
|
1913
|
+
const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
1914
|
+
previousTitle || '',
|
|
1915
|
+
semanticOrNumericVarName,
|
|
1916
|
+
selectedTagNameFromPicker,
|
|
1917
|
+
);
|
|
1918
|
+
if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
|
|
1919
|
+
setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
|
|
1920
|
+
// Remount segment editor: tag insert replaces {{n}} with e.g. {{tag.FORMAT_1}} — slot ids change; avoids stale UI vs manual typing in full-mode TextArea
|
|
1921
|
+
setRcsVarSegmentEditorRemountKey((k) => k + 1);
|
|
1922
|
+
return titleAfterReplacingNumericPlaceholder;
|
|
1923
|
+
});
|
|
1924
|
+
} else {
|
|
1925
|
+
setTemplateDesc((previousDescription) => {
|
|
1926
|
+
const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
1927
|
+
previousDescription || '',
|
|
1928
|
+
semanticOrNumericVarName,
|
|
1929
|
+
selectedTagNameFromPicker,
|
|
1930
|
+
);
|
|
1931
|
+
if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
|
|
1932
|
+
return previousDescription;
|
|
1933
|
+
}
|
|
1934
|
+
setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
|
|
1935
|
+
setRcsVarSegmentEditorRemountKey((k) => k + 1);
|
|
1936
|
+
return descriptionAfterReplacingNumericPlaceholder;
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
|
|
1943
|
+
|
|
1944
|
+
const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
|
|
1945
|
+
|
|
1946
|
+
const onCarouselTagSelect = (data) => {
|
|
1947
|
+
if (!carouselFocusedVarId) return;
|
|
1948
|
+
const sep = carouselFocusedVarId.lastIndexOf('_');
|
|
696
1949
|
if (sep === -1) return;
|
|
697
|
-
const
|
|
698
|
-
if (isNaN(numId)) return;
|
|
699
|
-
const token = areaId.slice(0, sep);
|
|
1950
|
+
const token = carouselFocusedVarId.slice(0, sep);
|
|
700
1951
|
const variableName = getVarNameFromToken(token);
|
|
701
1952
|
if (!variableName) return;
|
|
702
1953
|
setCardVarMapped((prev) => {
|
|
@@ -709,10 +1960,6 @@ export const Rcs = (props) => {
|
|
|
709
1960
|
});
|
|
710
1961
|
};
|
|
711
1962
|
|
|
712
|
-
const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
|
|
713
|
-
|
|
714
|
-
const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
|
|
715
|
-
|
|
716
1963
|
const onTagSelectFallback = (data) => {
|
|
717
1964
|
const tempMsg = `${fallbackMessage}{{${data}}}`;
|
|
718
1965
|
const error = fallbackMessageErrorHandler(tempMsg);
|
|
@@ -732,15 +1979,14 @@ export const Rcs = (props) => {
|
|
|
732
1979
|
};
|
|
733
1980
|
// tag Code end
|
|
734
1981
|
|
|
735
|
-
const renderLabel = (value,
|
|
736
|
-
const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
|
|
1982
|
+
const renderLabel = (value, desc) => {
|
|
737
1983
|
return (
|
|
738
1984
|
<>
|
|
739
|
-
<
|
|
1985
|
+
<div className="rcs-form-section-heading">
|
|
740
1986
|
<CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
|
|
741
|
-
</
|
|
1987
|
+
</div>
|
|
742
1988
|
{desc && (
|
|
743
|
-
<CapLabel type="label3"
|
|
1989
|
+
<CapLabel type="label3" className="rcs-form-field-caption">
|
|
744
1990
|
{formatMessage(messages[desc])}
|
|
745
1991
|
</CapLabel>
|
|
746
1992
|
)}
|
|
@@ -757,15 +2003,14 @@ export const Rcs = (props) => {
|
|
|
757
2003
|
value: contentType.rich_card,
|
|
758
2004
|
label: formatMessage(messages.richCard),
|
|
759
2005
|
},
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
},
|
|
2006
|
+
...(!isHostInfoBip
|
|
2007
|
+
? [
|
|
2008
|
+
{
|
|
2009
|
+
value: contentType.carousel,
|
|
2010
|
+
label: formatMessage(messages.carousel),
|
|
2011
|
+
},
|
|
2012
|
+
]
|
|
2013
|
+
: []),
|
|
769
2014
|
];
|
|
770
2015
|
|
|
771
2016
|
const onTemplateNameChange = ({ target: { value } }) => {
|
|
@@ -776,6 +2021,10 @@ export const Rcs = (props) => {
|
|
|
776
2021
|
|
|
777
2022
|
const onTemplateTypeChange = ({ target: { value } }) => {
|
|
778
2023
|
setTemplateType(value);
|
|
2024
|
+
// Carousel has per-card media; keep template-level media type neutral.
|
|
2025
|
+
if (value === contentType.carousel) {
|
|
2026
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
2027
|
+
}
|
|
779
2028
|
};
|
|
780
2029
|
|
|
781
2030
|
|
|
@@ -796,8 +2045,39 @@ export const Rcs = (props) => {
|
|
|
796
2045
|
const onTemplateMediaTypeChange = ({ target: { value } }) => {
|
|
797
2046
|
setTemplateMediaType(value);
|
|
798
2047
|
};
|
|
799
|
-
|
|
800
|
-
|
|
2048
|
+
const renderedRCSEditMessage = (descArray, type) => {
|
|
2049
|
+
const renderArray = [];
|
|
2050
|
+
if (descArray?.length) {
|
|
2051
|
+
descArray.forEach((elem, index) => {
|
|
2052
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
2053
|
+
// Variable input
|
|
2054
|
+
renderArray.push(
|
|
2055
|
+
<TextArea
|
|
2056
|
+
id={`${elem}_${index}`}
|
|
2057
|
+
key={`${elem}_${index}`}
|
|
2058
|
+
placeholder={`enter the value for ${elem}`}
|
|
2059
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2060
|
+
onChange={e => textAreaValueChange(e, type)}
|
|
2061
|
+
value={textAreaValue(index, type)}
|
|
2062
|
+
onFocus={(e) => setTextAreaId(e, type)}
|
|
2063
|
+
/>
|
|
2064
|
+
);
|
|
2065
|
+
} else if (elem) {
|
|
2066
|
+
// Static text
|
|
2067
|
+
renderArray.push(
|
|
2068
|
+
<TextArea
|
|
2069
|
+
key={`static_${index}`}
|
|
2070
|
+
value={elem}
|
|
2071
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2072
|
+
disabled
|
|
2073
|
+
className="rcs-edit-template-message-static-textarea"
|
|
2074
|
+
/>
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
return renderArray;
|
|
2080
|
+
};
|
|
801
2081
|
const onTemplateTitleChange = ({ target: { value } }) => {
|
|
802
2082
|
let errorMessage = false;
|
|
803
2083
|
if (templateType === contentType.rich_card && !value.trim()) {
|
|
@@ -813,7 +2093,7 @@ export const Rcs = (props) => {
|
|
|
813
2093
|
|
|
814
2094
|
const onTemplateDescChange = ({ target: { value } }) => {
|
|
815
2095
|
let errorMessage = false;
|
|
816
|
-
if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
|
|
2096
|
+
if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
|
|
817
2097
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
818
2098
|
} else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
|
|
819
2099
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
@@ -829,16 +2109,16 @@ export const Rcs = (props) => {
|
|
|
829
2109
|
|
|
830
2110
|
const templateDescErrorHandler = (value) => {
|
|
831
2111
|
let errorMessage = false;
|
|
832
|
-
const { isBraceError } = validateTags({
|
|
2112
|
+
const { unsupportedTags, isBraceError } = validateTags({
|
|
833
2113
|
content: value,
|
|
834
2114
|
tagsParam: tags,
|
|
2115
|
+
injectedTagsParams: injectedTags,
|
|
835
2116
|
location,
|
|
836
2117
|
tagModule: getDefaultTags,
|
|
837
|
-
isFullMode,
|
|
838
2118
|
}) || {};
|
|
839
2119
|
|
|
840
2120
|
const maxLength = templateType === contentType.text_message
|
|
841
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH
|
|
2121
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
|
|
842
2122
|
: RCS_RICH_CARD_MAX_LENGTH;
|
|
843
2123
|
|
|
844
2124
|
if (value === '' && isMediaTypeText) {
|
|
@@ -861,10 +2141,25 @@ export const Rcs = (props) => {
|
|
|
861
2141
|
};
|
|
862
2142
|
|
|
863
2143
|
const fallbackMessageErrorHandler = (value) => {
|
|
2144
|
+
let errorMessage = false;
|
|
2145
|
+
const { unsupportedTags } = validateTags({
|
|
2146
|
+
content: value,
|
|
2147
|
+
tagsParam: tags,
|
|
2148
|
+
injectedTagsParams: injectedTags,
|
|
2149
|
+
location,
|
|
2150
|
+
tagModule: getDefaultTags,
|
|
2151
|
+
}) || {};
|
|
864
2152
|
if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
|
|
865
|
-
|
|
2153
|
+
errorMessage = formatMessage(messages.fallbackMsgLenError);
|
|
2154
|
+
} else if (unsupportedTags?.length > 0) {
|
|
2155
|
+
errorMessage = formatMessage(
|
|
2156
|
+
globalMessages.unsupportedTagsValidationError,
|
|
2157
|
+
{
|
|
2158
|
+
unsupportedTags,
|
|
2159
|
+
},
|
|
2160
|
+
);
|
|
866
2161
|
}
|
|
867
|
-
return
|
|
2162
|
+
return errorMessage;
|
|
868
2163
|
};
|
|
869
2164
|
|
|
870
2165
|
// Check for forbidden characters: square brackets [] and single curly braces {}
|
|
@@ -911,53 +2206,43 @@ export const Rcs = (props) => {
|
|
|
911
2206
|
if(!isFullMode){
|
|
912
2207
|
return false;
|
|
913
2208
|
}
|
|
914
|
-
|
|
2209
|
+
// Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
|
|
2210
|
+
if (!/^[\w.]+$/.test(paramName)) {
|
|
915
2211
|
return formatMessage(messages.unknownCharactersError);
|
|
916
2212
|
}
|
|
917
2213
|
}
|
|
918
2214
|
return false;
|
|
919
2215
|
};
|
|
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
2216
|
|
|
948
2217
|
const onMessageAddVar = () => {
|
|
949
|
-
onAddVar(
|
|
2218
|
+
onAddVar(templateDesc);
|
|
950
2219
|
};
|
|
951
2220
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
2221
|
+
/**
|
|
2222
|
+
* Returns the smallest positive integer not already used as a `{{N}}` variable
|
|
2223
|
+
* in either the title or description fields, or null if the limit (19) is reached.
|
|
2224
|
+
* Scans both fields so title and description vars never share the same number
|
|
2225
|
+
* (duplicate numbers would share a cardVarMapped key and bleed values across fields).
|
|
2226
|
+
*/
|
|
2227
|
+
const getNextRcsNumericVarNumber = (titleStr, descStr) => {
|
|
2228
|
+
const allExistingVars = [
|
|
2229
|
+
...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
|
|
2230
|
+
...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
|
|
2231
|
+
];
|
|
2232
|
+
const existingNumbers = allExistingVars.flatMap(v => {
|
|
2233
|
+
const m = v.match(/\d+/);
|
|
2234
|
+
return m ? [parseInt(m[0], 10)] : [];
|
|
2235
|
+
});
|
|
956
2236
|
let nextNumber = 1;
|
|
957
2237
|
while (existingNumbers.includes(nextNumber)) {
|
|
958
2238
|
nextNumber++;
|
|
959
2239
|
}
|
|
960
|
-
|
|
2240
|
+
return nextNumber > 19 ? null : nextNumber;
|
|
2241
|
+
};
|
|
2242
|
+
|
|
2243
|
+
const onAddVar = (messageContent) => {
|
|
2244
|
+
const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
|
|
2245
|
+
if (nextNumber === null) {
|
|
961
2246
|
return;
|
|
962
2247
|
}
|
|
963
2248
|
const nextVar = `{{${nextNumber}}}`;
|
|
@@ -969,15 +2254,13 @@ const onAddVar = (type, messageContent, regex) => {
|
|
|
969
2254
|
};
|
|
970
2255
|
|
|
971
2256
|
const onTitleAddVar = () => {
|
|
972
|
-
//
|
|
2257
|
+
// Scan both title AND description so the new title var number doesn't
|
|
2258
|
+
// duplicate a number already used in the description. Duplicate numeric
|
|
2259
|
+
// names would share the same cardVarMapped semantic key, causing the
|
|
2260
|
+
// description slot to reflect the title slot value and vice-versa.
|
|
973
2261
|
const messageContent = templateTitle;
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
let nextNumber = 1;
|
|
977
|
-
while (existingNumbers.includes(nextNumber)) {
|
|
978
|
-
nextNumber++;
|
|
979
|
-
}
|
|
980
|
-
if (nextNumber > 19) {
|
|
2262
|
+
const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
|
|
2263
|
+
if (nextNumber === null) {
|
|
981
2264
|
return;
|
|
982
2265
|
}
|
|
983
2266
|
const nextVar = `{{${nextNumber}}}`;
|
|
@@ -989,26 +2272,35 @@ const onTitleAddVar = () => {
|
|
|
989
2272
|
setTemplateTitleError(error);
|
|
990
2273
|
};
|
|
991
2274
|
|
|
992
|
-
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
2275
|
+
// Carousel: global variables across the whole carousel (all cards, title+body)
|
|
2276
|
+
const getNextCarouselVarToken = () => {
|
|
2277
|
+
const nums = [];
|
|
2278
|
+
(carouselData || []).forEach((c = {}) => {
|
|
2279
|
+
const s1 = (c.title || '').match(/\{\{(\d+)\}\}/g) || [];
|
|
2280
|
+
const s2 = (c.description || '').match(/\{\{(\d+)\}\}/g) || [];
|
|
2281
|
+
[...s1, ...s2].forEach((tok) => {
|
|
2282
|
+
const n = parseInt((tok.match(/\d+/) || [])[0], 10);
|
|
2283
|
+
if (!Number.isNaN(n)) nums.push(n);
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
const existing = new Set(nums);
|
|
2287
|
+
let nextNumber = 1;
|
|
2288
|
+
while (existing.has(nextNumber)) nextNumber++;
|
|
2289
|
+
if (nextNumber > 19) return '';
|
|
2290
|
+
return `{{${nextNumber}}}`;
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
const appendVarToCarouselField = (cardIndex, fieldName) => {
|
|
2294
|
+
const token = getNextCarouselVarToken();
|
|
2295
|
+
if (!token) return;
|
|
2296
|
+
setCarouselData((prev = []) => {
|
|
2297
|
+
const updated = cloneDeep(prev);
|
|
2298
|
+
if (!updated[cardIndex]) return prev;
|
|
2299
|
+
const current = (updated[cardIndex][fieldName] || '').toString();
|
|
2300
|
+
updated[cardIndex][fieldName] = `${current}${token}`;
|
|
2301
|
+
return updated;
|
|
2302
|
+
});
|
|
2303
|
+
};
|
|
1012
2304
|
|
|
1013
2305
|
const textAreaValue = (idValue, type) => {
|
|
1014
2306
|
if (idValue >= 0) {
|
|
@@ -1024,6 +2316,46 @@ const splitTemplateVarString = (str) => {
|
|
|
1024
2316
|
return "";
|
|
1025
2317
|
};
|
|
1026
2318
|
|
|
2319
|
+
// Carousel: render variable-value editor for a given template string (title/description).
|
|
2320
|
+
// This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
|
|
2321
|
+
const renderCarouselEditMessage = (templateStr) => {
|
|
2322
|
+
const renderArray = [];
|
|
2323
|
+
const templateArr = splitTemplateVarString(templateStr);
|
|
2324
|
+
if (templateArr?.length) {
|
|
2325
|
+
templateArr.forEach((elem, index) => {
|
|
2326
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
2327
|
+
const varName = getVarNameFromToken(elem);
|
|
2328
|
+
renderArray.push(
|
|
2329
|
+
<div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
|
|
2330
|
+
<TextArea
|
|
2331
|
+
id={`${elem}_${index}`}
|
|
2332
|
+
placeholder={`enter the value for ${elem}`}
|
|
2333
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
2334
|
+
onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
|
|
2335
|
+
value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
|
|
2336
|
+
onFocus={(e) => {
|
|
2337
|
+
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
2338
|
+
setCarouselFocusedVarId(id);
|
|
2339
|
+
}}
|
|
2340
|
+
/>
|
|
2341
|
+
</div>
|
|
2342
|
+
);
|
|
2343
|
+
} else if (elem) {
|
|
2344
|
+
renderArray.push(
|
|
2345
|
+
<CapHeading
|
|
2346
|
+
key={`static_${index}`}
|
|
2347
|
+
type="h4"
|
|
2348
|
+
className="rcs-edit-template-message-split"
|
|
2349
|
+
>
|
|
2350
|
+
{elem}
|
|
2351
|
+
</CapHeading>
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
2357
|
+
};
|
|
2358
|
+
|
|
1027
2359
|
const textAreaValueChange = (e, type) => {
|
|
1028
2360
|
const value = e?.target?.value ?? '';
|
|
1029
2361
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
@@ -1043,7 +2375,9 @@ const splitTemplateVarString = (str) => {
|
|
|
1043
2375
|
};
|
|
1044
2376
|
|
|
1045
2377
|
const setTextAreaId = (e, type) => {
|
|
1046
|
-
|
|
2378
|
+
// VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
|
|
2379
|
+
// have an `.target.id` shape. Support both.
|
|
2380
|
+
const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
|
|
1047
2381
|
if (!id) return;
|
|
1048
2382
|
if (type === TITLE_TEXT) setTitleTextAreaId(id);
|
|
1049
2383
|
else setDescTextAreaId(id);
|
|
@@ -1074,44 +2408,71 @@ const splitTemplateVarString = (str) => {
|
|
|
1074
2408
|
isEditFlow={isEditFlow}
|
|
1075
2409
|
isFullMode={isFullMode}
|
|
1076
2410
|
maxButtons={MAX_BUTTONS}
|
|
1077
|
-
|
|
2411
|
+
host={hostName}
|
|
2412
|
+
/>
|
|
1078
2413
|
</>
|
|
1079
2414
|
);
|
|
1080
2415
|
};
|
|
1081
2416
|
|
|
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
|
-
|
|
2417
|
+
const getRcsValueMap = (fieldTemplateString, fieldType) => {
|
|
2418
|
+
if (!fieldTemplateString) return {};
|
|
2419
|
+
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
2420
|
+
const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
|
|
2421
|
+
const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
|
|
2422
|
+
const segmentIdToResolvedValue = {};
|
|
2423
|
+
let varOrdinal = 0;
|
|
2424
|
+
templateSegments.forEach((segmentToken, segmentIndexInField) => {
|
|
2425
|
+
if (rcsVarTestRegex.test(segmentToken)) {
|
|
2426
|
+
const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
|
|
2427
|
+
const varName = getVarNameFromToken(segmentToken);
|
|
2428
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
2429
|
+
varOrdinal += 1;
|
|
2430
|
+
segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
|
|
2431
|
+
cardVarMapped,
|
|
2432
|
+
varName,
|
|
2433
|
+
globalSlot,
|
|
2434
|
+
isEditLike,
|
|
2435
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
2436
|
+
);
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
return segmentIdToResolvedValue;
|
|
2440
|
+
};
|
|
2441
|
+
|
|
2442
|
+
const titleVarSegmentValueMapById = useMemo(
|
|
2443
|
+
() => getRcsValueMap(templateTitle, TITLE_TEXT),
|
|
2444
|
+
[templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
2445
|
+
);
|
|
2446
|
+
const descriptionVarSegmentValueMapById = useMemo(
|
|
2447
|
+
() => getRcsValueMap(templateDesc, MESSAGE_TEXT),
|
|
2448
|
+
[templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
2449
|
+
);
|
|
2450
|
+
|
|
2451
|
+
const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
|
|
2452
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
2453
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
2454
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
2455
|
+
const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
2456
|
+
if (variableName === undefined || variableName === null || variableName === '') return;
|
|
2457
|
+
const isInvalidValue = value?.trim() === '';
|
|
2458
|
+
const coercedSlotValue = isInvalidValue ? '' : value;
|
|
2459
|
+
const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
2460
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
2461
|
+
varSegmentCompositeDomId,
|
|
2462
|
+
templateStringForField,
|
|
2463
|
+
type,
|
|
2464
|
+
);
|
|
2465
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
2466
|
+
const updatedCardVarMapped = { ...previousCardVarMapped };
|
|
2467
|
+
// Remove stale semantic key: keeping it causes every other slot sharing the same
|
|
2468
|
+
// variable name (e.g. {{adv}} in both title and description) to read the same value
|
|
2469
|
+
// via the semantic-key fallback in resolveCardVarMappedSlotValue.
|
|
2470
|
+
delete updatedCardVarMapped[variableName];
|
|
2471
|
+
if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
|
|
2472
|
+
updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
|
|
2473
|
+
}
|
|
2474
|
+
return updatedCardVarMapped;
|
|
2475
|
+
});
|
|
1115
2476
|
};
|
|
1116
2477
|
|
|
1117
2478
|
const renderTextComponent = () => {
|
|
@@ -1131,6 +2492,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1131
2492
|
}
|
|
1132
2493
|
suffix={
|
|
1133
2494
|
<>
|
|
2495
|
+
|
|
1134
2496
|
{(isEditFlow || !isFullMode) ? (
|
|
1135
2497
|
<TagList
|
|
1136
2498
|
label={formatMessage(globalMessages.addLabels)}
|
|
@@ -1147,18 +2509,28 @@ const splitTemplateVarString = (str) => {
|
|
|
1147
2509
|
type="flat"
|
|
1148
2510
|
isAddBtn
|
|
1149
2511
|
onClick={onTitleAddVar}
|
|
1150
|
-
disabled={
|
|
2512
|
+
disabled={templateTitleError}
|
|
1151
2513
|
>
|
|
1152
2514
|
{formatMessage(messages.addVar)}
|
|
1153
2515
|
</CapButton>
|
|
1154
|
-
|
|
2516
|
+
)}
|
|
1155
2517
|
</>
|
|
1156
|
-
}
|
|
2518
|
+
}
|
|
2519
|
+
/>
|
|
2520
|
+
|
|
2521
|
+
{(isEditFlow || !isFullMode) ? (
|
|
2522
|
+
<VarSegmentMessageEditor
|
|
2523
|
+
key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
2524
|
+
templateString={templateTitle}
|
|
2525
|
+
valueMap={titleVarSegmentValueMapById}
|
|
2526
|
+
onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
|
|
2527
|
+
onFocus={(id) => setTitleTextAreaId(id)}
|
|
2528
|
+
varRegex={rcsVarRegex}
|
|
2529
|
+
placeholderPrefix=""
|
|
2530
|
+
getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
|
|
1157
2531
|
/>
|
|
1158
|
-
<div className="rcs_text_area_wrapper">
|
|
1159
|
-
{(isEditFlow || !isFullMode) ? (
|
|
1160
|
-
renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
|
|
1161
2532
|
) : (
|
|
2533
|
+
<div className="rcs_text_area_wrapper">
|
|
1162
2534
|
<CapInput
|
|
1163
2535
|
className={`rcs-template-title-input ${
|
|
1164
2536
|
!isTemplateApproved ? "rcs-edit-disabled" : ""
|
|
@@ -1171,8 +2543,8 @@ const splitTemplateVarString = (str) => {
|
|
|
1171
2543
|
errorMessage={templateTitleError}
|
|
1172
2544
|
disabled={isEditFlow || !isFullMode}
|
|
1173
2545
|
/>
|
|
2546
|
+
</div>
|
|
1174
2547
|
)}
|
|
1175
|
-
</div>
|
|
1176
2548
|
{(isEditFlow || !isFullMode) && templateTitleError && (
|
|
1177
2549
|
<CapError className="rcs-template-title-error">
|
|
1178
2550
|
{templateTitleError}
|
|
@@ -1180,7 +2552,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1180
2552
|
)}
|
|
1181
2553
|
{!isEditFlow && isFullMode && renderTitleCharacterCount()}
|
|
1182
2554
|
</>
|
|
1183
|
-
|
|
2555
|
+
)}
|
|
1184
2556
|
|
|
1185
2557
|
{/* Template Message */}
|
|
1186
2558
|
<CapRow id="rcs-template-message-label">
|
|
@@ -1218,9 +2590,21 @@ const splitTemplateVarString = (str) => {
|
|
|
1218
2590
|
/>
|
|
1219
2591
|
</CapRow>
|
|
1220
2592
|
<CapRow className="rcs-create-template-message-input">
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
2593
|
+
{/* Edit/library: segmented inputs (split on {{…}}). Full-mode create: single TextArea below — manual entry there never hits segment split. TagList replaces {{n}} in template string here. */}
|
|
2594
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
2595
|
+
{(isEditFlow || !isFullMode)?
|
|
2596
|
+
(
|
|
2597
|
+
<VarSegmentMessageEditor
|
|
2598
|
+
key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
2599
|
+
templateString={templateDesc}
|
|
2600
|
+
valueMap={descriptionVarSegmentValueMapById}
|
|
2601
|
+
onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
|
|
2602
|
+
onFocus={(id) => setDescTextAreaId(id)}
|
|
2603
|
+
varRegex={rcsVarRegex}
|
|
2604
|
+
placeholderPrefix=""
|
|
2605
|
+
getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
|
|
2606
|
+
/>
|
|
2607
|
+
)
|
|
1224
2608
|
: (
|
|
1225
2609
|
<>
|
|
1226
2610
|
<TextArea
|
|
@@ -1258,13 +2642,15 @@ const splitTemplateVarString = (str) => {
|
|
|
1258
2642
|
</>
|
|
1259
2643
|
)
|
|
1260
2644
|
}
|
|
1261
|
-
</
|
|
2645
|
+
</CapRow>
|
|
1262
2646
|
{(isEditFlow || !isFullMode) && templateDescError && (
|
|
1263
2647
|
<CapError className="rcs-template-message-error">
|
|
1264
2648
|
{templateDescError}
|
|
1265
2649
|
</CapError>
|
|
1266
2650
|
)}
|
|
1267
|
-
{
|
|
2651
|
+
{(isEditFlow || !isFullMode)
|
|
2652
|
+
? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
|
|
2653
|
+
: (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
|
|
1268
2654
|
{!isFullMode && hasTag() && (
|
|
1269
2655
|
<CapAlert
|
|
1270
2656
|
message={
|
|
@@ -1278,24 +2664,13 @@ const splitTemplateVarString = (str) => {
|
|
|
1278
2664
|
/>
|
|
1279
2665
|
)}
|
|
1280
2666
|
</CapRow>
|
|
1281
|
-
{
|
|
2667
|
+
{((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
|
|
2668
|
+
renderButtonComponent()}
|
|
1282
2669
|
</>
|
|
1283
2670
|
|
|
1284
2671
|
);
|
|
1285
2672
|
};
|
|
1286
2673
|
|
|
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
2674
|
// Get character count for title (rich card only)
|
|
1300
2675
|
const getTitleCharacterCount = () => {
|
|
1301
2676
|
if (templateType === contentType.text_message) return 0;
|
|
@@ -1310,7 +2685,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1310
2685
|
// Get max length for description based on template type
|
|
1311
2686
|
const getDescriptionMaxLength = () => {
|
|
1312
2687
|
return templateType === contentType.text_message
|
|
1313
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
|
|
2688
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
|
|
1314
2689
|
: RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
|
|
1315
2690
|
};
|
|
1316
2691
|
|
|
@@ -1336,6 +2711,63 @@ const splitTemplateVarString = (str) => {
|
|
|
1336
2711
|
);
|
|
1337
2712
|
};
|
|
1338
2713
|
|
|
2714
|
+
const rcsDltCardDeleteHandler = () => {
|
|
2715
|
+
closeDltContainerHandler();
|
|
2716
|
+
setDltEditData({});
|
|
2717
|
+
setFallbackMessage('');
|
|
2718
|
+
setFallbackMessageError(false);
|
|
2719
|
+
setShowDltCard(false);
|
|
2720
|
+
};
|
|
2721
|
+
|
|
2722
|
+
const dltFallbackListingPreviewhandler = (data) => {
|
|
2723
|
+
const {
|
|
2724
|
+
'updated-sms-editor': updatedSmsEditor = [],
|
|
2725
|
+
'sms-editor': smsEditor = '',
|
|
2726
|
+
} = data.versions.base || {};
|
|
2727
|
+
setFallbackPreviewmode(true);
|
|
2728
|
+
setDltPreviewData(
|
|
2729
|
+
updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
|
|
2730
|
+
);
|
|
2731
|
+
};
|
|
2732
|
+
|
|
2733
|
+
const getDltContentCardList = (content, channel) => {
|
|
2734
|
+
const extra = [
|
|
2735
|
+
<CapIcon
|
|
2736
|
+
type="edit"
|
|
2737
|
+
style={{ marginRight: '8px' }}
|
|
2738
|
+
onClick={() => rcsDltEditSelectHandler(dltEditData)}
|
|
2739
|
+
/>,
|
|
2740
|
+
<CapDropdown
|
|
2741
|
+
overlay={(
|
|
2742
|
+
<CapMenu>
|
|
2743
|
+
<>
|
|
2744
|
+
<CapMenu.Item
|
|
2745
|
+
className="ant-dropdown-menu-item"
|
|
2746
|
+
onClick={() => setFallbackPreviewmode(true)}
|
|
2747
|
+
>
|
|
2748
|
+
{formatMessage(globalMessages.preview)}
|
|
2749
|
+
</CapMenu.Item>
|
|
2750
|
+
<CapMenu.Item
|
|
2751
|
+
className="ant-dropdown-menu-item"
|
|
2752
|
+
onClick={rcsDltCardDeleteHandler}
|
|
2753
|
+
>
|
|
2754
|
+
{formatMessage(globalMessages.delete)}
|
|
2755
|
+
</CapMenu.Item>
|
|
2756
|
+
</>
|
|
2757
|
+
</CapMenu>
|
|
2758
|
+
)}
|
|
2759
|
+
>
|
|
2760
|
+
<CapIcon type="more" />
|
|
2761
|
+
</CapDropdown>,
|
|
2762
|
+
];
|
|
2763
|
+
return {
|
|
2764
|
+
title: channel,
|
|
2765
|
+
content,
|
|
2766
|
+
cardType: channel,
|
|
2767
|
+
extra,
|
|
2768
|
+
};
|
|
2769
|
+
};
|
|
2770
|
+
|
|
1339
2771
|
// Render character count for description/message
|
|
1340
2772
|
const renderDescriptionCharacterCount = (className = "rcs-character-count") => {
|
|
1341
2773
|
const currentLength = getDescriptionCharacterCount();
|
|
@@ -1351,6 +2783,26 @@ const splitTemplateVarString = (str) => {
|
|
|
1351
2783
|
);
|
|
1352
2784
|
};
|
|
1353
2785
|
|
|
2786
|
+
// Carousel: per-card character counts (same limits as rich card)
|
|
2787
|
+
const getCarouselTitleCharacterCount = (cardIndex) => {
|
|
2788
|
+
const t = carouselData?.[cardIndex]?.title || '';
|
|
2789
|
+
return t ? t.length : 0;
|
|
2790
|
+
};
|
|
2791
|
+
|
|
2792
|
+
const getCarouselDescriptionCharacterCount = (cardIndex) => {
|
|
2793
|
+
const d = carouselData?.[cardIndex]?.description || '';
|
|
2794
|
+
return d ? d.length : 0;
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
const renderCarouselCharacterCount = (currentLength, maxLength, className = "rcs-character-count") => (
|
|
2798
|
+
<CapLabel type="label1" className={className}>
|
|
2799
|
+
{formatMessage(messages.templateMessageLength, {
|
|
2800
|
+
currentLength,
|
|
2801
|
+
maxLength,
|
|
2802
|
+
})}
|
|
2803
|
+
</CapLabel>
|
|
2804
|
+
);
|
|
2805
|
+
|
|
1354
2806
|
// Check if any RCS variables contain tags (similar to Zalo hasTag logic)
|
|
1355
2807
|
const hasTag = () => {
|
|
1356
2808
|
// Check cardVarMapped values for tags
|
|
@@ -1403,68 +2855,17 @@ const splitTemplateVarString = (str) => {
|
|
|
1403
2855
|
const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
|
|
1404
2856
|
'',
|
|
1405
2857
|
);
|
|
2858
|
+
const templateNameFromDlt = get(dltEditData, 'name', '')
|
|
2859
|
+
|| get(tempData, 'versions.base.name', '')
|
|
2860
|
+
|| '';
|
|
1406
2861
|
closeDltContainerHandler();
|
|
1407
2862
|
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
|
-
};
|
|
2863
|
+
const unicodeFromDlt = get(tempData, 'versions.base.unicode-validity');
|
|
2864
|
+
setSmsFallbackData({
|
|
2865
|
+
templateName: templateNameFromDlt,
|
|
2866
|
+
content: fallMsg,
|
|
2867
|
+
...(typeof unicodeFromDlt === 'boolean' ? { unicodeValidity: unicodeFromDlt } : {}),
|
|
2868
|
+
});
|
|
1468
2869
|
};
|
|
1469
2870
|
|
|
1470
2871
|
const getDltSlideBoxContent = () => {
|
|
@@ -1512,148 +2913,34 @@ const splitTemplateVarString = (str) => {
|
|
|
1512
2913
|
return { dltHeader, dltContent };
|
|
1513
2914
|
};
|
|
1514
2915
|
|
|
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
|
-
};
|
|
2916
|
+
const renderFallBackSmsComponent = () => (
|
|
2917
|
+
<SmsFallback
|
|
2918
|
+
value={smsFallbackData}
|
|
2919
|
+
onChange={setSmsFallbackData}
|
|
2920
|
+
parentLocation={location}
|
|
2921
|
+
smsRegister={smsRegister}
|
|
2922
|
+
isFullMode={isFullMode}
|
|
2923
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
2924
|
+
channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
|
|
2925
|
+
sectionTitle={
|
|
2926
|
+
smsFallbackData
|
|
2927
|
+
? formatMessage(messages.fallbackLabel)
|
|
2928
|
+
: formatMessage(messages.smsFallbackOptional)
|
|
2929
|
+
}
|
|
2930
|
+
templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
|
|
2931
|
+
templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
|
|
2932
|
+
/* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
|
|
2933
|
+
showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
|
|
2934
|
+
disableSelectTemplate={isEditFlow}
|
|
2935
|
+
eventContextTags={eventContextTags}
|
|
2936
|
+
onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
|
|
2937
|
+
isRcsEditFlow={isEditFlow}
|
|
2938
|
+
/>
|
|
2939
|
+
);
|
|
1653
2940
|
|
|
1654
2941
|
const uploadRcsImage = useCallback((file, type, fileParams, index) => {
|
|
1655
2942
|
setImageError(null);
|
|
1656
|
-
const isRcsThumbnail = index
|
|
2943
|
+
const isRcsThumbnail = isThumbnailAssetIndex(index);
|
|
1657
2944
|
actions.uploadRcsAsset(file, type, {
|
|
1658
2945
|
isRcsThumbnail,
|
|
1659
2946
|
...fileParams,
|
|
@@ -1691,10 +2978,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1691
2978
|
const updateOnRcsImageReUpload = useCallback(() => {
|
|
1692
2979
|
setUpdateRcsImageSrc('');
|
|
1693
2980
|
}, []);
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
2981
|
const uploadRcsVideo = (file, type, fileParams) => {
|
|
1697
|
-
setImageError(null);
|
|
1698
2982
|
actions.uploadRcsAsset(file, type, {
|
|
1699
2983
|
...fileParams,
|
|
1700
2984
|
type: 'video',
|
|
@@ -1703,9 +2987,6 @@ const splitTemplateVarString = (str) => {
|
|
|
1703
2987
|
});
|
|
1704
2988
|
};
|
|
1705
2989
|
|
|
1706
|
-
const updateRcsVideoSrc = (val) => {
|
|
1707
|
-
setRcsVideoSrc(val);
|
|
1708
|
-
};
|
|
1709
2990
|
const setUpdateRcsVideoSrc = useCallback((index, val) => {
|
|
1710
2991
|
setRcsVideoSrc(val);
|
|
1711
2992
|
setAssetList(val);
|
|
@@ -1728,7 +3009,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1728
3009
|
updateRcsThumbnailSrc('');
|
|
1729
3010
|
};
|
|
1730
3011
|
|
|
1731
|
-
|
|
3012
|
+
const renderThumbnailComponent = () => {
|
|
1732
3013
|
const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
1733
3014
|
return !isEditFlow && (
|
|
1734
3015
|
<>
|
|
@@ -1752,6 +3033,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1752
3033
|
channel={RCS}
|
|
1753
3034
|
channelSpecificStyle={!isFullMode}
|
|
1754
3035
|
skipDimensionValidation={true}
|
|
3036
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
1755
3037
|
/>
|
|
1756
3038
|
</>
|
|
1757
3039
|
)
|
|
@@ -1777,7 +3059,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1777
3059
|
value: dim.type,
|
|
1778
3060
|
label: `${dim.label}`
|
|
1779
3061
|
}))}
|
|
1780
|
-
|
|
3062
|
+
className="rcs-dimension-select--bottom-spacing"
|
|
1781
3063
|
/>
|
|
1782
3064
|
</>
|
|
1783
3065
|
)}
|
|
@@ -1791,7 +3073,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1791
3073
|
</div>
|
|
1792
3074
|
) : (
|
|
1793
3075
|
<CapImageUpload
|
|
1794
|
-
|
|
3076
|
+
style={{ paddingTop: '20px' }}
|
|
1795
3077
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1796
3078
|
imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
|
|
1797
3079
|
imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
|
|
@@ -1802,7 +3084,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1802
3084
|
updateImageSrc={setUpdateRcsImageSrc}
|
|
1803
3085
|
updateOnReUpload={updateOnRcsImageReUpload}
|
|
1804
3086
|
index={0}
|
|
1805
|
-
className="cap-custom-image-upload"
|
|
3087
|
+
className="cap-custom-image-upload rcs-image-upload--top-spacing"
|
|
1806
3088
|
key={`rcs-uploaded-image-${selectedDimension}`}
|
|
1807
3089
|
imageData={rcsData}
|
|
1808
3090
|
channel={RCS}
|
|
@@ -1813,7 +3095,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1813
3095
|
|
|
1814
3096
|
</>
|
|
1815
3097
|
);
|
|
1816
|
-
|
|
3098
|
+
}
|
|
1817
3099
|
|
|
1818
3100
|
const renderVideoComponent = () => {
|
|
1819
3101
|
const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
@@ -1832,7 +3114,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1832
3114
|
value: dim.type,
|
|
1833
3115
|
label: `${dim.label}`
|
|
1834
3116
|
}))}
|
|
1835
|
-
|
|
3117
|
+
className="rcs-dimension-select--bottom-spacing"
|
|
1836
3118
|
/>
|
|
1837
3119
|
)}
|
|
1838
3120
|
{(isEditFlow || !isFullMode) ? (
|
|
@@ -1890,10 +3172,48 @@ const splitTemplateVarString = (str) => {
|
|
|
1890
3172
|
};
|
|
1891
3173
|
|
|
1892
3174
|
const getRcsPreview = () => {
|
|
3175
|
+
|
|
3176
|
+
if (templateType === contentType.carousel) {
|
|
3177
|
+
const cardsForPreview = buildCarouselCardsForPreview(carouselData);
|
|
3178
|
+
const carouselDimKey = getCarouselDimensionKey();
|
|
3179
|
+
const carouselImgDims =
|
|
3180
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
3181
|
+
const carouselVidDims =
|
|
3182
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
3183
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
3184
|
+
// Debug log for embedded/library mode preview payload (carousel)
|
|
3185
|
+
// eslint-disable-next-line no-console
|
|
3186
|
+
return (
|
|
3187
|
+
<UnifiedPreview
|
|
3188
|
+
channel={RCS}
|
|
3189
|
+
content={{
|
|
3190
|
+
carouselData: cardsForPreview,
|
|
3191
|
+
carouselPreviewDimensions: {
|
|
3192
|
+
imageWidth: carouselImgDims.width,
|
|
3193
|
+
imageHeight: carouselImgDims.height,
|
|
3194
|
+
videoThumbWidth: carouselVidDims.width,
|
|
3195
|
+
videoThumbHeight: carouselVidDims.height,
|
|
3196
|
+
},
|
|
3197
|
+
}}
|
|
3198
|
+
device={ANDROID}
|
|
3199
|
+
showDeviceToggle={false}
|
|
3200
|
+
showHeader={false}
|
|
3201
|
+
formatMessage={formatMessage}
|
|
3202
|
+
/>
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
1893
3205
|
|
|
1894
3206
|
const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
|
|
1895
|
-
const
|
|
1896
|
-
const
|
|
3207
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
3208
|
+
const titleVarCountForResolve = isMediaTypeText
|
|
3209
|
+
? 0
|
|
3210
|
+
: ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
|
|
3211
|
+
const resolvedTitle = isMediaTypeText
|
|
3212
|
+
? ''
|
|
3213
|
+
: (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
|
|
3214
|
+
const resolvedDesc = isSlotMappingMode
|
|
3215
|
+
? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
|
|
3216
|
+
: templateDesc;
|
|
1897
3217
|
return (
|
|
1898
3218
|
<UnifiedPreview
|
|
1899
3219
|
channel={RCS}
|
|
@@ -1916,51 +3236,65 @@ const splitTemplateVarString = (str) => {
|
|
|
1916
3236
|
);
|
|
1917
3237
|
};
|
|
1918
3238
|
|
|
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
3239
|
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;
|
|
3240
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
1960
3241
|
const alignment = isMediaTypeImage
|
|
1961
3242
|
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
|
|
1962
3243
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
|
|
1963
3244
|
|
|
3245
|
+
const heightTypeForCardWidth = isMediaTypeText
|
|
3246
|
+
? undefined
|
|
3247
|
+
: isMediaTypeImage
|
|
3248
|
+
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
|
|
3249
|
+
: isMediaTypeVideo
|
|
3250
|
+
? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
|
|
3251
|
+
: undefined;
|
|
3252
|
+
const cardWidthFromSelection =
|
|
3253
|
+
heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
|
|
3254
|
+
|
|
3255
|
+
/** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
|
|
3256
|
+
const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
|
|
3257
|
+
const smsFallbackMerged = !isFullMode
|
|
3258
|
+
? (() => {
|
|
3259
|
+
const local =
|
|
3260
|
+
smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
|
|
3261
|
+
return {
|
|
3262
|
+
...smsFromApiShape,
|
|
3263
|
+
...local,
|
|
3264
|
+
rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
|
|
3265
|
+
};
|
|
3266
|
+
})()
|
|
3267
|
+
: (smsFallbackData || {});
|
|
3268
|
+
const smsFallbackForPayload = (() => {
|
|
3269
|
+
if (isFullMode) {
|
|
3270
|
+
return hasMeaningfulSmsFallbackShape(smsFallbackData) ? smsFallbackData : null;
|
|
3271
|
+
}
|
|
3272
|
+
const mapped = {
|
|
3273
|
+
templateName:
|
|
3274
|
+
smsFallbackMerged.templateName
|
|
3275
|
+
|| smsFallbackMerged.smsTemplateName
|
|
3276
|
+
|| '',
|
|
3277
|
+
// Use `||` so empty `content` does not block campaign/API `message` (common in embedded flows).
|
|
3278
|
+
content:
|
|
3279
|
+
smsFallbackMerged.content
|
|
3280
|
+
|| smsFallbackMerged.smsContent
|
|
3281
|
+
|| smsFallbackMerged.smsTemplateContent
|
|
3282
|
+
|| smsFallbackMerged.message
|
|
3283
|
+
|| '',
|
|
3284
|
+
templateContent:
|
|
3285
|
+
pickFirstSmsFallbackTemplateString(smsFallbackMerged)
|
|
3286
|
+
|| '',
|
|
3287
|
+
...(typeof smsFallbackMerged.unicodeValidity === 'boolean'
|
|
3288
|
+
&& { unicodeValidity: smsFallbackMerged.unicodeValidity }),
|
|
3289
|
+
...(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
|
|
3290
|
+
&& Object.keys(smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]).length > 0 && {
|
|
3291
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
|
|
3292
|
+
smsFallbackMerged[RCS_SMS_FALLBACK_VAR_MAPPED_PROP],
|
|
3293
|
+
}),
|
|
3294
|
+
};
|
|
3295
|
+
return hasMeaningfulSmsFallbackShape(mapped) ? mapped : null;
|
|
3296
|
+
})();
|
|
3297
|
+
|
|
1964
3298
|
const payload = {
|
|
1965
3299
|
name: templateName,
|
|
1966
3300
|
versions: {
|
|
@@ -1968,17 +3302,18 @@ const splitTemplateVarString = (str) => {
|
|
|
1968
3302
|
content: {
|
|
1969
3303
|
RCS: {
|
|
1970
3304
|
rcsContent: {
|
|
1971
|
-
...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
|
|
1972
3305
|
cardType: STANDALONE,
|
|
1973
3306
|
cardSettings: {
|
|
1974
3307
|
cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
|
|
1975
3308
|
...(alignment && { mediaAlignment: alignment }),
|
|
1976
|
-
cardWidth:
|
|
3309
|
+
cardWidth: cardWidthFromSelection,
|
|
1977
3310
|
},
|
|
1978
3311
|
cardContent: [
|
|
1979
3312
|
{
|
|
1980
|
-
|
|
1981
|
-
|
|
3313
|
+
// Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
|
|
3314
|
+
// / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
|
|
3315
|
+
title: templateTitle,
|
|
3316
|
+
description: templateDesc,
|
|
1982
3317
|
mediaType: templateMediaType,
|
|
1983
3318
|
...(!isMediaTypeText && {media: {
|
|
1984
3319
|
mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
|
|
@@ -1987,23 +3322,32 @@ const splitTemplateVarString = (str) => {
|
|
|
1987
3322
|
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
|
|
1988
3323
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
|
|
1989
3324
|
}}),
|
|
1990
|
-
...(
|
|
1991
|
-
const
|
|
1992
|
-
...(templateTitle
|
|
1993
|
-
...(templateDesc
|
|
3325
|
+
...(isSlotMappingMode && (() => {
|
|
3326
|
+
const templateVarTokens = [
|
|
3327
|
+
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
3328
|
+
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
1994
3329
|
];
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
3330
|
+
const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
|
|
3331
|
+
cardVarMapped,
|
|
3332
|
+
);
|
|
3333
|
+
// Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
|
|
3334
|
+
// semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
|
|
3335
|
+
// and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
|
|
3336
|
+
const persistedSlotVarMap = {};
|
|
3337
|
+
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
3338
|
+
const varName = getVarNameFromToken(token);
|
|
3339
|
+
if (!varName) return;
|
|
3340
|
+
const resolvedRawValue = resolveCardVarMappedSlotValue(
|
|
3341
|
+
cardVarMappedForRcsCardOnly,
|
|
3342
|
+
varName,
|
|
3343
|
+
slotIndexZeroBased,
|
|
3344
|
+
isSlotMappingMode,
|
|
3345
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
3346
|
+
);
|
|
3347
|
+
const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
|
|
3348
|
+
persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
|
|
2005
3349
|
});
|
|
2006
|
-
return { cardVarMapped:
|
|
3350
|
+
return { cardVarMapped: persistedSlotVarMap };
|
|
2007
3351
|
})()),
|
|
2008
3352
|
...(suggestions.length > 0 && { suggestions }),
|
|
2009
3353
|
}
|
|
@@ -2011,17 +3355,88 @@ const splitTemplateVarString = (str) => {
|
|
|
2011
3355
|
contentType: isFullMode ? templateType : RICHCARD,
|
|
2012
3356
|
...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
|
|
2013
3357
|
},
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
3358
|
+
...(smsFallbackForPayload && (() => {
|
|
3359
|
+
const smsBodyText =
|
|
3360
|
+
smsFallbackForPayload.content
|
|
3361
|
+
|| smsFallbackForPayload.templateContent
|
|
3362
|
+
|| smsFallbackForPayload.message
|
|
3363
|
+
|| smsFallbackForPayload.smsContent
|
|
3364
|
+
|| '';
|
|
3365
|
+
/**
|
|
3366
|
+
* Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
|
|
3367
|
+
* Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
|
|
3368
|
+
*/
|
|
3369
|
+
const m = smsFallbackMerged || {};
|
|
3370
|
+
const tcSibling = m.templateConfigs && typeof m.templateConfigs === 'object'
|
|
3371
|
+
? m.templateConfigs
|
|
3372
|
+
: {};
|
|
3373
|
+
const smsFallbackTemplateId =
|
|
3374
|
+
(m.smsTemplateId != null && String(m.smsTemplateId).trim() !== ''
|
|
3375
|
+
? String(m.smsTemplateId)
|
|
3376
|
+
: '')
|
|
3377
|
+
|| (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
|
|
3378
|
+
? String(tcSibling.templateId)
|
|
3379
|
+
: '');
|
|
3380
|
+
const smsFallbackTemplateStr =
|
|
3381
|
+
pickFirstSmsFallbackTemplateString(m)
|
|
3382
|
+
|| (typeof m.templateContent === 'string' ? m.templateContent : '')
|
|
3383
|
+
|| (typeof tcSibling.template === 'string' ? tcSibling.template : '')
|
|
3384
|
+
|| '';
|
|
3385
|
+
const smsFallbackTemplateName =
|
|
3386
|
+
m.templateName
|
|
3387
|
+
|| m.smsTemplateName
|
|
3388
|
+
|| tcSibling.templateName
|
|
3389
|
+
|| tcSibling.name
|
|
3390
|
+
|| '';
|
|
3391
|
+
const registeredSenderIdsForPayload = Array.isArray(m.registeredSenderIds)
|
|
3392
|
+
? m.registeredSenderIds
|
|
3393
|
+
: Array.isArray(tcSibling.registeredSenderIds)
|
|
3394
|
+
? tcSibling.registeredSenderIds
|
|
3395
|
+
: Array.isArray(tcSibling.header)
|
|
3396
|
+
? tcSibling.header
|
|
3397
|
+
: null;
|
|
3398
|
+
const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
|
|
3399
|
+
const smsFallbackTemplateConfigs =
|
|
3400
|
+
smsFallbackTemplateId || hasRegisteredSenderIds
|
|
3401
|
+
? {
|
|
3402
|
+
...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
|
|
3403
|
+
...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
|
|
3404
|
+
...(smsFallbackTemplateName && {
|
|
3405
|
+
templateName: smsFallbackTemplateName,
|
|
3406
|
+
}),
|
|
3407
|
+
...(hasRegisteredSenderIds && {
|
|
3408
|
+
registeredSenderIds: registeredSenderIdsForPayload,
|
|
3409
|
+
}),
|
|
3410
|
+
}
|
|
3411
|
+
: null;
|
|
3412
|
+
const isDltCampaign = !isFullMode && isTraiDLTEnable(isFullMode, smsRegister);
|
|
3413
|
+
return {
|
|
3414
|
+
smsFallBackContent: isFullMode
|
|
3415
|
+
? {
|
|
3416
|
+
smsTemplateName: smsFallbackForPayload.templateName || '',
|
|
3417
|
+
smsContent: smsBodyText,
|
|
3418
|
+
// cap-campaigns-v2 `normalizeRcsMessageContentForApi` only serializes `message` (+ templateConfigs); without this key SMS fallback is dropped on send.
|
|
3419
|
+
message: smsBodyText,
|
|
3420
|
+
...(typeof smsFallbackForPayload.unicodeValidity === 'boolean' && {
|
|
3421
|
+
unicodeValidity: smsFallbackForPayload.unicodeValidity,
|
|
3422
|
+
}),
|
|
3423
|
+
...(smsFallbackForPayload.rcsSmsFallbackVarMapped
|
|
3424
|
+
&& Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
|
|
3425
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
|
|
3426
|
+
}),
|
|
3427
|
+
...(smsFallbackTemplateConfigs && {
|
|
3428
|
+
templateConfigs: smsFallbackTemplateConfigs,
|
|
3429
|
+
}),
|
|
3430
|
+
}
|
|
3431
|
+
: {
|
|
3432
|
+
// campaign payload: non-DLT → message only; DLT → message + templateConfigs
|
|
3433
|
+
message: smsBodyText,
|
|
3434
|
+
...(isDltCampaign && smsFallbackTemplateConfigs && {
|
|
3435
|
+
templateConfigs: smsFallbackTemplateConfigs,
|
|
3436
|
+
}),
|
|
3437
|
+
},
|
|
3438
|
+
};
|
|
3439
|
+
})()),
|
|
2025
3440
|
},
|
|
2026
3441
|
},
|
|
2027
3442
|
},
|
|
@@ -2031,6 +3446,109 @@ const splitTemplateVarString = (str) => {
|
|
|
2031
3446
|
return payload;
|
|
2032
3447
|
};
|
|
2033
3448
|
|
|
3449
|
+
/** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
|
|
3450
|
+
const testPreviewFormData = useMemo(() => {
|
|
3451
|
+
const payload = createPayload();
|
|
3452
|
+
const rcs = payload?.versions?.base?.content?.RCS;
|
|
3453
|
+
if (!rcs) return null;
|
|
3454
|
+
// createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
|
|
3455
|
+
const accountIdForCreateMessageMeta =
|
|
3456
|
+
(wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
|
|
3457
|
+
? String(wecrmAccountId)
|
|
3458
|
+
: accountId;
|
|
3459
|
+
const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
|
|
3460
|
+
let rcsForTest = {
|
|
3461
|
+
...rcs,
|
|
3462
|
+
rcsContent: {
|
|
3463
|
+
...rcs.rcsContent,
|
|
3464
|
+
...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
|
|
3465
|
+
},
|
|
3466
|
+
};
|
|
3467
|
+
/** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
|
|
3468
|
+
if (isSlotMappingModeForPreview) {
|
|
3469
|
+
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
3470
|
+
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
3471
|
+
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
3472
|
+
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
3473
|
+
templateTitle,
|
|
3474
|
+
templateDesc,
|
|
3475
|
+
rcsVarRegex,
|
|
3476
|
+
);
|
|
3477
|
+
rcsForTest = {
|
|
3478
|
+
...rcsForTest,
|
|
3479
|
+
rcsContent: {
|
|
3480
|
+
...rcsForTest.rcsContent,
|
|
3481
|
+
cardContent: [
|
|
3482
|
+
{ ...cardContent[0], cardVarMapped: fullCardVarMapped },
|
|
3483
|
+
...cardContent.slice(1),
|
|
3484
|
+
],
|
|
3485
|
+
},
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
const out = {
|
|
3490
|
+
versions: {
|
|
3491
|
+
base: {
|
|
3492
|
+
content: {
|
|
3493
|
+
RCS: rcsForTest,
|
|
3494
|
+
},
|
|
3495
|
+
},
|
|
3496
|
+
},
|
|
3497
|
+
};
|
|
3498
|
+
const fb = smsFallbackData;
|
|
3499
|
+
if (fb && (fb.smsTemplateId || fb.templateContent || fb.content)) {
|
|
3500
|
+
out.templateConfigs = {
|
|
3501
|
+
templateId: fb.smsTemplateId || '',
|
|
3502
|
+
template: fb.templateContent || fb.content || '',
|
|
3503
|
+
traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
|
|
3504
|
+
registeredSenderIds: Array.isArray(fb.registeredSenderIds) ? fb.registeredSenderIds : [],
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
return out;
|
|
3508
|
+
}, [
|
|
3509
|
+
templateName,
|
|
3510
|
+
templateTitle,
|
|
3511
|
+
templateDesc,
|
|
3512
|
+
templateMediaType,
|
|
3513
|
+
cardVarMapped,
|
|
3514
|
+
suggestions,
|
|
3515
|
+
rcsImageSrc,
|
|
3516
|
+
rcsVideoSrc,
|
|
3517
|
+
rcsThumbnailSrc,
|
|
3518
|
+
selectedDimension,
|
|
3519
|
+
smsFallbackData,
|
|
3520
|
+
isFullMode,
|
|
3521
|
+
isEditFlow,
|
|
3522
|
+
templateType,
|
|
3523
|
+
accountId,
|
|
3524
|
+
wecrmAccountId,
|
|
3525
|
+
accessToken,
|
|
3526
|
+
accountName,
|
|
3527
|
+
hostName,
|
|
3528
|
+
smsRegister,
|
|
3529
|
+
]);
|
|
3530
|
+
|
|
3531
|
+
/**
|
|
3532
|
+
* Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
|
|
3533
|
+
* with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
|
|
3534
|
+
* miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
|
|
3535
|
+
*/
|
|
3536
|
+
const librarySmsFallbackMergedForValidation = useMemo(() => {
|
|
3537
|
+
if (isFullMode) {
|
|
3538
|
+
return smsFallbackData;
|
|
3539
|
+
}
|
|
3540
|
+
const smsFromApiShape = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
|
|
3541
|
+
const local =
|
|
3542
|
+
smsFallbackData && typeof smsFallbackData === 'object' ? smsFallbackData : {};
|
|
3543
|
+
return {
|
|
3544
|
+
...smsFromApiShape,
|
|
3545
|
+
...local,
|
|
3546
|
+
rcsSmsFallbackVarMapped: mergeRcsSmsFallbackVarMapLayers(smsFromApiShape, local),
|
|
3547
|
+
};
|
|
3548
|
+
}, [isFullMode, templateData, smsFallbackData]);
|
|
3549
|
+
|
|
3550
|
+
|
|
3551
|
+
|
|
2034
3552
|
const actionCallback = ({ errorMessage, resp }, isEdit) => {
|
|
2035
3553
|
// eslint-disable-next-line no-undef
|
|
2036
3554
|
const error = errorMessage?.message || errorMessage;
|
|
@@ -2060,6 +3578,9 @@ const splitTemplateVarString = (str) => {
|
|
|
2060
3578
|
_id: params?.id,
|
|
2061
3579
|
validity: true,
|
|
2062
3580
|
type: RCS,
|
|
3581
|
+
// CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
|
|
3582
|
+
// the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
|
|
3583
|
+
closeSlideBoxAfterSubmit: !isFullMode,
|
|
2063
3584
|
};
|
|
2064
3585
|
getFormData(formDataParams);
|
|
2065
3586
|
};
|
|
@@ -2073,6 +3594,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2073
3594
|
actionCallback({ resp, errorMessage });
|
|
2074
3595
|
setSpin(false); // Always turn off spinner
|
|
2075
3596
|
if (!errorMessage) {
|
|
3597
|
+
setTemplateStatus(RCS_STATUSES.pending);
|
|
2076
3598
|
onCreateComplete();
|
|
2077
3599
|
}
|
|
2078
3600
|
});
|
|
@@ -2084,6 +3606,86 @@ const splitTemplateVarString = (str) => {
|
|
|
2084
3606
|
}
|
|
2085
3607
|
};
|
|
2086
3608
|
|
|
3609
|
+
/** When a fallback SMS row exists, require non-empty body (trimmed) and filled var slots (DLT). */
|
|
3610
|
+
const smsFallbackBlocksDone = () => {
|
|
3611
|
+
// Non-DLT library: user removed SMS fallback (local null) but template still carries fallback — block Done.
|
|
3612
|
+
if (
|
|
3613
|
+
!isFullMode
|
|
3614
|
+
&& !isTraiDLTEnable(isFullMode, smsRegister)
|
|
3615
|
+
&& smsFallbackData == null
|
|
3616
|
+
&& hasMeaningfulSmsFallbackShape(
|
|
3617
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
3618
|
+
)
|
|
3619
|
+
) {
|
|
3620
|
+
return true;
|
|
3621
|
+
}
|
|
3622
|
+
if (!smsFallbackData) return false;
|
|
3623
|
+
// Full-mode (Send for approval): SMS fallback is optional. Tag-slot mapping is a display/preview
|
|
3624
|
+
// concern, not a structural requirement for approval — the registered SMS template body stands on
|
|
3625
|
+
// its own. Never block the Send for approval button due to missing or unfilled fallback var slots.
|
|
3626
|
+
if (isFullMode) return false;
|
|
3627
|
+
const merged = librarySmsFallbackMergedForValidation;
|
|
3628
|
+
const templateText = pickFirstSmsFallbackTemplateString(merged);
|
|
3629
|
+
if (!templateText) {
|
|
3630
|
+
return true;
|
|
3631
|
+
}
|
|
3632
|
+
const rawVarMap =
|
|
3633
|
+
merged.rcsSmsFallbackVarMapped
|
|
3634
|
+
|| merged['rcs-sms-fallback-var-mapped'];
|
|
3635
|
+
const varMap =
|
|
3636
|
+
rawVarMap != null && typeof rawVarMap === 'object' ? rawVarMap : {};
|
|
3637
|
+
return !areAllRcsSmsFallbackVarSlotsFilled(templateText, varMap);
|
|
3638
|
+
};
|
|
3639
|
+
|
|
3640
|
+
/**
|
|
3641
|
+
* Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
|
|
3642
|
+
* semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
|
|
3643
|
+
* / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
|
|
3644
|
+
*/
|
|
3645
|
+
const isLibraryCampaignCardVarMappingIncomplete = () => {
|
|
3646
|
+
if (isFullMode) return false;
|
|
3647
|
+
const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
|
|
3648
|
+
rcsVarTestRegex.test(elem),
|
|
3649
|
+
);
|
|
3650
|
+
const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
|
|
3651
|
+
rcsVarTestRegex.test(elem),
|
|
3652
|
+
);
|
|
3653
|
+
const orderedVarNames = [
|
|
3654
|
+
...titleTokens.map((t) => t.replace(/^\{\{|\}\}$/g, '')),
|
|
3655
|
+
...descTokens.map((t) => t.replace(/^\{\{|\}\}$/g, '')),
|
|
3656
|
+
];
|
|
3657
|
+
if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
|
|
3658
|
+
return true;
|
|
3659
|
+
}
|
|
3660
|
+
return orderedVarNames.some((name, globalIdx) => {
|
|
3661
|
+
const v = resolveCardVarMappedSlotValue(
|
|
3662
|
+
cardVarMapped,
|
|
3663
|
+
name,
|
|
3664
|
+
globalIdx,
|
|
3665
|
+
true,
|
|
3666
|
+
rcsSpanningSemanticVarNames.has(name),
|
|
3667
|
+
);
|
|
3668
|
+
const s = v == null ? '' : String(v);
|
|
3669
|
+
return s.trim() === '';
|
|
3670
|
+
});
|
|
3671
|
+
};
|
|
3672
|
+
|
|
3673
|
+
const isCarouselLibraryIncomplete = () => {
|
|
3674
|
+
if (!isCarouselType || isFullMode) return false;
|
|
3675
|
+
if ((carouselErrors || []).some((err) => err?.title || err?.description)) return true;
|
|
3676
|
+
const unfilledVar = (carouselData || []).some((card) =>
|
|
3677
|
+
['title', 'description'].some((field) => {
|
|
3678
|
+
const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((t) => rcsVarTestRegex.test(t));
|
|
3679
|
+
return tokens.some((t) => {
|
|
3680
|
+
const name = t.replace(/^\{\{|\}\}$/g, '');
|
|
3681
|
+
const v = cardVarMapped?.[name];
|
|
3682
|
+
return v == null || String(v).trim() === '';
|
|
3683
|
+
});
|
|
3684
|
+
})
|
|
3685
|
+
);
|
|
3686
|
+
return unfilledVar;
|
|
3687
|
+
};
|
|
3688
|
+
|
|
2087
3689
|
const isDisableDone = () => {
|
|
2088
3690
|
if(isEditFlow){
|
|
2089
3691
|
return false;
|
|
@@ -2094,46 +3696,26 @@ const splitTemplateVarString = (str) => {
|
|
|
2094
3696
|
}
|
|
2095
3697
|
}
|
|
2096
3698
|
|
|
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
|
-
}
|
|
3699
|
+
if (isCarouselLibraryIncomplete()) {
|
|
3700
|
+
return true;
|
|
3701
|
+
}
|
|
2117
3702
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
if (typeof v !== 'string') return !v;
|
|
2121
|
-
return v.trim() === '';
|
|
2122
|
-
});
|
|
2123
|
-
if (anyMissing) {
|
|
2124
|
-
return true;
|
|
2125
|
-
}
|
|
3703
|
+
if (isLibraryCampaignCardVarMappingIncomplete()) {
|
|
3704
|
+
return true;
|
|
2126
3705
|
}
|
|
2127
3706
|
|
|
2128
|
-
|
|
3707
|
+
if (smsFallbackBlocksDone()) {
|
|
2129
3708
|
return true;
|
|
3709
|
+
}
|
|
2130
3710
|
|
|
3711
|
+
if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
|
|
3712
|
+
return true;
|
|
2131
3713
|
}
|
|
2132
|
-
if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
3714
|
+
if (!isCarouselType && isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2133
3715
|
return true;
|
|
2134
3716
|
}
|
|
2135
3717
|
|
|
2136
|
-
if (isMediaTypeVideo && (rcsVideoSrc.videoSrc
|
|
3718
|
+
if (!isCarouselType && isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2137
3719
|
return true;
|
|
2138
3720
|
}
|
|
2139
3721
|
if (buttonType.includes(CTA)) {
|
|
@@ -2145,53 +3727,36 @@ const splitTemplateVarString = (str) => {
|
|
|
2145
3727
|
return true;
|
|
2146
3728
|
}
|
|
2147
3729
|
}
|
|
2148
|
-
if (templateDescError || templateTitleError
|
|
3730
|
+
if (templateDescError || templateTitleError) {
|
|
3731
|
+
return true;
|
|
3732
|
+
}
|
|
3733
|
+
if (
|
|
3734
|
+
smsFallbackData?.content
|
|
3735
|
+
&& smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
|
|
3736
|
+
) {
|
|
2149
3737
|
return true;
|
|
2150
3738
|
}
|
|
2151
3739
|
return false;
|
|
2152
3740
|
};
|
|
2153
3741
|
|
|
2154
3742
|
const isEditDisableDone = () => {
|
|
2155
|
-
|
|
2156
|
-
if (templateStatus !== RCS_STATUSES.approved) {
|
|
3743
|
+
if (isFullMode && !isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
|
|
2157
3744
|
return true;
|
|
2158
3745
|
}
|
|
2159
3746
|
|
|
2160
|
-
if (!isFullMode) {
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
3747
|
+
// if (!isFullMode) {
|
|
3748
|
+
// if (templateName.trim() === '' || templateNameError) {
|
|
3749
|
+
// return true;
|
|
3750
|
+
// }
|
|
3751
|
+
// }
|
|
3752
|
+
if (isLibraryCampaignCardVarMappingIncomplete()) {
|
|
3753
|
+
return true;
|
|
2164
3754
|
}
|
|
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
|
-
|
|
2174
|
-
const hasEmptyMapping =
|
|
2175
|
-
cardVarMapped &&
|
|
2176
|
-
Object.keys(cardVarMapped).length > 0 &&
|
|
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
|
-
}
|
|
2185
3755
|
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
if (typeof v !== 'string') return !v;
|
|
2189
|
-
return v.trim() === '';
|
|
2190
|
-
});
|
|
2191
|
-
if (anyMissing) {
|
|
2192
|
-
return true;
|
|
2193
|
-
}
|
|
3756
|
+
if (smsFallbackBlocksDone()) {
|
|
3757
|
+
return true;
|
|
2194
3758
|
}
|
|
3759
|
+
|
|
2195
3760
|
if (isMediaTypeText && templateDesc.trim() === '') {
|
|
2196
3761
|
return true;
|
|
2197
3762
|
}
|
|
@@ -2210,7 +3775,13 @@ const splitTemplateVarString = (str) => {
|
|
|
2210
3775
|
return true;
|
|
2211
3776
|
}
|
|
2212
3777
|
}
|
|
2213
|
-
if (templateTitleError || templateDescError
|
|
3778
|
+
if (templateTitleError || templateDescError) {
|
|
3779
|
+
return true;
|
|
3780
|
+
}
|
|
3781
|
+
if (
|
|
3782
|
+
smsFallbackData?.content
|
|
3783
|
+
&& smsFallbackData.content.length > FALLBACK_MESSAGE_MAX_LENGTH
|
|
3784
|
+
) {
|
|
2214
3785
|
return true;
|
|
2215
3786
|
}
|
|
2216
3787
|
return false;
|
|
@@ -2260,54 +3831,58 @@ const splitTemplateVarString = (str) => {
|
|
|
2260
3831
|
};
|
|
2261
3832
|
|
|
2262
3833
|
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
|
-
|
|
3834
|
+
// Slideboxes are rendered outside the page-level spinner to avoid
|
|
3835
|
+
// stacking/blur issues during initial loads.
|
|
3836
|
+
if (showDltContainer) return null;
|
|
2277
3837
|
return (
|
|
2278
3838
|
<>
|
|
2279
|
-
{templateStatus !== '' && (
|
|
2280
|
-
<
|
|
2281
|
-
{
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
3839
|
+
{templateStatus !== '' && (
|
|
3840
|
+
<CapRow className="template-status-container">
|
|
3841
|
+
<CapColumn span={14}>
|
|
3842
|
+
<CapLabel type="label2">
|
|
3843
|
+
{formatMessage(messages.templateStatusLabel)}
|
|
3844
|
+
</CapLabel>
|
|
3845
|
+
|
|
3846
|
+
{!isHostInfoBip && templateStatus && (
|
|
3847
|
+
<CapAlert
|
|
3848
|
+
message={getTemplateStatusMessage()}
|
|
3849
|
+
type={getTemplateStatusType(templateStatus)}
|
|
3850
|
+
/>
|
|
3851
|
+
)}
|
|
3852
|
+
</CapColumn>
|
|
3853
|
+
</CapRow>
|
|
2291
3854
|
)}
|
|
2292
|
-
<CapRow className=
|
|
3855
|
+
<CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
|
|
2293
3856
|
<CapColumn span={14}>
|
|
2294
3857
|
{/* template name */}
|
|
2295
3858
|
{isFullMode && (
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
3859
|
+
isEditFlow ? (
|
|
3860
|
+
<div className="rcs-creative-name-readonly">
|
|
3861
|
+
<CapHeading type="h4">
|
|
3862
|
+
{formatMessage(globalMessages.creativeNameLabel)}
|
|
3863
|
+
</CapHeading>
|
|
3864
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
3865
|
+
{templateName || '-'}
|
|
3866
|
+
</CapHeading>
|
|
3867
|
+
</div>
|
|
3868
|
+
) : (
|
|
3869
|
+
<CapInput
|
|
3870
|
+
id="rcs_template_name_input"
|
|
3871
|
+
data-testid="template_name"
|
|
3872
|
+
onChange={onTemplateNameChange}
|
|
3873
|
+
errorMessage={templateNameError}
|
|
3874
|
+
placeholder={formatMessage(
|
|
3875
|
+
globalMessages.templateNamePlaceholder,
|
|
3876
|
+
)}
|
|
3877
|
+
value={templateName || ''}
|
|
3878
|
+
size="default"
|
|
3879
|
+
label={formatMessage(globalMessages.creativeNameLabel)}
|
|
3880
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
3881
|
+
/>
|
|
3882
|
+
)
|
|
2309
3883
|
)}
|
|
2310
3884
|
{renderLabel('templateTypeLabel')}
|
|
3885
|
+
|
|
2311
3886
|
<CapRadioGroup
|
|
2312
3887
|
id="select-rcs-template-type"
|
|
2313
3888
|
options={TEMPLATE_TYPE_OPTIONS}
|
|
@@ -2316,24 +3891,30 @@ const splitTemplateVarString = (str) => {
|
|
|
2316
3891
|
disabled={(isEditFlow || !isFullMode)}
|
|
2317
3892
|
/>
|
|
2318
3893
|
|
|
2319
|
-
{
|
|
2320
|
-
|
|
3894
|
+
{templateType === contentType.carousel ? (
|
|
3895
|
+
renderCarouselSection()
|
|
3896
|
+
) : (
|
|
2321
3897
|
<>
|
|
2322
|
-
{
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
3898
|
+
{/* Show media only for rich_card */}
|
|
3899
|
+
{templateType === contentType.rich_card && (
|
|
3900
|
+
<>
|
|
3901
|
+
{renderLabel('mediaLabel')}
|
|
3902
|
+
<CapRadioGroup
|
|
3903
|
+
options={mediaRadioOptions || []}
|
|
3904
|
+
value={templateMediaType}
|
|
3905
|
+
onChange={onTemplateMediaTypeChange}
|
|
3906
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
3907
|
+
className="rcs-radio"
|
|
3908
|
+
/>
|
|
3909
|
+
<div className="rcs-container-image">
|
|
3910
|
+
{getMediaBasedComponent()}
|
|
3911
|
+
</div>
|
|
3912
|
+
</>
|
|
3913
|
+
)}
|
|
3914
|
+
{renderTextComponent()}
|
|
2333
3915
|
</>
|
|
2334
3916
|
)}
|
|
2335
|
-
|
|
2336
|
-
<CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
|
|
3917
|
+
<CapDivider className="rcs-fallback-section-divider" />
|
|
2337
3918
|
{renderFallBackSmsComponent()}
|
|
2338
3919
|
<div className="rcs-scroll-div" />
|
|
2339
3920
|
</CapColumn>
|
|
@@ -2345,7 +3926,8 @@ const splitTemplateVarString = (str) => {
|
|
|
2345
3926
|
|
|
2346
3927
|
|
|
2347
3928
|
<div className="rcs-footer">
|
|
2348
|
-
{!
|
|
3929
|
+
{/* 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). */}
|
|
3930
|
+
{!isEditFlow && isFullMode && (
|
|
2349
3931
|
<>
|
|
2350
3932
|
<div className="button-disabled-tooltip-wrapper">
|
|
2351
3933
|
<CapButton
|
|
@@ -2353,7 +3935,9 @@ const splitTemplateVarString = (str) => {
|
|
|
2353
3935
|
disabled={isDisableDone()}
|
|
2354
3936
|
className="rcs-done-btn"
|
|
2355
3937
|
>
|
|
2356
|
-
<FormattedMessage
|
|
3938
|
+
<FormattedMessage
|
|
3939
|
+
{...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
|
|
3940
|
+
/>
|
|
2357
3941
|
</CapButton>
|
|
2358
3942
|
</div>
|
|
2359
3943
|
<CapTooltip
|
|
@@ -2366,7 +3950,6 @@ const splitTemplateVarString = (str) => {
|
|
|
2366
3950
|
className="rcs-test-preview-btn"
|
|
2367
3951
|
type="secondary"
|
|
2368
3952
|
disabled={true}
|
|
2369
|
-
style={{ marginLeft: "8px" }}
|
|
2370
3953
|
>
|
|
2371
3954
|
<FormattedMessage {...creativesMessages.testAndPreview} />
|
|
2372
3955
|
</CapButton>
|
|
@@ -2387,7 +3970,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2387
3970
|
</div>
|
|
2388
3971
|
</>
|
|
2389
3972
|
)}
|
|
2390
|
-
{isEditFlow && templateStatus === RCS_STATUSES.approved && (
|
|
3973
|
+
{isEditFlow && (isHostInfoBip || templateStatus === RCS_STATUSES.approved) && (
|
|
2391
3974
|
<>
|
|
2392
3975
|
<CapButton
|
|
2393
3976
|
onClick={handleTestAndPreview}
|
|
@@ -2399,51 +3982,6 @@ const splitTemplateVarString = (str) => {
|
|
|
2399
3982
|
</>
|
|
2400
3983
|
)}
|
|
2401
3984
|
</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
3985
|
</>
|
|
2448
3986
|
);
|
|
2449
3987
|
};
|
|
@@ -2452,23 +3990,57 @@ const splitTemplateVarString = (str) => {
|
|
|
2452
3990
|
<CapSpin spinning={loadingTags || spin}>
|
|
2453
3991
|
{getMainContent()}
|
|
2454
3992
|
</CapSpin>
|
|
3993
|
+
|
|
3994
|
+
{showDltContainer && (() => {
|
|
3995
|
+
const { dltHeader = '', dltContent = '' } = getDltSlideBoxContent() || {};
|
|
3996
|
+
return (
|
|
3997
|
+
<CapSlideBox
|
|
3998
|
+
show={showDltContainer}
|
|
3999
|
+
header={dltHeader}
|
|
4000
|
+
content={dltContent}
|
|
4001
|
+
handleClose={closeDltContainerHandler}
|
|
4002
|
+
size="size-xl"
|
|
4003
|
+
/>
|
|
4004
|
+
);
|
|
4005
|
+
})()}
|
|
4006
|
+
|
|
2455
4007
|
<TestAndPreviewSlidebox
|
|
2456
4008
|
show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
|
|
2457
4009
|
onClose={handleCloseTestAndPreview}
|
|
2458
|
-
formData={
|
|
2459
|
-
content={
|
|
4010
|
+
formData={testPreviewFormData}
|
|
4011
|
+
content={testAndPreviewContent}
|
|
2460
4012
|
currentChannel={RCS}
|
|
4013
|
+
orgUnitId={orgUnitId}
|
|
4014
|
+
rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
|
|
4015
|
+
smsFallbackContent={
|
|
4016
|
+
smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
|
|
4017
|
+
? {
|
|
4018
|
+
templateContent:
|
|
4019
|
+
smsFallbackData.templateContent || smsFallbackData.content || '',
|
|
4020
|
+
templateName: smsFallbackData.templateName || '',
|
|
4021
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
|
|
4022
|
+
? mergeRcsSmsFallbackVarMapLayers(
|
|
4023
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
4024
|
+
smsFallbackData,
|
|
4025
|
+
)
|
|
4026
|
+
: mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
|
|
4027
|
+
}
|
|
4028
|
+
: null
|
|
4029
|
+
}
|
|
4030
|
+
smsRegister={smsRegister}
|
|
2461
4031
|
/>
|
|
2462
4032
|
</>
|
|
2463
4033
|
);
|
|
2464
4034
|
};
|
|
2465
4035
|
|
|
4036
|
+
|
|
2466
4037
|
const mapStateToProps = createStructuredSelector({
|
|
2467
4038
|
rcsData: makeSelectRcs(),
|
|
2468
4039
|
accountData: makeSelectAccount(),
|
|
2469
4040
|
metaEntities: makeSelectMetaEntities(),
|
|
2470
4041
|
loadingTags: isLoadingMetaEntities(),
|
|
2471
4042
|
injectedTags: setInjectedTags(),
|
|
4043
|
+
currentOrgDetails: selectCurrentOrgDetails(),
|
|
2472
4044
|
});
|
|
2473
4045
|
|
|
2474
4046
|
const mapDispatchToProps = (dispatch) => ({
|