@capillarytech/creatives-library 9.0.26 → 9.0.29-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +29 -0
- package/global-styles.js +1 -1
- package/package.json +1 -1
- package/services/tests/api.test.js +35 -20
- package/utils/commonUtils.js +22 -1
- package/utils/rcsPayloadUtils.js +102 -0
- package/utils/templateVarUtils.js +198 -0
- package/utils/tests/rcsPayloadUtils.test.js +295 -0
- package/utils/tests/templateVarUtils.test.js +204 -0
- package/v2Components/CapActionButton/constants.js +21 -1
- package/v2Components/CapActionButton/index.js +260 -144
- package/v2Components/CapActionButton/index.scss +245 -14
- package/v2Components/CapActionButton/messages.js +32 -3
- package/v2Components/CapActionButton/tests/index.test.js +74 -19
- package/v2Components/CapImageUpload/index.js +2 -2
- package/v2Components/CapTagList/index.js +9 -0
- package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
- package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
- package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
- package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
- package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
- package/v2Components/CommonTestAndPreview/constants.js +35 -2
- package/v2Components/CommonTestAndPreview/index.js +770 -231
- package/v2Components/CommonTestAndPreview/messages.js +45 -3
- package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
- package/v2Components/CommonTestAndPreview/sagas.js +25 -6
- package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
- 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 +108 -15
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
- package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
- package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
- package/v2Components/CommonTestAndPreview/utils.js +84 -0
- package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
- package/v2Components/SmsFallback/constants.js +94 -0
- package/v2Components/SmsFallback/index.js +958 -0
- package/v2Components/SmsFallback/index.scss +266 -0
- package/v2Components/SmsFallback/messages.js +78 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
- package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
- package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
- package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
- package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
- package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
- package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +147 -32
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
- package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
- package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
- package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
- package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
- package/v2Components/VarSegmentMessageEditor/index.js +125 -0
- package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
- package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
- package/v2Containers/CreativesContainer/constants.js +11 -0
- package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
- package/v2Containers/CreativesContainer/index.js +328 -106
- package/v2Containers/CreativesContainer/index.scss +102 -1
- package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
- package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
- package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
- package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
- package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
- package/v2Containers/Email/index.js +3 -54
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
- package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
- package/v2Containers/Rcs/carouselUtils.js +224 -0
- package/v2Containers/Rcs/components/CarouselCard.js +317 -0
- package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
- package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
- package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
- package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
- package/v2Containers/Rcs/constants.js +132 -16
- package/v2Containers/Rcs/index.js +1808 -949
- package/v2Containers/Rcs/index.scss +443 -8
- package/v2Containers/Rcs/messages.js +45 -22
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +484 -0
- package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
- package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
- package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
- package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
- package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
- package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
- package/v2Containers/Rcs/tests/index.test.js +219 -40
- package/v2Containers/Rcs/tests/mockData.js +38 -0
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
- package/v2Containers/Rcs/tests/utils.test.js +682 -30
- package/v2Containers/Rcs/utils.js +514 -12
- package/v2Containers/Sms/Create/index.js +115 -48
- package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
- package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
- package/v2Containers/SmsTrai/Create/index.js +9 -4
- package/v2Containers/SmsTrai/Edit/constants.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +678 -169
- package/v2Containers/SmsTrai/Edit/index.scss +126 -0
- package/v2Containers/SmsTrai/Edit/messages.js +14 -4
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
- package/v2Containers/SmsWrapper/index.js +37 -8
- package/v2Containers/TagList/index.js +5 -0
- package/v2Containers/Templates/TemplatesActionBar.js +101 -0
- package/v2Containers/Templates/_templates.scss +196 -12
- package/v2Containers/Templates/actions.js +11 -0
- package/v2Containers/Templates/constants.js +2 -0
- package/v2Containers/Templates/index.js +131 -59
- package/v2Containers/Templates/sagas.js +57 -13
- package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
- package/v2Containers/Templates/tests/sagas.test.js +199 -16
- package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
- package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
- package/v2Containers/TemplatesV2/index.js +88 -25
- package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
- package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
- package/v2Containers/Whatsapp/index.js +3 -20
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/* eslint-disable no-unused-expressions */
|
|
2
|
-
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
3
3
|
import { bindActionCreators } from 'redux';
|
|
4
4
|
import { createStructuredSelector } from 'reselect';
|
|
5
|
-
import {
|
|
5
|
+
import { FormattedMessage } from 'react-intl';
|
|
6
6
|
import get from 'lodash/get';
|
|
7
7
|
import isEmpty from 'lodash/isEmpty';
|
|
8
8
|
import cloneDeep from 'lodash/cloneDeep';
|
|
9
9
|
import isNil from 'lodash/isNil';
|
|
10
|
-
import styled from 'styled-components';
|
|
11
10
|
import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
|
|
12
11
|
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
13
12
|
import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
|
|
@@ -23,8 +22,6 @@ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
|
23
22
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
24
23
|
import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
25
24
|
import CapImage from '@capillarytech/cap-ui-library/CapImage';
|
|
26
|
-
import CapCard from '@capillarytech/cap-ui-library/CapCard';
|
|
27
|
-
import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
|
|
28
25
|
import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
|
|
29
26
|
import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
|
|
30
27
|
import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
|
|
@@ -35,6 +32,8 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
|
|
|
35
32
|
import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
|
|
36
33
|
import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
|
|
37
34
|
import CapLink from '@capillarytech/cap-ui-library/CapLink';
|
|
35
|
+
import CapTab from '@capillarytech/cap-ui-library/CapTab';
|
|
36
|
+
import { flushSync } from 'react-dom';
|
|
38
37
|
|
|
39
38
|
import {
|
|
40
39
|
CAP_G01,
|
|
@@ -50,17 +49,29 @@ import {
|
|
|
50
49
|
import CapVideoUpload from '../../v2Components/CapVideoUpload';
|
|
51
50
|
import * as globalActions from '../Cap/actions';
|
|
52
51
|
import CapActionButton from '../../v2Components/CapActionButton';
|
|
52
|
+
import TemplatePreview from '../../v2Components/TemplatePreview';
|
|
53
53
|
import { makeSelectRcs, makeSelectAccount } from './selectors';
|
|
54
54
|
import { DATE_DISPLAY_FORMAT, TIME_DISPLAY_FORMAT } from '../App/constants';
|
|
55
55
|
import {
|
|
56
56
|
isLoadingMetaEntities,
|
|
57
57
|
makeSelectMetaEntities,
|
|
58
58
|
setInjectedTags,
|
|
59
|
+
selectCurrentOrgDetails,
|
|
59
60
|
} from '../Cap/selectors';
|
|
60
61
|
import * as RcsActions from './actions';
|
|
61
62
|
import { isAiContentBotDisabled } from '../../utils/common';
|
|
62
|
-
import * as TemplatesActions from '../Templates/actions';
|
|
63
63
|
import './index.scss';
|
|
64
|
+
import {
|
|
65
|
+
normalizeLibraryLoadedTitleDesc,
|
|
66
|
+
mergeRcsSmsFallbackVarMapLayers,
|
|
67
|
+
applySmsFallbackHydration,
|
|
68
|
+
syncCardVarMappedSemanticsFromSlots,
|
|
69
|
+
hasMeaningfulSmsFallbackShape,
|
|
70
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData,
|
|
71
|
+
mergeSmsFallbackForLibrary,
|
|
72
|
+
buildSmsFallBackContentForPayload,
|
|
73
|
+
pickRcsCardVarMappedEntries,
|
|
74
|
+
} from './rcsLibraryHydrationUtils';
|
|
64
75
|
import {
|
|
65
76
|
RCS,
|
|
66
77
|
SMS,
|
|
@@ -75,8 +86,8 @@ import {
|
|
|
75
86
|
LIBRARY,
|
|
76
87
|
ALLOWED_IMAGE_EXTENSIONS_REGEX,
|
|
77
88
|
RCS_IMG_SIZE,
|
|
78
|
-
RCS_DLT_MODE,
|
|
79
89
|
CTA,
|
|
90
|
+
AI_CONTENT_BOT_DISABLED,
|
|
80
91
|
RCS_STATUSES,
|
|
81
92
|
TITLE_TEXT,
|
|
82
93
|
MESSAGE_TEXT,
|
|
@@ -91,9 +102,13 @@ import {
|
|
|
91
102
|
rcsVarTestRegex,
|
|
92
103
|
RCS_IMAGE_DIMENSIONS,
|
|
93
104
|
RCS_TEXT_MESSAGE_MAX_LENGTH,
|
|
105
|
+
RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP,
|
|
94
106
|
RCS_RICH_CARD_MAX_LENGTH,
|
|
95
107
|
RCS_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
108
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS,
|
|
109
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS,
|
|
96
110
|
MAX_BUTTONS,
|
|
111
|
+
INITIAL_SUGGESTIONS,
|
|
97
112
|
INITIAL_SUGGESTIONS_DATA_STOP,
|
|
98
113
|
RCS_BUTTON_TYPES,
|
|
99
114
|
titletype,
|
|
@@ -103,25 +118,66 @@ import {
|
|
|
103
118
|
SMALL,
|
|
104
119
|
MEDIUM,
|
|
105
120
|
RICHCARD,
|
|
121
|
+
HOST_INFOBIP,
|
|
122
|
+
HOST_ICS,
|
|
123
|
+
STOP,
|
|
124
|
+
RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
|
|
125
|
+
MAX_RCS_CAROUSEL_ALLOWED,
|
|
126
|
+
RCS_CAROUSEL_INITIAL_CARD,
|
|
127
|
+
RCS_CAROUSEL_INITIAL_FIRST_CARD,
|
|
128
|
+
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
129
|
+
RCS_NUMERIC_VAR_TOKEN_REGEX,
|
|
130
|
+
RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
|
|
131
|
+
RCS_TAG_AREA_FIELD_TITLE,
|
|
132
|
+
RCS_TAG_AREA_FIELD_DESC,
|
|
133
|
+
REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
|
|
134
|
+
JS_TYPES,
|
|
106
135
|
} from './constants';
|
|
107
136
|
import globalMessages from '../Cap/messages';
|
|
108
137
|
import messages from './messages';
|
|
109
138
|
import creativesMessages from '../CreativesContainer/messages';
|
|
110
139
|
import withCreatives from '../../hoc/withCreatives';
|
|
111
140
|
import UnifiedPreview from '../../v2Components/CommonTestAndPreview/UnifiedPreview';
|
|
112
|
-
import
|
|
141
|
+
import VarSegmentMessageEditor from '../../v2Components/VarSegmentMessageEditor';
|
|
142
|
+
import { ANDROID, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
|
|
113
143
|
import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
|
|
144
|
+
import { splitTemplateVarString } from '../../utils/templateVarUtils';
|
|
114
145
|
import CapImageUpload from '../../v2Components/CapImageUpload';
|
|
115
|
-
import
|
|
116
|
-
import
|
|
117
|
-
import SmsTraiEdit from '../SmsTrai/Edit';
|
|
146
|
+
import SmsFallback from '../../v2Components/SmsFallback';
|
|
147
|
+
import { CHANNELS_TO_HIDE_FOR_SMS_ONLY } from '../../v2Components/SmsFallback/constants';
|
|
118
148
|
import TagList from '../TagList';
|
|
119
149
|
import { validateTags } from '../../utils/tagValidations';
|
|
120
|
-
import {
|
|
150
|
+
import { isTraiDLTEnable } from '../../utils/common';
|
|
121
151
|
import { isTagIncluded } from '../../utils/commonUtils';
|
|
122
152
|
import injectReducer from '../../utils/injectReducer';
|
|
123
153
|
import v2RcsReducer from './reducer';
|
|
124
|
-
import {
|
|
154
|
+
import {
|
|
155
|
+
buildRcsNumericMustachePlaceholderRegex,
|
|
156
|
+
getTemplateStatusType,
|
|
157
|
+
normalizeCardVarMapped,
|
|
158
|
+
coalesceCardVarMappedToTemplate,
|
|
159
|
+
getRcsSemanticVarNamesSpanningTitleAndDesc,
|
|
160
|
+
resolveCardVarMappedSlotValue,
|
|
161
|
+
sanitizeCardVarMappedValue,
|
|
162
|
+
checkSmsFallbackBlocksDone,
|
|
163
|
+
isSmsFallbackContentTooLong,
|
|
164
|
+
} from './utils';
|
|
165
|
+
import {
|
|
166
|
+
getCarouselImageAssetIndex,
|
|
167
|
+
getCarouselVideoAssetIndex,
|
|
168
|
+
getCarouselThumbnailAssetIndex,
|
|
169
|
+
isThumbnailAssetIndex,
|
|
170
|
+
getCarouselDimensionKey,
|
|
171
|
+
ensureFirstCardDefaultPhoneSuggestions,
|
|
172
|
+
checkDisableAddCarouselButton,
|
|
173
|
+
getNextCarouselVarToken,
|
|
174
|
+
getCarouselTitleCharacterCount,
|
|
175
|
+
getCarouselDescriptionCharacterCount,
|
|
176
|
+
buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
|
|
177
|
+
buildCarouselCardContentForPayload,
|
|
178
|
+
} from './carouselUtils';
|
|
179
|
+
import CarouselDimensionSelection from './components/CarouselDimensionSelection';
|
|
180
|
+
import CarouselCard from './components/CarouselCard';
|
|
125
181
|
|
|
126
182
|
|
|
127
183
|
const { Group: CapCheckboxGroup } = CapCheckbox;
|
|
@@ -135,31 +191,48 @@ export const Rcs = (props) => {
|
|
|
135
191
|
templateData = {},
|
|
136
192
|
rcsData = {},
|
|
137
193
|
thumbnailData = {}, // Add separate thumbnail data
|
|
138
|
-
templatesActions,
|
|
139
194
|
globalActions,
|
|
140
195
|
location,
|
|
141
|
-
handleClose,
|
|
142
196
|
getDefaultTags,
|
|
143
197
|
supportedTags,
|
|
144
198
|
metaEntities,
|
|
145
199
|
injectedTags,
|
|
146
200
|
loadingTags,
|
|
147
201
|
getFormData,
|
|
148
|
-
isDltEnabled,
|
|
149
202
|
smsRegister,
|
|
203
|
+
orgUnitId,
|
|
150
204
|
selectedOfferDetails,
|
|
151
205
|
eventContextTags,
|
|
152
|
-
waitEventContextTags,
|
|
153
206
|
accountData = {},
|
|
207
|
+
currentOrgDetails,
|
|
154
208
|
// TestAndPreviewSlidebox props
|
|
155
209
|
showTestAndPreviewSlidebox: propsShowTestAndPreviewSlidebox,
|
|
156
210
|
handleTestAndPreview: propsHandleTestAndPreview,
|
|
157
211
|
handleCloseTestAndPreview: propsHandleCloseTestAndPreview,
|
|
212
|
+
handleClose,
|
|
158
213
|
} = props || {};
|
|
159
214
|
const { formatMessage } = intl;
|
|
160
215
|
const { TextArea } = CapInput;
|
|
161
216
|
const { CapCustomCardList } = CapCustomCard;
|
|
217
|
+
|
|
218
|
+
// Defensive: React cannot render plain objects as children (crashes with
|
|
219
|
+
// "Objects are not valid as a React child"). Some campaigns (!isFullMode) flows
|
|
220
|
+
// can accidentally set an error state to an object (e.g. `{}`).
|
|
221
|
+
const normalizeErrorMessage = (err) => {
|
|
222
|
+
if (!err) return '';
|
|
223
|
+
if (React.isValidElement(err)) return err;
|
|
224
|
+
if (typeof err === JS_TYPES.STRING) return err;
|
|
225
|
+
if (typeof err === JS_TYPES.NUMBER) return String(err);
|
|
226
|
+
if (typeof err === JS_TYPES.OBJECT && typeof err.message === JS_TYPES.STRING) return err.message;
|
|
227
|
+
try {
|
|
228
|
+
return JSON.stringify(err);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return String(err);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
162
234
|
const [isEditFlow, setEditFlow] = useState(false);
|
|
235
|
+
const isEditLike = isEditFlow || !isFullMode;
|
|
163
236
|
const [tags, updateTags] = useState([]);
|
|
164
237
|
const [spin, setSpin] = useState(false);
|
|
165
238
|
//template
|
|
@@ -168,33 +241,17 @@ export const Rcs = (props) => {
|
|
|
168
241
|
const [templateMediaType, setTemplateMediaType] = useState(
|
|
169
242
|
RCS_MEDIA_TYPES.NONE,
|
|
170
243
|
);
|
|
171
|
-
const [templateRejectionReason, setTemplateRejectionReason] = useState(null);
|
|
172
244
|
const [templateTitle, setTemplateTitle] = useState('');
|
|
173
245
|
const [templateDesc, setTemplateDesc] = useState('');
|
|
174
246
|
const [templateDescError, setTemplateDescError] = useState(false);
|
|
175
247
|
const [templateStatus, setTemplateStatus] = useState('');
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const [fallbackMessage, setFallbackMessage] = useState('');
|
|
179
|
-
const [fallbackMessageError, setFallbackMessageError] = useState(false);
|
|
180
|
-
//fallback dlt
|
|
181
|
-
const [showDltContainer, setShowDltContainer] = useState(false);
|
|
182
|
-
const [dltMode, setDltMode] = useState('');
|
|
183
|
-
const [dltEditData, setDltEditData] = useState({});
|
|
184
|
-
const [showDltCard, setShowDltCard] = useState(false);
|
|
185
|
-
const [fallbackPreviewmode, setFallbackPreviewmode] = useState(false);
|
|
186
|
-
const [dltPreviewData, setDltPreviewData] = useState('');
|
|
248
|
+
/** `undefined` = not hydrated yet; `null` = no fallback / user removed template; object = selected fallback */
|
|
249
|
+
const [smsFallbackData, setSmsFallbackData] = useState(undefined);
|
|
187
250
|
const [suggestions, setSuggestions] = useState(INITIAL_SUGGESTIONS_DATA_STOP);
|
|
188
|
-
const
|
|
251
|
+
const buttonType = RCS_BUTTON_TYPES.NONE;
|
|
189
252
|
const [suggestionError, setSuggestionError] = useState(true);
|
|
253
|
+
const [isTestAndPreviewMode, setIsTestAndPreviewMode] = useState(false);
|
|
190
254
|
const [templateType, setTemplateType] = useState('text_message');
|
|
191
|
-
const [templateHeader, setTemplateHeader] = useState('');
|
|
192
|
-
const [templateMessage, setTemplateMessage] = useState('');
|
|
193
|
-
const [templateHeaderError, setTemplateHeaderError] = useState('');
|
|
194
|
-
const [templateMessageError, setTemplateMessageError] = useState('');
|
|
195
|
-
const validVarRegex = /\{\{(\d+)\}\}/g;
|
|
196
|
-
const [updatedTitleData, setUpdatedTitleData] = useState([]);
|
|
197
|
-
const [updatedDescData, setUpdatedDescData] = useState([]);
|
|
198
255
|
const [titleVarMappedData, setTitleVarMappedData] = useState({});
|
|
199
256
|
const [descVarMappedData, setDescVarMappedData] = useState({});
|
|
200
257
|
const [titleTextAreaId, setTitleTextAreaId] = useState();
|
|
@@ -205,75 +262,58 @@ export const Rcs = (props) => {
|
|
|
205
262
|
const [rcsVideoSrc, setRcsVideoSrc] = useState({});
|
|
206
263
|
const [rcsThumbnailSrc, setRcsThumbnailSrc] = useState('');
|
|
207
264
|
const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
|
|
208
|
-
|
|
265
|
+
// Carousel (UI-only) state
|
|
266
|
+
const [selectedCarousel, setSelectedCarousel] = useState('');
|
|
267
|
+
const [selectedCarouselHeight, setSelectedCarouselHeight] = useState(MEDIUM);
|
|
268
|
+
const [selectedCarouselWidth, setSelectedCarouselWidth] = useState(SMALL);
|
|
269
|
+
const [carouselData, setCarouselData] = useState([]);
|
|
270
|
+
const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
|
|
271
|
+
const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
|
|
272
|
+
const [carouselResetNonce, setCarouselResetNonce] = useState(0);
|
|
273
|
+
const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
|
|
274
|
+
const [imageError, setImageError] = useState(null);
|
|
209
275
|
const [templateTitleError, setTemplateTitleError] = useState(false);
|
|
210
276
|
const [cardVarMapped, setCardVarMapped] = useState({});
|
|
277
|
+
/** Bump when hydrated cardVarMapped payload changes so VarSegment TextAreas remount with fresh valueMap (controlled-input sync). */
|
|
278
|
+
const [rcsVarSegmentEditorRemountKey, setRcsVarSegmentEditorRemountKey] = useState(0);
|
|
279
|
+
const lastHydratedRcsCardVarSignatureRef = useRef(null);
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Hydrate only from template payload — not from full `rcsData`. Uploads/asset updates change `rcsData`
|
|
283
|
+
* without changing `templateDetails`; re-running hydration then cleared `smsFallbackData`, so the SMS
|
|
284
|
+
* fallback card / content stopped appearing until re-selected.
|
|
285
|
+
*/
|
|
286
|
+
const rcsHydrationDetails = useMemo(
|
|
287
|
+
() => (isFullMode ? rcsData?.templateDetails : templateData),
|
|
288
|
+
[isFullMode, rcsData?.templateDetails, templateData],
|
|
289
|
+
);
|
|
211
290
|
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
226
|
-
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
227
|
-
|
|
228
|
-
// Build media preview object (same pattern as getRcsPreview)
|
|
229
|
-
const mediaPreview = {};
|
|
230
|
-
if (isMediaTypeImage && rcsImageSrc) {
|
|
231
|
-
mediaPreview.rcsImageSrc = rcsImageSrc;
|
|
232
|
-
}
|
|
233
|
-
if (isMediaTypeVideo && !isMediaTypeText) {
|
|
234
|
-
// For video, use thumbnailSrc as rcsVideoSrc (same as getRcsPreview line 2104)
|
|
235
|
-
if (rcsThumbnailSrc) {
|
|
236
|
-
mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
|
|
237
|
-
} else if (rcsVideoSrc?.videoSrc) {
|
|
238
|
-
mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
|
|
239
|
-
}
|
|
240
|
-
// Also include thumbnailSrc separately if available
|
|
241
|
-
if (rcsThumbnailSrc) {
|
|
242
|
-
mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
|
|
291
|
+
/** Skip duplicate /meta/TAG fetches: same query is triggered from (1) useEffect below, (2) title TagList mount, (3) description TagList mount — each calls getTagsforContext('Outbound'). */
|
|
292
|
+
const lastTagSchemaQueryKeyRef = useRef(null);
|
|
293
|
+
/**
|
|
294
|
+
* Library: parent often passes a new `templateData` object reference every render. Re-applying the same
|
|
295
|
+
* SMS fallback snapshot was resetting `smsFallbackData` and caused VarSegment inputs to flicker old/new.
|
|
296
|
+
*/
|
|
297
|
+
const lastSmsFallbackHydrationKeyRef = useRef(null);
|
|
298
|
+
|
|
299
|
+
const fetchTagSchemaIfNewQuery = useCallback(
|
|
300
|
+
(query) => {
|
|
301
|
+
const key = JSON.stringify(query);
|
|
302
|
+
if (lastTagSchemaQueryKeyRef.current === key) {
|
|
303
|
+
return;
|
|
243
304
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// templateDesc is used for ALL types (text message body or rich card description)
|
|
250
|
-
// For UnifiedPreview, we map templateTitle -> templateHeader and templateDesc -> templateMessage
|
|
251
|
-
const contentObj = {
|
|
252
|
-
// Map templateTitle to templateHeader and templateDesc to templateMessage
|
|
253
|
-
templateHeader: templateTitle,
|
|
254
|
-
templateMessage: templateDesc,
|
|
255
|
-
...mediaPreview,
|
|
256
|
-
...(suggestions.length > 0 && {
|
|
257
|
-
suggestions: suggestions,
|
|
258
|
-
}),
|
|
259
|
-
};
|
|
305
|
+
lastTagSchemaQueryKeyRef.current = key;
|
|
306
|
+
globalActions.fetchSchemaForEntity(query);
|
|
307
|
+
},
|
|
308
|
+
[globalActions],
|
|
309
|
+
);
|
|
260
310
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
templateMediaType,
|
|
264
|
-
templateTitle,
|
|
265
|
-
templateDesc,
|
|
266
|
-
rcsImageSrc,
|
|
267
|
-
rcsVideoSrc,
|
|
268
|
-
rcsThumbnailSrc,
|
|
269
|
-
suggestions,
|
|
270
|
-
selectedDimension,
|
|
271
|
-
]);
|
|
311
|
+
// TestAndPreviewSlidebox state
|
|
312
|
+
const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
|
|
272
313
|
|
|
273
314
|
// Handle Test and Preview button click
|
|
274
315
|
const handleTestAndPreview = useCallback(() => {
|
|
275
316
|
setShowTestAndPreviewSlidebox(true);
|
|
276
|
-
setIsTestAndPreviewMode(true);
|
|
277
317
|
if (propsHandleTestAndPreview) {
|
|
278
318
|
propsHandleTestAndPreview();
|
|
279
319
|
}
|
|
@@ -297,31 +337,332 @@ export const Rcs = (props) => {
|
|
|
297
337
|
// For video
|
|
298
338
|
return RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL;
|
|
299
339
|
};
|
|
340
|
+
/** Merge editor slot map into `smsFallbackData` (like `cardVarMapped` for title/desc). */
|
|
341
|
+
const handleSmsFallbackEditorStateChange = useCallback((patch) => {
|
|
342
|
+
setSmsFallbackData((prev) => {
|
|
343
|
+
if (!patch || typeof patch !== 'object') return prev;
|
|
344
|
+
// Bail out when no template has been selected yet — SmsTraiEdit fires
|
|
345
|
+
// onRcsFallbackEditorStateChange on mount (unicodeValidity, varMapped),
|
|
346
|
+
// which would create a non-null smsFallbackData from nothing and cause
|
|
347
|
+
// the card to appear as "Untitled creative" before any save.
|
|
348
|
+
if (!prev) return prev;
|
|
349
|
+
return { ...prev, ...patch };
|
|
350
|
+
});
|
|
351
|
+
}, []);
|
|
300
352
|
|
|
353
|
+
/** RCS template save / edit API: `rcsContent.accountId` must stay `sourceAccountIdentifier` (pairs with accessToken). */
|
|
301
354
|
const [accountId, setAccountId] = useState('');
|
|
355
|
+
/** WeCRM list row `id` — only for CommonTestAndPreview → createMessageMeta payload, not for template save. */
|
|
356
|
+
const [wecrmAccountId, setWecrmAccountId] = useState('');
|
|
302
357
|
const [accessToken, setAccessToken] = useState('');
|
|
303
358
|
const [hostName, setHostName] = useState('');
|
|
304
359
|
const [accountName, setAccountName] = useState('');
|
|
360
|
+
const isHostInfoBip = hostName === HOST_INFOBIP;
|
|
361
|
+
const isHostIcs = hostName === HOST_ICS;
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
setSuggestions(isHostIcs ? INITIAL_SUGGESTIONS_DATA_STOP : []);
|
|
365
|
+
}, [isHostIcs]);
|
|
305
366
|
const [rcsAccount, setRcsAccount] = useState('');
|
|
306
367
|
useEffect(() => {
|
|
307
368
|
const accountObj = accountData.selectedRcsAccount || {};
|
|
308
369
|
if (!isEmpty(accountObj)) {
|
|
309
370
|
const {
|
|
371
|
+
id: wecrmId,
|
|
310
372
|
sourceAccountIdentifier = '',
|
|
311
373
|
configs = {},
|
|
312
374
|
} = accountObj;
|
|
313
|
-
|
|
314
375
|
setAccountId(sourceAccountIdentifier);
|
|
376
|
+
setWecrmAccountId(
|
|
377
|
+
wecrmId != null && String(wecrmId).trim() !== '' ? String(wecrmId) : '',
|
|
378
|
+
);
|
|
315
379
|
setAccessToken(configs.accessToken || '');
|
|
316
380
|
setHostName(accountObj.hostName || '');
|
|
317
381
|
setAccountName(accountObj.name || '');
|
|
318
382
|
setRcsAccount(accountObj.id || '');
|
|
383
|
+
} else {
|
|
384
|
+
setAccountId('');
|
|
385
|
+
setWecrmAccountId('');
|
|
386
|
+
setAccessToken('');
|
|
387
|
+
setHostName('');
|
|
388
|
+
setAccountName('');
|
|
319
389
|
}
|
|
320
390
|
}, [accountData.selectedRcsAccount]);
|
|
321
391
|
|
|
322
392
|
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
323
393
|
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
324
394
|
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
395
|
+
const isCarouselType = templateType === contentType.carousel;
|
|
396
|
+
|
|
397
|
+
const clearCarouselCardMedia = (cardIndex, { clearImage = true, clearVideo = true, clearThumb = true } = {}) => {
|
|
398
|
+
setCarouselData((prev = []) => {
|
|
399
|
+
const updated = cloneDeep(prev);
|
|
400
|
+
if (!updated?.[cardIndex]) return updated;
|
|
401
|
+
if (clearImage) updated[cardIndex].imageSrc = '';
|
|
402
|
+
if (clearVideo) updated[cardIndex].videoAsset = {};
|
|
403
|
+
if (clearThumb) updated[cardIndex].thumbnailSrc = '';
|
|
404
|
+
return updated;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (clearImage) actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIndex));
|
|
408
|
+
if (clearVideo) actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIndex));
|
|
409
|
+
if (clearThumb) actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIndex));
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const resetCarouselMediaForAllCards = () => {
|
|
413
|
+
setCarouselData((prev = []) => {
|
|
414
|
+
const updated = cloneDeep(prev);
|
|
415
|
+
updated.forEach((card) => {
|
|
416
|
+
if (!card) return;
|
|
417
|
+
card.imageSrc = '';
|
|
418
|
+
card.videoAsset = {};
|
|
419
|
+
card.thumbnailSrc = '';
|
|
420
|
+
});
|
|
421
|
+
return updated;
|
|
422
|
+
});
|
|
423
|
+
(carouselData || []).forEach((_, idx) => {
|
|
424
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(idx));
|
|
425
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(idx));
|
|
426
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(idx));
|
|
427
|
+
});
|
|
428
|
+
// Force tab panes to remount after global reset (some tab implementations cache inactive panes)
|
|
429
|
+
setCarouselResetNonce((count) => count + 1);
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Always use functional updates: image upload completes in CapImageUpload's useEffect and calls
|
|
433
|
+
// updateImageSrc → this handler. If we cloned carouselData from render, we'd overwrite title/body/
|
|
434
|
+
// suggestions typed since the last commit (stale closure).
|
|
435
|
+
const handleCarouselValueChange = (carouselIndex, fields) => {
|
|
436
|
+
setCarouselData((prev = []) => {
|
|
437
|
+
const updated = cloneDeep(prev);
|
|
438
|
+
if (!updated[carouselIndex]) return prev;
|
|
439
|
+
fields.forEach(({ fieldName, value }) => {
|
|
440
|
+
updated[carouselIndex][fieldName] = value;
|
|
441
|
+
});
|
|
442
|
+
return updated;
|
|
443
|
+
});
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const updateCarouselErrors = (carouselIndex, patch) => {
|
|
447
|
+
setCarouselErrors((prev = []) => {
|
|
448
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
449
|
+
next[carouselIndex] = { ...(next[carouselIndex] || {}), ...patch };
|
|
450
|
+
return next;
|
|
451
|
+
});
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const deleteCarouselCard = (index) => {
|
|
455
|
+
let nextIdx = 0;
|
|
456
|
+
flushSync(() => {
|
|
457
|
+
setCarouselData((prev = []) => {
|
|
458
|
+
const updated = cloneDeep(prev);
|
|
459
|
+
if (index < 0 || index >= updated.length) return updated;
|
|
460
|
+
updated.splice(index, 1);
|
|
461
|
+
nextIdx = Math.max(0, Math.min(index - 1, updated.length - 1));
|
|
462
|
+
return ensureFirstCardDefaultPhoneSuggestions(updated);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
setCarouselErrors((prev = []) => {
|
|
466
|
+
const next = Array.isArray(prev) ? [...prev] : [];
|
|
467
|
+
next.splice(index, 1);
|
|
468
|
+
return next;
|
|
469
|
+
});
|
|
470
|
+
setActiveCarouselIndex(`${nextIdx}`);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
const addCarouselCard = () => {
|
|
475
|
+
let newIndex = 0;
|
|
476
|
+
flushSync(() => {
|
|
477
|
+
setCarouselData((prev = []) => {
|
|
478
|
+
const updated = cloneDeep(prev);
|
|
479
|
+
updated.push(cloneDeep(RCS_CAROUSEL_INITIAL_CARD));
|
|
480
|
+
newIndex = updated.length - 1;
|
|
481
|
+
return updated;
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
setCarouselErrors((prev = []) => ([...(Array.isArray(prev) ? prev : []), {}]));
|
|
485
|
+
setActiveCarouselIndex(`${newIndex}`);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Initialize carousel data when switching to carousel type
|
|
489
|
+
useEffect(() => {
|
|
490
|
+
if (!isCarouselType) return;
|
|
491
|
+
if (!carouselData || carouselData.length === 0) {
|
|
492
|
+
setCarouselData([cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)]);
|
|
493
|
+
setCarouselErrors([{}]);
|
|
494
|
+
setActiveCarouselIndex('0');
|
|
495
|
+
}
|
|
496
|
+
}, [isCarouselType]);
|
|
497
|
+
|
|
498
|
+
// keep derived carousel key in sync
|
|
499
|
+
useEffect(() => {
|
|
500
|
+
if (!isCarouselType) return;
|
|
501
|
+
if (!selectedCarouselHeight || !selectedCarouselWidth) return;
|
|
502
|
+
// Required format: HEIGHT_WIDTH
|
|
503
|
+
setSelectedCarousel(`${selectedCarouselHeight}_${selectedCarouselWidth}`);
|
|
504
|
+
}, [isCarouselType, selectedCarouselHeight, selectedCarouselWidth]);
|
|
505
|
+
|
|
506
|
+
const renderCarouselDimensionSelection = () => {
|
|
507
|
+
if (!isCarouselType) return null;
|
|
508
|
+
return (
|
|
509
|
+
<CarouselDimensionSelection
|
|
510
|
+
isEditFlow={isEditFlow}
|
|
511
|
+
isFullMode={isFullMode}
|
|
512
|
+
selectedCarouselHeight={selectedCarouselHeight}
|
|
513
|
+
selectedCarouselWidth={selectedCarouselWidth}
|
|
514
|
+
selectedCarousel={selectedCarousel}
|
|
515
|
+
onHeightChange={(val) => {
|
|
516
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
517
|
+
resetCarouselMediaForAllCards();
|
|
518
|
+
setSelectedCarouselHeight(val);
|
|
519
|
+
}}
|
|
520
|
+
onWidthChange={(val) => {
|
|
521
|
+
// Like rich-card dimension changes: clear media so user re-uploads matching new dimensions.
|
|
522
|
+
resetCarouselMediaForAllCards();
|
|
523
|
+
setSelectedCarouselWidth(val);
|
|
524
|
+
}}
|
|
525
|
+
/>
|
|
526
|
+
);
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const getCarouselTabPanes = () => {
|
|
530
|
+
const tagsForPicker = getRcsTags();
|
|
531
|
+
return (carouselData || []).map((card, index) => ({
|
|
532
|
+
key: String(index),
|
|
533
|
+
tab: index + 1,
|
|
534
|
+
content: (
|
|
535
|
+
<CarouselCard
|
|
536
|
+
cardIndex={index}
|
|
537
|
+
card={card}
|
|
538
|
+
carouselLength={carouselData.length}
|
|
539
|
+
isEditFlow={isEditFlow}
|
|
540
|
+
isFullMode={isFullMode}
|
|
541
|
+
mediaRadioOptions={mediaRadioOptions}
|
|
542
|
+
cardError={carouselErrors?.[index]}
|
|
543
|
+
location={location}
|
|
544
|
+
tagsForPicker={tagsForPicker}
|
|
545
|
+
injectedTags={injectedTags}
|
|
546
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
547
|
+
formatMessage={formatMessage}
|
|
548
|
+
titleMaxLength={getTitleMaxLength()}
|
|
549
|
+
descriptionMaxLength={getDescriptionMaxLength()}
|
|
550
|
+
titleCharCount={getCarouselTitleCharacterCount(carouselData, index)}
|
|
551
|
+
descriptionCharCount={getCarouselDescriptionCharacterCount(carouselData, index)}
|
|
552
|
+
dimKey={getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth)}
|
|
553
|
+
rcsData={rcsData}
|
|
554
|
+
uploadRcsImage={uploadRcsImage}
|
|
555
|
+
uploadRcsVideo={uploadRcsVideo}
|
|
556
|
+
hostName={hostName}
|
|
557
|
+
onDelete={() => deleteCarouselCard(index)}
|
|
558
|
+
onMediaTypeChange={(value) => {
|
|
559
|
+
// Reset media fields when switching type
|
|
560
|
+
if (value === RCS_MEDIA_TYPES.IMAGE) {
|
|
561
|
+
// Switching to IMAGE: clear video + thumbnail uploads so they don't auto-restore.
|
|
562
|
+
clearCarouselCardMedia(index, { clearImage: false, clearVideo: true, clearThumb: true });
|
|
563
|
+
handleCarouselValueChange(index, [
|
|
564
|
+
{ fieldName: 'mediaType', value },
|
|
565
|
+
{ fieldName: 'videoAsset', value: {} },
|
|
566
|
+
{ fieldName: 'thumbnailSrc', value: '' },
|
|
567
|
+
]);
|
|
568
|
+
} else {
|
|
569
|
+
// Switching to VIDEO: clear image upload so it doesn't auto-restore.
|
|
570
|
+
clearCarouselCardMedia(index, { clearImage: true, clearVideo: false, clearThumb: false });
|
|
571
|
+
handleCarouselValueChange(index, [
|
|
572
|
+
{ fieldName: 'mediaType', value },
|
|
573
|
+
{ fieldName: 'imageSrc', value: '' },
|
|
574
|
+
]);
|
|
575
|
+
}
|
|
576
|
+
}}
|
|
577
|
+
onTitleChange={({ target: { value } }) => {
|
|
578
|
+
let error = false;
|
|
579
|
+
if (value?.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
580
|
+
error = formatMessage(messages.templateHeaderLengthError);
|
|
581
|
+
} else {
|
|
582
|
+
error = variableErrorHandling(value);
|
|
583
|
+
}
|
|
584
|
+
updateCarouselErrors(index, { title: error });
|
|
585
|
+
handleCarouselValueChange(index, [{ fieldName: 'title', value }]);
|
|
586
|
+
}}
|
|
587
|
+
onDescriptionChange={({ target: { value } }) => {
|
|
588
|
+
let error = false;
|
|
589
|
+
if (value?.length > RCS_RICH_CARD_MAX_LENGTH) {
|
|
590
|
+
error = formatMessage(messages.templateMessageLengthError);
|
|
591
|
+
} else {
|
|
592
|
+
error = variableErrorHandling(value);
|
|
593
|
+
}
|
|
594
|
+
updateCarouselErrors(index, { description: error });
|
|
595
|
+
handleCarouselValueChange(index, [{ fieldName: 'description', value }]);
|
|
596
|
+
}}
|
|
597
|
+
onTitleAddVar={() => appendVarToCarouselField(index, 'title')}
|
|
598
|
+
onDescriptionAddVar={() => appendVarToCarouselField(index, 'description')}
|
|
599
|
+
onTagSelect={onCarouselTagSelect}
|
|
600
|
+
onContextChange={handleOnTagsContextChange}
|
|
601
|
+
renderEditMessage={renderCarouselEditMessage}
|
|
602
|
+
onFieldChange={(fieldName, value) => handleCarouselValueChange(index, [{ fieldName, value }])}
|
|
603
|
+
onSuggestionsChange={(nextSuggestions) =>
|
|
604
|
+
handleCarouselValueChange(index, [{ fieldName: 'suggestions', value: nextSuggestions }])}
|
|
605
|
+
/>
|
|
606
|
+
),
|
|
607
|
+
}));
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const renderCarouselSection = () => {
|
|
611
|
+
if (!isCarouselType) return null;
|
|
612
|
+
|
|
613
|
+
const isMaxCarouselCardsReached = MAX_RCS_CAROUSEL_ALLOWED === (carouselData?.length || 0);
|
|
614
|
+
const addCardTooltipTitle = isMaxCarouselCardsReached
|
|
615
|
+
? formatMessage(messages.rcsCarouselMaxCardTooltip, { max: MAX_RCS_CAROUSEL_ALLOWED })
|
|
616
|
+
// "Set up the first card" only makes sense while creating a new template —
|
|
617
|
+
// in edit flow the cards already exist, so don't show it there.
|
|
618
|
+
: (!isEditFlow && checkDisableAddCarouselButton(carouselData, activeCarouselIndex) ? formatMessage(messages.rcsCarouselAddCardDisabledTooltip) : '');
|
|
619
|
+
|
|
620
|
+
// No "+" in edit flow (cards can't be added there) — and with no "+", the
|
|
621
|
+
// divider before it has nothing to separate, so skip both together instead
|
|
622
|
+
// of rendering a dangling divider.
|
|
623
|
+
const operations = isEditFlow ? null : (
|
|
624
|
+
<>
|
|
625
|
+
<CapDivider type="vertical" />
|
|
626
|
+
<CapTooltip title={addCardTooltipTitle}>
|
|
627
|
+
<span className="button-disabled-tooltip-wrapper">
|
|
628
|
+
<CapButton
|
|
629
|
+
onClick={addCarouselCard}
|
|
630
|
+
type="flat"
|
|
631
|
+
className="add-carousel-content-button"
|
|
632
|
+
disabled={
|
|
633
|
+
isEditFlow ||
|
|
634
|
+
!isFullMode ||
|
|
635
|
+
isMaxCarouselCardsReached ||
|
|
636
|
+
checkDisableAddCarouselButton(carouselData, activeCarouselIndex)
|
|
637
|
+
}
|
|
638
|
+
>
|
|
639
|
+
<CapIcon type="plus" />
|
|
640
|
+
</CapButton>
|
|
641
|
+
</span>
|
|
642
|
+
</CapTooltip>
|
|
643
|
+
</>
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
return (
|
|
647
|
+
<CapRow className={`rcs-carousel-section ${isEditFlow ? 'rcs-carousel-section--readonly-above' : ''}`}>
|
|
648
|
+
{renderCarouselDimensionSelection()}
|
|
649
|
+
<CapRow className="rcs-carousel-tab">
|
|
650
|
+
<CapTab
|
|
651
|
+
key={`rcs-carousel-tab-${carouselResetNonce}`}
|
|
652
|
+
defaultActiveKey="0"
|
|
653
|
+
activeKey={activeCarouselIndex}
|
|
654
|
+
tabBarExtraContent={operations}
|
|
655
|
+
onChange={(key) => {
|
|
656
|
+
setActiveCarouselIndex(`${key}`);
|
|
657
|
+
// reset focused var when switching cards (as per requirement)
|
|
658
|
+
setCarouselFocusedVarId('');
|
|
659
|
+
}}
|
|
660
|
+
panes={getCarouselTabPanes()}
|
|
661
|
+
/>
|
|
662
|
+
</CapRow>
|
|
663
|
+
</CapRow>
|
|
664
|
+
);
|
|
665
|
+
};
|
|
325
666
|
|
|
326
667
|
const mediaRadioOptions = [
|
|
327
668
|
{
|
|
@@ -330,11 +671,16 @@ export const Rcs = (props) => {
|
|
|
330
671
|
},
|
|
331
672
|
{
|
|
332
673
|
value: RCS_MEDIA_TYPES.VIDEO,
|
|
333
|
-
label: formatMessage(
|
|
674
|
+
label: formatMessage(
|
|
675
|
+
templateType === contentType.carousel
|
|
676
|
+
? messages.carouselMediaVideoOption
|
|
677
|
+
: messages.mediaVideo,
|
|
678
|
+
),
|
|
334
679
|
},
|
|
335
680
|
];
|
|
336
681
|
const aiContentBotDisabled = isAiContentBotDisabled();
|
|
337
682
|
|
|
683
|
+
|
|
338
684
|
const updateButtonChange = (data, index) => {
|
|
339
685
|
if (data && data.text) {
|
|
340
686
|
const forbiddenError = forbiddenCharactersValidation(data.text);
|
|
@@ -371,7 +717,9 @@ export const Rcs = (props) => {
|
|
|
371
717
|
if (isFullMode) return;
|
|
372
718
|
if (loadingTags || !tags || tags.length === 0) return;
|
|
373
719
|
const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
374
|
-
const
|
|
720
|
+
const slotOffset =
|
|
721
|
+
type === TITLE_TEXT ? 0 : (templateTitle ? templateTitle.match(rcsVarRegex) || [] : []).length;
|
|
722
|
+
const resolved = resolveTemplateWithMap(templateStr, slotOffset); // placeholders -> mapped value (or '')
|
|
375
723
|
if (!resolved) {
|
|
376
724
|
if (type === TITLE_TEXT) setTemplateTitleError(false);
|
|
377
725
|
if (type === MESSAGE_TEXT) setTemplateDescError(false);
|
|
@@ -380,8 +728,8 @@ export const Rcs = (props) => {
|
|
|
380
728
|
|
|
381
729
|
let contentForValidation = resolved;
|
|
382
730
|
const placeholderTokens = templateStr.match(rcsVarRegex) || [];
|
|
383
|
-
placeholderTokens.forEach((
|
|
384
|
-
const escaped =
|
|
731
|
+
placeholderTokens.forEach((token) => {
|
|
732
|
+
const escaped = token.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
|
|
385
733
|
contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
|
|
386
734
|
});
|
|
387
735
|
if (!contentForValidation.trim()) {
|
|
@@ -398,10 +746,16 @@ export const Rcs = (props) => {
|
|
|
398
746
|
tagModule: getDefaultTags,
|
|
399
747
|
isFullMode,
|
|
400
748
|
}) || {};
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
749
|
+
const unsupportedTagsLengthCheck =
|
|
750
|
+
validationResponse?.unsupportedTags?.length > 0;
|
|
751
|
+
const errorMsg =
|
|
752
|
+
(unsupportedTagsLengthCheck &&
|
|
753
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
754
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
755
|
+
})) ||
|
|
756
|
+
(validationResponse.isBraceError &&
|
|
757
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
758
|
+
false;
|
|
405
759
|
if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
|
|
406
760
|
if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
|
|
407
761
|
};
|
|
@@ -414,55 +768,214 @@ export const Rcs = (props) => {
|
|
|
414
768
|
validateResolvedTagsForType(MESSAGE_TEXT);
|
|
415
769
|
}, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
|
|
416
770
|
|
|
771
|
+
useEffect(() => {
|
|
772
|
+
if (isFullMode || !isCarouselType) return;
|
|
773
|
+
if (loadingTags || !tags || tags.length === 0) return;
|
|
774
|
+
(carouselData || []).forEach((card, idx) => {
|
|
775
|
+
['title', 'description'].forEach((field) => {
|
|
776
|
+
const templateStr = card?.[field] || '';
|
|
777
|
+
if (!templateStr) return;
|
|
778
|
+
const resolved = resolveTemplateWithMap(templateStr);
|
|
779
|
+
if (!resolved) {
|
|
780
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
let contentForValidation = resolved;
|
|
784
|
+
const placeholderTokens = templateStr.match(rcsVarRegex) || [];
|
|
785
|
+
placeholderTokens.forEach((token) => {
|
|
786
|
+
const escaped = token.replace(REGEX_SPECIAL_CHARS_ESCAPE_PATTERN, '\\$&');
|
|
787
|
+
contentForValidation = contentForValidation.replace(new RegExp(escaped, 'g'), '');
|
|
788
|
+
});
|
|
789
|
+
if (!contentForValidation.trim()) {
|
|
790
|
+
updateCarouselErrors(idx, { [field]: false });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const validationResponse = validateTags({
|
|
794
|
+
content: contentForValidation,
|
|
795
|
+
tagsParam: tags,
|
|
796
|
+
injectedTagsParams: injectedTags,
|
|
797
|
+
location,
|
|
798
|
+
tagModule: getDefaultTags,
|
|
799
|
+
eventContextTags,
|
|
800
|
+
isFullMode,
|
|
801
|
+
}) || {};
|
|
802
|
+
const errorMsg =
|
|
803
|
+
(validationResponse?.unsupportedTags?.length > 0 &&
|
|
804
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
805
|
+
unsupportedTags: validationResponse.unsupportedTags,
|
|
806
|
+
})) ||
|
|
807
|
+
(validationResponse.isBraceError &&
|
|
808
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
|
|
809
|
+
false;
|
|
810
|
+
updateCarouselErrors(idx, { [field]: errorMsg });
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
}, [cardVarMapped, carouselData, tags, injectedTags, loadingTags, isCarouselType]);
|
|
814
|
+
|
|
417
815
|
const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
|
|
418
816
|
|
|
419
|
-
const
|
|
817
|
+
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
818
|
+
|
|
819
|
+
/** Same `{{tag}}` in both title and description must not share one semantic map entry. */
|
|
820
|
+
const rcsSpanningSemanticVarNames = useMemo(
|
|
821
|
+
() => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
|
|
822
|
+
[templateTitle, templateDesc],
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
/** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
|
|
826
|
+
const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
|
|
827
|
+
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
828
|
+
const offset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
|
|
829
|
+
const templateSegments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
|
|
830
|
+
let varOrdinal = 0;
|
|
831
|
+
for (let segmentIndexInField = 0; segmentIndexInField < templateSegments.length; segmentIndexInField += 1) {
|
|
832
|
+
const segmentToken = templateSegments[segmentIndexInField];
|
|
833
|
+
if (rcsVarTestRegex.test(segmentToken)) {
|
|
834
|
+
if (`${segmentToken}_${segmentIndexInField}` === varSegmentCompositeId) {
|
|
835
|
+
return offset + varOrdinal;
|
|
836
|
+
}
|
|
837
|
+
varOrdinal += 1;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Master-branch resolve: uses numeric slot keys + semantic spanning detection for correct
|
|
845
|
+
* multi-field variable resolution.
|
|
846
|
+
*/
|
|
847
|
+
const resolveTemplateWithMap = (str = '', slotOffset = 0) => {
|
|
420
848
|
if (!str) return '';
|
|
421
|
-
const arr =
|
|
849
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
850
|
+
let varOrdinal = 0;
|
|
422
851
|
return arr.map((elem) => {
|
|
423
852
|
if (rcsVarTestRegex.test(elem)) {
|
|
424
853
|
const key = getVarNameFromToken(elem);
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
854
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
855
|
+
varOrdinal += 1;
|
|
856
|
+
const slotValue = resolveCardVarMappedSlotValue(
|
|
857
|
+
cardVarMapped,
|
|
858
|
+
key,
|
|
859
|
+
globalSlot,
|
|
860
|
+
isEditLike,
|
|
861
|
+
rcsSpanningSemanticVarNames.has(key),
|
|
862
|
+
);
|
|
863
|
+
if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
|
|
864
|
+
return String(slotValue);
|
|
428
865
|
}
|
|
429
866
|
return elem;
|
|
430
867
|
}).join('');
|
|
431
868
|
};
|
|
432
869
|
|
|
870
|
+
const buildCarouselCardsForPreview = (cards = []) =>
|
|
871
|
+
buildCarouselCardsForPreviewUtil(cards, { isFullMode, rcsVarRegex, resolveTemplateWithMap });
|
|
433
872
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
873
|
+
/**
|
|
874
|
+
* Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
|
|
875
|
+
* (VarSegmentMessageEditor: isEditFlow || !isFullMode). Full-mode create without edit uses plain
|
|
876
|
+
* TextArea only — no mapping. Full-mode + edit uses slots; !isFullMode alone is not enough.
|
|
877
|
+
*/
|
|
878
|
+
const getTemplateContent = useCallback(() => {
|
|
879
|
+
if (templateType === contentType.carousel) {
|
|
880
|
+
const carouselDimKey = getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth);
|
|
881
|
+
const carouselImgDims =
|
|
882
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
883
|
+
const carouselVidDims =
|
|
884
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
885
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
886
|
+
return {
|
|
887
|
+
carouselData: buildCarouselCardsForPreview(carouselData),
|
|
888
|
+
carouselPreviewDimensions: {
|
|
889
|
+
imageWidth: carouselImgDims.width,
|
|
890
|
+
imageHeight: carouselImgDims.height,
|
|
891
|
+
videoThumbWidth: carouselVidDims.width,
|
|
892
|
+
videoThumbHeight: carouselVidDims.height,
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const isMediaTypeImage = templateMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
897
|
+
const isMediaTypeVideo = templateMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
898
|
+
const isMediaTypeText = templateMediaType === RCS_MEDIA_TYPES.NONE;
|
|
454
899
|
|
|
900
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
901
|
+
const titleVarCountForResolve = isMediaTypeText
|
|
902
|
+
? 0
|
|
903
|
+
: ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
|
|
904
|
+
const resolvedTitle = isMediaTypeText
|
|
905
|
+
? ''
|
|
906
|
+
: (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
|
|
907
|
+
const resolvedDesc = isSlotMappingMode
|
|
908
|
+
? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
|
|
909
|
+
: templateDesc;
|
|
910
|
+
|
|
911
|
+
const mediaPreview = {};
|
|
912
|
+
if (isMediaTypeImage && rcsImageSrc) {
|
|
913
|
+
mediaPreview.rcsImageSrc = rcsImageSrc;
|
|
914
|
+
}
|
|
915
|
+
if (isMediaTypeVideo && !isMediaTypeText) {
|
|
916
|
+
if (rcsThumbnailSrc) {
|
|
917
|
+
mediaPreview.rcsVideoSrc = rcsThumbnailSrc;
|
|
918
|
+
} else if (rcsVideoSrc?.videoSrc) {
|
|
919
|
+
mediaPreview.rcsVideoSrc = rcsVideoSrc.videoSrc;
|
|
920
|
+
}
|
|
921
|
+
if (rcsThumbnailSrc) {
|
|
922
|
+
mediaPreview.rcsThumbnailSrc = rcsThumbnailSrc;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const contentObj = {
|
|
927
|
+
templateHeader: resolvedTitle,
|
|
928
|
+
templateMessage: resolvedDesc,
|
|
929
|
+
...mediaPreview,
|
|
930
|
+
...(suggestions.length > 0 && {
|
|
931
|
+
suggestions: suggestions,
|
|
932
|
+
}),
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
return contentObj;
|
|
936
|
+
}, [
|
|
937
|
+
templateMediaType,
|
|
938
|
+
templateTitle,
|
|
939
|
+
templateDesc,
|
|
940
|
+
rcsImageSrc,
|
|
941
|
+
rcsVideoSrc,
|
|
942
|
+
rcsThumbnailSrc,
|
|
943
|
+
suggestions,
|
|
944
|
+
selectedDimension,
|
|
945
|
+
isFullMode,
|
|
946
|
+
isEditFlow,
|
|
947
|
+
cardVarMapped,
|
|
948
|
+
rcsSpanningSemanticVarNames,
|
|
949
|
+
carouselData,
|
|
950
|
+
selectedCarouselHeight,
|
|
951
|
+
selectedCarouselWidth,
|
|
952
|
+
]);
|
|
953
|
+
|
|
954
|
+
const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
|
|
455
955
|
|
|
456
|
-
const RcsLabel = styled.div`
|
|
457
|
-
display: flex;
|
|
458
|
-
margin-top: 20px;
|
|
459
|
-
`;
|
|
460
956
|
const paramObj = params || {};
|
|
461
957
|
useEffect(() => {
|
|
462
958
|
const { id } = paramObj;
|
|
463
959
|
if (id && isFullMode) {
|
|
464
960
|
setSpin(true);
|
|
465
961
|
actions.getTemplateDetails(id, setSpin);
|
|
962
|
+
} else if (!id && isFullMode) {
|
|
963
|
+
// Create New: clear standalone media and ALL possible carousel card Redux slots.
|
|
964
|
+
// Redux persists across mounts so we must clear unconditionally (carouselData may be [] on fresh mount).
|
|
965
|
+
updateRcsImageSrc('');
|
|
966
|
+
setRcsVideoSrc({});
|
|
967
|
+
setRcsThumbnailSrc('');
|
|
968
|
+
setAssetList({});
|
|
969
|
+
setEditFlow(false);
|
|
970
|
+
actions.clearRcsMediaAsset(0);
|
|
971
|
+
actions.clearRcsMediaAsset(1);
|
|
972
|
+
for (let cardIdx = 0; cardIdx < MAX_RCS_CAROUSEL_ALLOWED; cardIdx += 1) {
|
|
973
|
+
actions.clearRcsMediaAsset(getCarouselImageAssetIndex(cardIdx));
|
|
974
|
+
actions.clearRcsMediaAsset(getCarouselVideoAssetIndex(cardIdx));
|
|
975
|
+
actions.clearRcsMediaAsset(getCarouselThumbnailAssetIndex(cardIdx));
|
|
976
|
+
}
|
|
977
|
+
setCarouselData([]);
|
|
978
|
+
setCarouselErrors([]);
|
|
466
979
|
}
|
|
467
980
|
return () => {
|
|
468
981
|
actions.clearEditResponse();
|
|
@@ -470,59 +983,68 @@ export const Rcs = (props) => {
|
|
|
470
983
|
}, [paramObj.id]);
|
|
471
984
|
|
|
472
985
|
useEffect(() => {
|
|
473
|
-
|
|
986
|
+
if (!(isEditFlow || !isFullMode)) return;
|
|
474
987
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
988
|
+
const titleTokenCount = (templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length;
|
|
989
|
+
|
|
990
|
+
const initField = (targetString, setVarMap, slotOffset) => {
|
|
991
|
+
const arr = splitTemplateVarStringRcs(targetString);
|
|
992
|
+
if (!arr?.length) {
|
|
993
|
+
setVarMap({});
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const nextVarMap = {};
|
|
997
|
+
let varOrdinal = 0;
|
|
998
|
+
arr.forEach((elem, idx) => {
|
|
999
|
+
// Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
|
|
1000
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
1001
|
+
const id = `${elem}_${idx}`;
|
|
1002
|
+
const varName = getVarNameFromToken(elem);
|
|
1003
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
1004
|
+
varOrdinal += 1;
|
|
1005
|
+
const mappedValue = resolveCardVarMappedSlotValue(
|
|
1006
|
+
cardVarMapped,
|
|
1007
|
+
varName,
|
|
1008
|
+
globalSlot,
|
|
1009
|
+
isEditLike,
|
|
1010
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
1011
|
+
);
|
|
1012
|
+
nextVarMap[id] = mappedValue;
|
|
481
1013
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
// RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
|
|
486
|
-
if (rcsVarTestRegex.test(elem)) {
|
|
487
|
-
const id = `${elem}_${idx}`;
|
|
488
|
-
const varName = getVarNameFromToken(elem);
|
|
489
|
-
const mappedValue = (cardVarMapped?.[varName] ?? '').toString();
|
|
490
|
-
nextVarMap[id] = mappedValue;
|
|
491
|
-
if (mappedValue !== '') {
|
|
492
|
-
nextUpdated[idx] = mappedValue;
|
|
493
|
-
} else {
|
|
494
|
-
nextUpdated[idx] = elem;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
setVarMap(nextVarMap);
|
|
499
|
-
setUpdated(nextUpdated);
|
|
500
|
-
};
|
|
1014
|
+
});
|
|
1015
|
+
setVarMap(nextVarMap);
|
|
1016
|
+
};
|
|
501
1017
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1018
|
+
initField(templateTitle, setTitleVarMappedData, 0);
|
|
1019
|
+
initField(templateDesc, setDescVarMappedData, titleTokenCount);
|
|
1020
|
+
}, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
|
|
1021
|
+
|
|
1022
|
+
useEffect(() => {
|
|
1023
|
+
if (!isEditFlow && isFullMode) {
|
|
508
1024
|
setRcsVideoSrc({});
|
|
509
1025
|
updateRcsImageSrc('');
|
|
510
|
-
|
|
511
|
-
updateRcsThumbnailSrc('');
|
|
1026
|
+
setRcsThumbnailSrc('');
|
|
512
1027
|
setAssetList({});
|
|
513
|
-
|
|
514
|
-
|
|
1028
|
+
}
|
|
1029
|
+
}, [templateMediaType]);
|
|
515
1030
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1031
|
+
/** Status on first card — same merged card as title/description/cardVarMapped (library may only set rcsContent at root). */
|
|
1032
|
+
const templateStatusHelper = (cardContentFirst) => {
|
|
1033
|
+
const raw =
|
|
1034
|
+
cardContentFirst?.Status
|
|
1035
|
+
?? cardContentFirst?.status
|
|
1036
|
+
?? cardContentFirst?.approvalStatus
|
|
1037
|
+
?? '';
|
|
1038
|
+
const status = typeof raw === 'string' ? raw.trim() : String(raw);
|
|
1039
|
+
const normalizedStatus = status.toLowerCase();
|
|
1040
|
+
switch (normalizedStatus) {
|
|
519
1041
|
case RCS_STATUSES.approved:
|
|
520
1042
|
setTemplateStatus(RCS_STATUSES.approved);
|
|
521
1043
|
break;
|
|
522
1044
|
case RCS_STATUSES.pending:
|
|
523
1045
|
setTemplateStatus(RCS_STATUSES.pending);
|
|
524
1046
|
break;
|
|
525
|
-
case RCS_STATUSES.awaitingApproval:
|
|
1047
|
+
case RCS_STATUSES.awaitingApproval.toLowerCase():
|
|
526
1048
|
setTemplateStatus(RCS_STATUSES.awaitingApproval);
|
|
527
1049
|
break;
|
|
528
1050
|
case RCS_STATUSES.unavailable:
|
|
@@ -531,6 +1053,9 @@ export const Rcs = (props) => {
|
|
|
531
1053
|
case RCS_STATUSES.rejected:
|
|
532
1054
|
setTemplateStatus(RCS_STATUSES.rejected);
|
|
533
1055
|
break;
|
|
1056
|
+
case RCS_STATUSES.created:
|
|
1057
|
+
setTemplateStatus(RCS_STATUSES.created);
|
|
1058
|
+
break;
|
|
534
1059
|
default:
|
|
535
1060
|
setTemplateStatus(status);
|
|
536
1061
|
break;
|
|
@@ -584,48 +1109,248 @@ export const Rcs = (props) => {
|
|
|
584
1109
|
};
|
|
585
1110
|
|
|
586
1111
|
useEffect(() => {
|
|
587
|
-
const details =
|
|
1112
|
+
const details = rcsHydrationDetails;
|
|
588
1113
|
if (details && Object.keys(details).length > 0) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1114
|
+
// Library/campaign: match SMS fallback — read from versions… and from top-level rcsContent (getCreativesData / parent shape).
|
|
1115
|
+
const cardFromVersions = get(
|
|
1116
|
+
details,
|
|
1117
|
+
'versions.base.content.RCS.rcsContent.cardContent[0]',
|
|
1118
|
+
);
|
|
1119
|
+
const rcsContent = get(details, 'versions.base.content.RCS.rcsContent', {});
|
|
1120
|
+
const cardType = (rcsContent?.cardType || '').toString();
|
|
1121
|
+
|
|
1122
|
+
setEditFlow(true);
|
|
1123
|
+
setTemplateName(details?.name || details?.creativeName || '');
|
|
1124
|
+
|
|
1125
|
+
const cardFromTop = get(details, 'rcsContent.cardContent[0]');
|
|
1126
|
+
const card0 = { ...(cardFromTop || {}), ...(cardFromVersions || {}) };
|
|
1127
|
+
const cardVarMappedFromCardContent =
|
|
1128
|
+
card0?.cardVarMapped != null && typeof card0.cardVarMapped === 'object'
|
|
1129
|
+
? card0.cardVarMapped
|
|
1130
|
+
: {};
|
|
1131
|
+
const cardVarMappedFromRootMirror =
|
|
1132
|
+
details?.rcsCardVarMapped != null && typeof details.rcsCardVarMapped === 'object'
|
|
1133
|
+
? details.rcsCardVarMapped
|
|
1134
|
+
: {};
|
|
1135
|
+
// Root mirror from getCreativesData / getFormData — campaigns often preserve flat fields when
|
|
1136
|
+
// nested versions.cardContent[0].cardVarMapped is dropped on reload.
|
|
1137
|
+
const mergedCardVarMappedFromPayload = {
|
|
1138
|
+
...cardVarMappedFromRootMirror,
|
|
1139
|
+
...cardVarMappedFromCardContent,
|
|
1140
|
+
};
|
|
1141
|
+
const loadedTitleForMap = card0?.title != null ? String(card0.title) : '';
|
|
1142
|
+
const loadedDescForMap = card0?.description != null ? String(card0.description) : '';
|
|
1143
|
+
const hydratedCardVarPayloadSignature = `${loadedTitleForMap}\u0000${loadedDescForMap}\u0000${JSON.stringify(
|
|
1144
|
+
Object.keys(mergedCardVarMappedFromPayload)
|
|
1145
|
+
.sort()
|
|
1146
|
+
.reduce((accumulator, mapKey) => {
|
|
1147
|
+
accumulator[mapKey] = mergedCardVarMappedFromPayload[mapKey];
|
|
1148
|
+
return accumulator;
|
|
1149
|
+
}, {}),
|
|
1150
|
+
)}`;
|
|
1151
|
+
if (lastHydratedRcsCardVarSignatureRef.current !== hydratedCardVarPayloadSignature) {
|
|
1152
|
+
lastHydratedRcsCardVarSignatureRef.current = hydratedCardVarPayloadSignature;
|
|
1153
|
+
setRcsVarSegmentEditorRemountKey((previousKey) => previousKey + 1);
|
|
592
1154
|
}
|
|
593
|
-
const
|
|
594
|
-
|
|
1155
|
+
const tokenListForMap = [
|
|
1156
|
+
...(loadedTitleForMap ? loadedTitleForMap.match(rcsVarRegex) ?? [] : []),
|
|
1157
|
+
...(loadedDescForMap ? loadedDescForMap.match(rcsVarRegex) ?? [] : []),
|
|
1158
|
+
];
|
|
1159
|
+
const orderedTagNamesForMap = tokenListForMap.map((token) => getVarNameFromToken(token)).filter(Boolean);
|
|
1160
|
+
// Full-mode library/API payloads need normalize for legacy slot shapes. Campaign round-trip from
|
|
1161
|
+
// getFormData already stores TagList values as {{TagName}}; normalize can strip or remap them.
|
|
1162
|
+
const cardVarMappedBeforeCoalesce = isFullMode
|
|
1163
|
+
? normalizeCardVarMapped(mergedCardVarMappedFromPayload, orderedTagNamesForMap)
|
|
1164
|
+
: { ...mergedCardVarMappedFromPayload };
|
|
1165
|
+
const cardVarMappedAfterCoalesce = coalesceCardVarMappedToTemplate(
|
|
1166
|
+
cardVarMappedBeforeCoalesce,
|
|
1167
|
+
loadedTitleForMap,
|
|
1168
|
+
loadedDescForMap,
|
|
1169
|
+
rcsVarRegex,
|
|
1170
|
+
);
|
|
1171
|
+
const cardVarMappedAfterNumericSlotSync = !isFullMode
|
|
1172
|
+
? syncCardVarMappedSemanticsFromSlots(
|
|
1173
|
+
cardVarMappedAfterCoalesce,
|
|
1174
|
+
loadedTitleForMap,
|
|
1175
|
+
loadedDescForMap,
|
|
1176
|
+
rcsVarRegex,
|
|
1177
|
+
)
|
|
1178
|
+
: cardVarMappedAfterCoalesce;
|
|
1179
|
+
const hydratedCardVarMappedResult = { ...cardVarMappedAfterNumericSlotSync };
|
|
1180
|
+
// Pre-populate variable/tag mappings while opening an existing template in edit flows
|
|
1181
|
+
setCardVarMapped((previousVarMapState) => {
|
|
1182
|
+
const previousVarMap = previousVarMapState ?? {};
|
|
1183
|
+
if (previousVarMap === hydratedCardVarMappedResult) return previousVarMapState;
|
|
1184
|
+
const previousVarMapKeys = Object.keys(previousVarMap);
|
|
1185
|
+
const nextVarMapKeys = Object.keys(hydratedCardVarMappedResult);
|
|
1186
|
+
if (previousVarMapKeys.length === nextVarMapKeys.length) {
|
|
1187
|
+
const allSlotValuesMatchPrevious = previousVarMapKeys.every(
|
|
1188
|
+
(key) => previousVarMap[key] === hydratedCardVarMappedResult[key],
|
|
1189
|
+
);
|
|
1190
|
+
if (allSlotValuesMatchPrevious) return previousVarMapState;
|
|
1191
|
+
}
|
|
1192
|
+
return hydratedCardVarMappedResult;
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
if (cardType.toUpperCase() === contentType.carousel) {
|
|
1196
|
+
|
|
1197
|
+
setTemplateType(contentType.carousel);
|
|
1198
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1199
|
+
// Library/campaign payloads can store cardSettings/cardContent at the flat top-level
|
|
1200
|
+
// `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
|
|
1201
|
+
// used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
|
|
1202
|
+
// silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
|
|
1203
|
+
// actually uploaded at and causing object-fit:cover to crop the preview image.
|
|
1204
|
+
const cardSettings = rcsContent?.cardSettings
|
|
1205
|
+
|| get(details, 'rcsContent.cardSettings', {});
|
|
1206
|
+
const cardWidth = cardSettings?.cardWidth || SMALL;
|
|
1207
|
+
setSelectedCarouselWidth(cardWidth);
|
|
1208
|
+
|
|
1209
|
+
const cards = Array.isArray(rcsContent?.cardContent)
|
|
1210
|
+
? rcsContent.cardContent
|
|
1211
|
+
: get(details, 'rcsContent.cardContent', []);
|
|
1212
|
+
const firstHeight = cards?.[0]?.media?.height || MEDIUM;
|
|
1213
|
+
setSelectedCarouselHeight(firstHeight);
|
|
1214
|
+
setSelectedCarousel(`${firstHeight}_${cardWidth}`);
|
|
1215
|
+
setActiveCarouselIndex('0');
|
|
1216
|
+
|
|
1217
|
+
const hydratedCards = cards.map((c = {}, idx) => {
|
|
1218
|
+
const mediaType = c.mediaType;
|
|
1219
|
+
const media = c.media || {};
|
|
1220
|
+
const mediaUrl = media.mediaUrl || '';
|
|
1221
|
+
const thumbUrl = media.thumbnailUrl || '';
|
|
1222
|
+
const rawSuggestions = Array.isArray(c.suggestions) ? c.suggestions : [];
|
|
1223
|
+
const suggestions = idx === 0 && rawSuggestions.length === 0
|
|
1224
|
+
? cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS)
|
|
1225
|
+
: rawSuggestions;
|
|
1226
|
+
return {
|
|
1227
|
+
title: c.title || '',
|
|
1228
|
+
description: c.description || '',
|
|
1229
|
+
mediaType,
|
|
1230
|
+
imageSrc: mediaType === RCS_MEDIA_TYPES.IMAGE ? mediaUrl : '',
|
|
1231
|
+
videoAsset: mediaType === RCS_MEDIA_TYPES.VIDEO ? {
|
|
1232
|
+
videoSrc: mediaUrl,
|
|
1233
|
+
previewUrl: thumbUrl,
|
|
1234
|
+
videoThumbnail: thumbUrl,
|
|
1235
|
+
videoName: c?.media?.videoName || '',
|
|
1236
|
+
} : {},
|
|
1237
|
+
thumbnailSrc: mediaType === RCS_MEDIA_TYPES.VIDEO ? thumbUrl : '',
|
|
1238
|
+
suggestions,
|
|
1239
|
+
};
|
|
1240
|
+
});
|
|
1241
|
+
setCarouselData(
|
|
1242
|
+
hydratedCards.length > 0
|
|
1243
|
+
? ensureFirstCardDefaultPhoneSuggestions(hydratedCards)
|
|
1244
|
+
: [cloneDeep(RCS_CAROUSEL_INITIAL_FIRST_CARD)],
|
|
1245
|
+
);
|
|
1246
|
+
setCarouselErrors(new Array(hydratedCards.length > 0 ? hydratedCards.length : 1).fill({}));
|
|
1247
|
+
|
|
1248
|
+
// Status bar uses first card's Status, keep existing behavior.
|
|
1249
|
+
if (isHostInfoBip) {
|
|
1250
|
+
setTemplateStatus('');
|
|
1251
|
+
} else {
|
|
1252
|
+
const firstCard = cards?.[0] || {};
|
|
1253
|
+
const cardForCarouselStatus = {
|
|
1254
|
+
...firstCard,
|
|
1255
|
+
Status:
|
|
1256
|
+
firstCard.Status
|
|
1257
|
+
?? firstCard.status
|
|
1258
|
+
?? firstCard.approvalStatus
|
|
1259
|
+
?? get(details, 'templateStatus')
|
|
1260
|
+
?? get(details, 'approvalStatus')
|
|
1261
|
+
?? get(details, 'creativeStatus')
|
|
1262
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1263
|
+
?? '',
|
|
1264
|
+
};
|
|
1265
|
+
templateStatusHelper(cardForCarouselStatus);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Hydrate SMS fallback for carousel — same logic as for non-carousel types below.
|
|
1269
|
+
// Without this the early `return` skips fallback hydration entirely, so reopening
|
|
1270
|
+
// a carousel template never restores the saved SMS fallback data.
|
|
1271
|
+
applySmsFallbackHydration({
|
|
1272
|
+
details,
|
|
1273
|
+
isFullMode,
|
|
1274
|
+
lastSmsFallbackHydrationKeyRef,
|
|
1275
|
+
setSmsFallbackData,
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const mediaType =
|
|
1282
|
+
card0.mediaType
|
|
1283
|
+
|| get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].mediaType', '');
|
|
1284
|
+
if (cardType.toUpperCase() !== contentType.carousel && mediaType === RCS_MEDIA_TYPES.NONE) {
|
|
595
1285
|
setTemplateType(contentType.text_message);
|
|
596
|
-
} else {
|
|
1286
|
+
} else if (cardType.toUpperCase() !== contentType.carousel && mediaType !== RCS_MEDIA_TYPES.NONE) {
|
|
597
1287
|
setTemplateType(contentType.rich_card);
|
|
598
1288
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1289
|
+
|
|
1290
|
+
const loadedTitle = loadedTitleForMap;
|
|
1291
|
+
const loadedDesc = loadedDescForMap;
|
|
1292
|
+
const { normalizedTitle, normalizedDesc } = normalizeLibraryLoadedTitleDesc({
|
|
1293
|
+
loadedTitle,
|
|
1294
|
+
loadedDesc,
|
|
1295
|
+
isFullMode,
|
|
1296
|
+
cardVarMappedAfterHydration: hydratedCardVarMappedResult,
|
|
1297
|
+
rcsVarRegex,
|
|
1298
|
+
});
|
|
606
1299
|
setTemplateTitle(normalizedTitle);
|
|
607
1300
|
setTemplateDesc(normalizedDesc);
|
|
608
|
-
setSuggestions(
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1301
|
+
setSuggestions(
|
|
1302
|
+
Array.isArray(card0.suggestions)
|
|
1303
|
+
? card0.suggestions
|
|
1304
|
+
: get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].suggestions', []),
|
|
1305
|
+
);
|
|
1306
|
+
const cardForStatus = {
|
|
1307
|
+
...card0,
|
|
1308
|
+
Status:
|
|
1309
|
+
card0.Status
|
|
1310
|
+
?? card0.status
|
|
1311
|
+
?? card0.approvalStatus
|
|
1312
|
+
?? get(details, 'templateStatus')
|
|
1313
|
+
?? get(details, 'approvalStatus')
|
|
1314
|
+
?? get(details, 'creativeStatus')
|
|
1315
|
+
?? get(details, 'versions.base.content.RCS.templateApprovalStatus')
|
|
1316
|
+
?? '',
|
|
1317
|
+
};
|
|
1318
|
+
if (isHostInfoBip) {
|
|
1319
|
+
setTemplateStatus('');
|
|
1320
|
+
} else {
|
|
1321
|
+
templateStatusHelper(cardForStatus);
|
|
1322
|
+
}
|
|
1323
|
+
const mediaData =
|
|
1324
|
+
card0.media != null && card0.media !== ''
|
|
1325
|
+
? card0.media
|
|
1326
|
+
: get(details, 'versions.base.content.RCS.rcsContent.cardContent[0].media', '');
|
|
1327
|
+
const cardSettings =
|
|
1328
|
+
get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
|
|
1329
|
+
|| get(details, 'rcsContent.cardSettings', '');
|
|
612
1330
|
setMediaData(mediaData, mediaType, cardSettings);
|
|
613
1331
|
if (details?.edit) {
|
|
614
1332
|
const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
|
|
615
1333
|
setRcsAccount(rcsAccountId);
|
|
616
1334
|
}
|
|
1335
|
+
|
|
1336
|
+
applySmsFallbackHydration({
|
|
1337
|
+
details,
|
|
1338
|
+
isFullMode,
|
|
1339
|
+
lastSmsFallbackHydrationKeyRef,
|
|
1340
|
+
setSmsFallbackData,
|
|
1341
|
+
});
|
|
617
1342
|
}
|
|
618
|
-
}, [
|
|
619
|
-
|
|
1343
|
+
}, [rcsHydrationDetails, isFullMode, isHostInfoBip]);
|
|
620
1344
|
|
|
621
1345
|
useEffect(() => {
|
|
622
1346
|
if (templateType === contentType.text_message) {
|
|
623
1347
|
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
624
|
-
|
|
625
|
-
|
|
1348
|
+
// Full-mode create only: switching to plain text clears draft title/media. Never clear when
|
|
1349
|
+
// hydrating library/edit (would wipe templateData after load) — regression seen after SMS fallback work.
|
|
626
1350
|
if (!isEditFlow && isFullMode) {
|
|
1351
|
+
setTemplateTitle('');
|
|
1352
|
+
setTemplateTitleError('');
|
|
627
1353
|
setUpdateRcsImageSrc('');
|
|
628
|
-
setUpdateRcsVideoSrc({});
|
|
629
1354
|
setRcsVideoSrc({});
|
|
630
1355
|
setSelectedDimension(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
|
|
631
1356
|
}
|
|
@@ -649,23 +1374,22 @@ export const Rcs = (props) => {
|
|
|
649
1374
|
}, [templateType, isEditFlow, isFullMode]);
|
|
650
1375
|
|
|
651
1376
|
useEffect(() => {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
666
|
-
globalActions.fetchSchemaForEntity(query);
|
|
1377
|
+
const { type, module } = location.query || {};
|
|
1378
|
+
const isEmbedded = type === EMBEDDED;
|
|
1379
|
+
// Match TagList initial fetch (getTagsforContext('Outbound') → context "outbound") so we do not request a different context than the two TagList headers.
|
|
1380
|
+
const context = isEmbedded ? module : 'outbound';
|
|
1381
|
+
const embedded = isEmbedded ? type : FULL;
|
|
1382
|
+
const query = {
|
|
1383
|
+
layout: SMS,
|
|
1384
|
+
type: TAG,
|
|
1385
|
+
context,
|
|
1386
|
+
embedded,
|
|
1387
|
+
};
|
|
1388
|
+
if (getDefaultTags) {
|
|
1389
|
+
query.context = getDefaultTags;
|
|
667
1390
|
}
|
|
668
|
-
|
|
1391
|
+
fetchTagSchemaIfNewQuery(query);
|
|
1392
|
+
}, [fetchTagSchemaIfNewQuery]);
|
|
669
1393
|
|
|
670
1394
|
useEffect(() => {
|
|
671
1395
|
let tag = get(metaEntities, `tags.standard`, []);
|
|
@@ -688,16 +1412,128 @@ export const Rcs = (props) => {
|
|
|
688
1412
|
context,
|
|
689
1413
|
embedded,
|
|
690
1414
|
};
|
|
1415
|
+
if (getDefaultTags) {
|
|
1416
|
+
query.context = getDefaultTags;
|
|
1417
|
+
}
|
|
1418
|
+
fetchTagSchemaIfNewQuery(query);
|
|
691
1419
|
globalActions.fetchSchemaForEntity(query);
|
|
692
1420
|
};
|
|
693
1421
|
|
|
694
|
-
const
|
|
695
|
-
if (!
|
|
696
|
-
const
|
|
1422
|
+
const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
|
|
1423
|
+
if (!templateStr || !numericVarName || !tagName) return templateStr;
|
|
1424
|
+
const placeholderRegex = buildRcsNumericMustachePlaceholderRegex(numericVarName);
|
|
1425
|
+
return templateStr.replace(placeholderRegex, `{{${tagName}}}`);
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
|
|
1429
|
+
if (!varSegmentCompositeDomId) return;
|
|
1430
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
1431
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
1432
|
+
const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
|
|
1433
|
+
if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
|
|
1434
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
1435
|
+
const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
1436
|
+
if (!semanticOrNumericVarName) return;
|
|
1437
|
+
const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
|
|
1438
|
+
const templateStringForField =
|
|
1439
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
|
|
1440
|
+
const titleOrMessageFieldType =
|
|
1441
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
|
|
1442
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
1443
|
+
varSegmentCompositeDomId,
|
|
1444
|
+
templateStringForField,
|
|
1445
|
+
titleOrMessageFieldType,
|
|
1446
|
+
);
|
|
1447
|
+
const cardVarMappedNumericSlotKey =
|
|
1448
|
+
globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
|
|
1449
|
+
? String(globalVarSlotIndexZeroBased + 1)
|
|
1450
|
+
: null;
|
|
1451
|
+
|
|
1452
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
1453
|
+
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
1454
|
+
if (isNumericPlaceholderSlot) {
|
|
1455
|
+
const existingValueBeforeAppend = (
|
|
1456
|
+
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
1457
|
+
).toString();
|
|
1458
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1459
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
1460
|
+
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
1461
|
+
} else {
|
|
1462
|
+
// Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
|
|
1463
|
+
// "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
|
|
1464
|
+
// read/write the global numeric slot only and drop the shared semantic key.
|
|
1465
|
+
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
1466
|
+
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
1467
|
+
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
1468
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1469
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
1470
|
+
if (cardVarMappedNumericSlotKey) {
|
|
1471
|
+
updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
|
|
1472
|
+
// Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
|
|
1473
|
+
const fieldOffset = titleOrMessageFieldType === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
1474
|
+
const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
|
|
1475
|
+
let varOrdinal = 0;
|
|
1476
|
+
for (let i = 0; i < fieldSegments.length; i++) {
|
|
1477
|
+
if (rcsVarTestRegex.test(fieldSegments[i])) {
|
|
1478
|
+
const slotVarName = getVarNameFromToken(fieldSegments[i]);
|
|
1479
|
+
const globalSlot = fieldOffset + varOrdinal;
|
|
1480
|
+
varOrdinal += 1;
|
|
1481
|
+
if (slotVarName === semanticOrNumericVarName && globalSlot !== globalVarSlotIndexZeroBased) {
|
|
1482
|
+
updatedCardVarMapped[String(globalSlot + 1)] = mappedValueAfterAppendingTag;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
} else {
|
|
1487
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return updatedCardVarMapped;
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
if (
|
|
1494
|
+
isNumericPlaceholderSlot
|
|
1495
|
+
&& (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
|
|
1496
|
+
) {
|
|
1497
|
+
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
1498
|
+
setTemplateTitle((previousTitle) => {
|
|
1499
|
+
const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
1500
|
+
previousTitle || '',
|
|
1501
|
+
semanticOrNumericVarName,
|
|
1502
|
+
selectedTagNameFromPicker,
|
|
1503
|
+
);
|
|
1504
|
+
if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
|
|
1505
|
+
setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
|
|
1506
|
+
// 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
|
|
1507
|
+
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1508
|
+
return titleAfterReplacingNumericPlaceholder;
|
|
1509
|
+
});
|
|
1510
|
+
} else {
|
|
1511
|
+
setTemplateDesc((previousDescription) => {
|
|
1512
|
+
const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
1513
|
+
previousDescription || '',
|
|
1514
|
+
semanticOrNumericVarName,
|
|
1515
|
+
selectedTagNameFromPicker,
|
|
1516
|
+
);
|
|
1517
|
+
if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
|
|
1518
|
+
return previousDescription;
|
|
1519
|
+
}
|
|
1520
|
+
setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
|
|
1521
|
+
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1522
|
+
return descriptionAfterReplacingNumericPlaceholder;
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
const onTitleTagSelect = (tagName) => onTagSelect(tagName, titleTextAreaId, RCS_TAG_AREA_FIELD_TITLE);
|
|
1529
|
+
|
|
1530
|
+
const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
|
|
1531
|
+
|
|
1532
|
+
const onCarouselTagSelect = (data) => {
|
|
1533
|
+
if (!carouselFocusedVarId) return;
|
|
1534
|
+
const sep = carouselFocusedVarId.lastIndexOf('_');
|
|
697
1535
|
if (sep === -1) return;
|
|
698
|
-
const
|
|
699
|
-
if (isNaN(numId)) return;
|
|
700
|
-
const token = areaId.slice(0, sep);
|
|
1536
|
+
const token = carouselFocusedVarId.slice(0, sep);
|
|
701
1537
|
const variableName = getVarNameFromToken(token);
|
|
702
1538
|
if (!variableName) return;
|
|
703
1539
|
setCardVarMapped((prev) => {
|
|
@@ -710,18 +1546,6 @@ export const Rcs = (props) => {
|
|
|
710
1546
|
});
|
|
711
1547
|
};
|
|
712
1548
|
|
|
713
|
-
const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
|
|
714
|
-
|
|
715
|
-
const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
|
|
716
|
-
|
|
717
|
-
const onTagSelectFallback = (data) => {
|
|
718
|
-
const tempMsg = `${fallbackMessage}{{${data}}}`;
|
|
719
|
-
const error = fallbackMessageErrorHandler(tempMsg);
|
|
720
|
-
setFallbackMessage(tempMsg);
|
|
721
|
-
setFallbackMessageError(error);
|
|
722
|
-
};
|
|
723
|
-
|
|
724
|
-
|
|
725
1549
|
//removing optout tag for rcs
|
|
726
1550
|
const getRcsTags = () => {
|
|
727
1551
|
const tempTags = cloneDeep(tags);
|
|
@@ -733,15 +1557,14 @@ export const Rcs = (props) => {
|
|
|
733
1557
|
};
|
|
734
1558
|
// tag Code end
|
|
735
1559
|
|
|
736
|
-
const renderLabel = (value,
|
|
737
|
-
const isTemplateApproved = (templateStatus === RCS_STATUSES.approved);
|
|
1560
|
+
const renderLabel = (value, desc) => {
|
|
738
1561
|
return (
|
|
739
1562
|
<>
|
|
740
|
-
<
|
|
1563
|
+
<div className="rcs-form-section-heading">
|
|
741
1564
|
<CapHeading type="h4">{formatMessage(messages[value])}</CapHeading>
|
|
742
|
-
</
|
|
1565
|
+
</div>
|
|
743
1566
|
{desc && (
|
|
744
|
-
<CapLabel type="label3"
|
|
1567
|
+
<CapLabel type="label3" className="rcs-form-field-caption">
|
|
745
1568
|
{formatMessage(messages[desc])}
|
|
746
1569
|
</CapLabel>
|
|
747
1570
|
)}
|
|
@@ -760,13 +1583,8 @@ export const Rcs = (props) => {
|
|
|
760
1583
|
},
|
|
761
1584
|
{
|
|
762
1585
|
value: contentType.carousel,
|
|
763
|
-
label: (
|
|
764
|
-
|
|
765
|
-
{formatMessage(messages.carousel)}
|
|
766
|
-
</CapTooltip>
|
|
767
|
-
),
|
|
768
|
-
disabled: true,
|
|
769
|
-
},
|
|
1586
|
+
label: formatMessage(messages.carousel),
|
|
1587
|
+
}
|
|
770
1588
|
];
|
|
771
1589
|
|
|
772
1590
|
const onTemplateNameChange = ({ target: { value } }) => {
|
|
@@ -777,6 +1595,10 @@ export const Rcs = (props) => {
|
|
|
777
1595
|
|
|
778
1596
|
const onTemplateTypeChange = ({ target: { value } }) => {
|
|
779
1597
|
setTemplateType(value);
|
|
1598
|
+
// Carousel has per-card media; keep template-level media type neutral.
|
|
1599
|
+
if (value === contentType.carousel) {
|
|
1600
|
+
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1601
|
+
}
|
|
780
1602
|
};
|
|
781
1603
|
|
|
782
1604
|
|
|
@@ -797,8 +1619,39 @@ export const Rcs = (props) => {
|
|
|
797
1619
|
const onTemplateMediaTypeChange = ({ target: { value } }) => {
|
|
798
1620
|
setTemplateMediaType(value);
|
|
799
1621
|
};
|
|
800
|
-
|
|
801
|
-
|
|
1622
|
+
const renderedRCSEditMessage = (descArray, type) => {
|
|
1623
|
+
const renderArray = [];
|
|
1624
|
+
if (descArray?.length) {
|
|
1625
|
+
descArray.forEach((elem, index) => {
|
|
1626
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
1627
|
+
// Variable input
|
|
1628
|
+
renderArray.push(
|
|
1629
|
+
<TextArea
|
|
1630
|
+
id={`${elem}_${index}`}
|
|
1631
|
+
key={`${elem}_${index}`}
|
|
1632
|
+
placeholder={`enter the value for ${elem}`}
|
|
1633
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1634
|
+
onChange={e => textAreaValueChange(e, type)}
|
|
1635
|
+
value={textAreaValue(index, type)}
|
|
1636
|
+
onFocus={(e) => setTextAreaId(e, type)}
|
|
1637
|
+
/>
|
|
1638
|
+
);
|
|
1639
|
+
} else if (elem) {
|
|
1640
|
+
// Static text
|
|
1641
|
+
renderArray.push(
|
|
1642
|
+
<TextArea
|
|
1643
|
+
key={`static_${index}`}
|
|
1644
|
+
value={elem}
|
|
1645
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1646
|
+
disabled
|
|
1647
|
+
className="rcs-edit-template-message-static-textarea"
|
|
1648
|
+
/>
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
return renderArray;
|
|
1654
|
+
};
|
|
802
1655
|
const onTemplateTitleChange = ({ target: { value } }) => {
|
|
803
1656
|
let errorMessage = false;
|
|
804
1657
|
if (templateType === contentType.rich_card && !value.trim()) {
|
|
@@ -814,7 +1667,7 @@ export const Rcs = (props) => {
|
|
|
814
1667
|
|
|
815
1668
|
const onTemplateDescChange = ({ target: { value } }) => {
|
|
816
1669
|
let errorMessage = false;
|
|
817
|
-
if(templateType === contentType.text_message && value?.length > RCS_TEXT_MESSAGE_MAX_LENGTH){
|
|
1670
|
+
if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
|
|
818
1671
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
819
1672
|
} else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
|
|
820
1673
|
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
@@ -830,16 +1683,16 @@ export const Rcs = (props) => {
|
|
|
830
1683
|
|
|
831
1684
|
const templateDescErrorHandler = (value) => {
|
|
832
1685
|
let errorMessage = false;
|
|
833
|
-
const { isBraceError } = validateTags({
|
|
1686
|
+
const { unsupportedTags, isBraceError } = validateTags({
|
|
834
1687
|
content: value,
|
|
835
1688
|
tagsParam: tags,
|
|
1689
|
+
injectedTagsParams: injectedTags,
|
|
836
1690
|
location,
|
|
837
1691
|
tagModule: getDefaultTags,
|
|
838
|
-
isFullMode,
|
|
839
1692
|
}) || {};
|
|
840
1693
|
|
|
841
1694
|
const maxLength = templateType === contentType.text_message
|
|
842
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH
|
|
1695
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)
|
|
843
1696
|
: RCS_RICH_CARD_MAX_LENGTH;
|
|
844
1697
|
|
|
845
1698
|
if (value === '' && isMediaTypeText) {
|
|
@@ -855,19 +1708,6 @@ export const Rcs = (props) => {
|
|
|
855
1708
|
};
|
|
856
1709
|
|
|
857
1710
|
|
|
858
|
-
const onFallbackMessageChange = ({ target: { value } }) => {
|
|
859
|
-
const error = fallbackMessageErrorHandler(value);
|
|
860
|
-
setFallbackMessage(value);
|
|
861
|
-
setFallbackMessageError(error);
|
|
862
|
-
};
|
|
863
|
-
|
|
864
|
-
const fallbackMessageErrorHandler = (value) => {
|
|
865
|
-
if (value?.length > FALLBACK_MESSAGE_MAX_LENGTH) {
|
|
866
|
-
return formatMessage(messages.fallbackMsgLenError);
|
|
867
|
-
}
|
|
868
|
-
return false;
|
|
869
|
-
};
|
|
870
|
-
|
|
871
1711
|
// Check for forbidden characters: square brackets [] and single curly braces {}
|
|
872
1712
|
const forbiddenCharactersValidation = (value) => {
|
|
873
1713
|
if (!value) return false;
|
|
@@ -912,53 +1752,43 @@ export const Rcs = (props) => {
|
|
|
912
1752
|
if(!isFullMode){
|
|
913
1753
|
return false;
|
|
914
1754
|
}
|
|
915
|
-
|
|
1755
|
+
// Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
|
|
1756
|
+
if (!/^[\w.]+$/.test(paramName)) {
|
|
916
1757
|
return formatMessage(messages.unknownCharactersError);
|
|
917
1758
|
}
|
|
918
1759
|
}
|
|
919
1760
|
return false;
|
|
920
1761
|
};
|
|
921
|
-
|
|
922
|
-
const templateHeaderErrorHandler = (value) => {
|
|
923
|
-
let errorMessage = false;
|
|
924
|
-
if (value?.length > TEMPLATE_HEADER_MAX_LENGTH) {
|
|
925
|
-
errorMessage = formatMessage(messages.templateHeaderLengthError);
|
|
926
|
-
} else {
|
|
927
|
-
errorMessage = variableErrorHandling(value);
|
|
928
|
-
}
|
|
929
|
-
return errorMessage;
|
|
930
|
-
};
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const templateMessageErrorHandler = (value) => {
|
|
934
|
-
let errorMessage = false;
|
|
935
|
-
if (value === '') {
|
|
936
|
-
errorMessage = formatMessage(messages.emptyTemplateMessageErrorMessage);
|
|
937
|
-
} else if (
|
|
938
|
-
value?.length
|
|
939
|
-
> TEMPLATE_MESSAGE_MAX_LENGTH
|
|
940
|
-
) {
|
|
941
|
-
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
942
|
-
} else {
|
|
943
|
-
errorMessage = variableErrorHandling(value);
|
|
944
|
-
}
|
|
945
|
-
return errorMessage;
|
|
946
|
-
};
|
|
947
|
-
|
|
948
1762
|
|
|
949
1763
|
const onMessageAddVar = () => {
|
|
950
|
-
onAddVar(
|
|
1764
|
+
onAddVar(templateDesc);
|
|
951
1765
|
};
|
|
952
1766
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1767
|
+
/**
|
|
1768
|
+
* Returns the smallest positive integer not already used as a `{{N}}` variable
|
|
1769
|
+
* in either the title or description fields, or null if the limit (19) is reached.
|
|
1770
|
+
* Scans both fields so title and description vars never share the same number
|
|
1771
|
+
* (duplicate numbers would share a cardVarMapped key and bleed values across fields).
|
|
1772
|
+
*/
|
|
1773
|
+
const getNextRcsNumericVarNumber = (titleStr, descStr) => {
|
|
1774
|
+
const allExistingVars = [
|
|
1775
|
+
...(titleStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
|
|
1776
|
+
...(descStr.match(RCS_NUMERIC_VAR_TOKEN_REGEX) || []),
|
|
1777
|
+
];
|
|
1778
|
+
const existingNumbers = allExistingVars.flatMap(varToken => {
|
|
1779
|
+
const numericMatch = varToken.match(/\d+/);
|
|
1780
|
+
return numericMatch ? [parseInt(numericMatch[0], 10)] : [];
|
|
1781
|
+
});
|
|
957
1782
|
let nextNumber = 1;
|
|
958
1783
|
while (existingNumbers.includes(nextNumber)) {
|
|
959
1784
|
nextNumber++;
|
|
960
1785
|
}
|
|
961
|
-
|
|
1786
|
+
return nextNumber > 19 ? null : nextNumber;
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const onAddVar = (messageContent) => {
|
|
1790
|
+
const nextNumber = getNextRcsNumericVarNumber(templateTitle, messageContent);
|
|
1791
|
+
if (nextNumber === null) {
|
|
962
1792
|
return;
|
|
963
1793
|
}
|
|
964
1794
|
const nextVar = `{{${nextNumber}}}`;
|
|
@@ -970,15 +1800,13 @@ const onAddVar = (type, messageContent, regex) => {
|
|
|
970
1800
|
};
|
|
971
1801
|
|
|
972
1802
|
const onTitleAddVar = () => {
|
|
973
|
-
//
|
|
1803
|
+
// Scan both title AND description so the new title var number doesn't
|
|
1804
|
+
// duplicate a number already used in the description. Duplicate numeric
|
|
1805
|
+
// names would share the same cardVarMapped semantic key, causing the
|
|
1806
|
+
// description slot to reflect the title slot value and vice-versa.
|
|
974
1807
|
const messageContent = templateTitle;
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
let nextNumber = 1;
|
|
978
|
-
while (existingNumbers.includes(nextNumber)) {
|
|
979
|
-
nextNumber++;
|
|
980
|
-
}
|
|
981
|
-
if (nextNumber > 19) {
|
|
1808
|
+
const nextNumber = getNextRcsNumericVarNumber(templateTitle, templateDesc);
|
|
1809
|
+
if (nextNumber === null) {
|
|
982
1810
|
return;
|
|
983
1811
|
}
|
|
984
1812
|
const nextVar = `{{${nextNumber}}}`;
|
|
@@ -990,26 +1818,17 @@ const onTitleAddVar = () => {
|
|
|
990
1818
|
setTemplateTitleError(error);
|
|
991
1819
|
};
|
|
992
1820
|
|
|
993
|
-
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
content = content.substring(index + validVarArr?.[0]?.length, content?.length);
|
|
1005
|
-
validVarArr?.shift();
|
|
1006
|
-
} else {
|
|
1007
|
-
templateVarArray.push(content);
|
|
1008
|
-
break;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return templateVarArray.filter(Boolean);
|
|
1012
|
-
};
|
|
1821
|
+
const appendVarToCarouselField = (cardIndex, fieldName) => {
|
|
1822
|
+
const token = getNextCarouselVarToken(carouselData);
|
|
1823
|
+
if (!token) return;
|
|
1824
|
+
setCarouselData((prev = []) => {
|
|
1825
|
+
const updated = cloneDeep(prev);
|
|
1826
|
+
if (!updated[cardIndex]) return prev;
|
|
1827
|
+
const current = (updated[cardIndex][fieldName] || '').toString();
|
|
1828
|
+
updated[cardIndex][fieldName] = `${current}${token}`;
|
|
1829
|
+
return updated;
|
|
1830
|
+
});
|
|
1831
|
+
};
|
|
1013
1832
|
|
|
1014
1833
|
const textAreaValue = (idValue, type) => {
|
|
1015
1834
|
if (idValue >= 0) {
|
|
@@ -1025,6 +1844,46 @@ const splitTemplateVarString = (str) => {
|
|
|
1025
1844
|
return "";
|
|
1026
1845
|
};
|
|
1027
1846
|
|
|
1847
|
+
// Carousel: render variable-value editor for a given template string (title/description).
|
|
1848
|
+
// This matches rich-card/text edit behavior: static pieces are read-only, variable tokens are editable.
|
|
1849
|
+
const renderCarouselEditMessage = (templateStr) => {
|
|
1850
|
+
const renderArray = [];
|
|
1851
|
+
const templateArr = splitTemplateVarString(templateStr);
|
|
1852
|
+
if (templateArr?.length) {
|
|
1853
|
+
templateArr.forEach((elem, index) => {
|
|
1854
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
1855
|
+
const varName = getVarNameFromToken(elem);
|
|
1856
|
+
renderArray.push(
|
|
1857
|
+
<div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
|
|
1858
|
+
<TextArea
|
|
1859
|
+
id={`${elem}_${index}`}
|
|
1860
|
+
placeholder={`enter the value for ${elem}`}
|
|
1861
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1862
|
+
onChange={(e) => textAreaValueChange(e, TITLE_TEXT)}
|
|
1863
|
+
value={varName ? ((cardVarMapped?.[varName] ?? '').toString()) : ''}
|
|
1864
|
+
onFocus={(e) => {
|
|
1865
|
+
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1866
|
+
setCarouselFocusedVarId(id);
|
|
1867
|
+
}}
|
|
1868
|
+
/>
|
|
1869
|
+
</div>
|
|
1870
|
+
);
|
|
1871
|
+
} else if (elem) {
|
|
1872
|
+
renderArray.push(
|
|
1873
|
+
<CapHeading
|
|
1874
|
+
key={`static_${index}`}
|
|
1875
|
+
type="h4"
|
|
1876
|
+
className="rcs-edit-template-message-split"
|
|
1877
|
+
>
|
|
1878
|
+
{elem}
|
|
1879
|
+
</CapHeading>
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1028
1887
|
const textAreaValueChange = (e, type) => {
|
|
1029
1888
|
const value = e?.target?.value ?? '';
|
|
1030
1889
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
@@ -1044,7 +1903,9 @@ const splitTemplateVarString = (str) => {
|
|
|
1044
1903
|
};
|
|
1045
1904
|
|
|
1046
1905
|
const setTextAreaId = (e, type) => {
|
|
1047
|
-
|
|
1906
|
+
// VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
|
|
1907
|
+
// have an `.target.id` shape. Support both.
|
|
1908
|
+
const id = typeof e === 'string' ? e : (e?.target?.id || e?.currentTarget?.id || '');
|
|
1048
1909
|
if (!id) return;
|
|
1049
1910
|
if (type === TITLE_TEXT) setTitleTextAreaId(id);
|
|
1050
1911
|
else setDescTextAreaId(id);
|
|
@@ -1063,10 +1924,12 @@ const splitTemplateVarString = (str) => {
|
|
|
1063
1924
|
</CapRow>
|
|
1064
1925
|
}
|
|
1065
1926
|
description={
|
|
1066
|
-
|
|
1927
|
+
!isEditFlow && (
|
|
1928
|
+
<CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
|
|
1929
|
+
)
|
|
1067
1930
|
}
|
|
1068
1931
|
/>
|
|
1069
|
-
|
|
1932
|
+
|
|
1070
1933
|
<CapActionButton
|
|
1071
1934
|
buttonType={buttonType}
|
|
1072
1935
|
updateButtonChange={updateButtonChange}
|
|
@@ -1075,44 +1938,85 @@ const splitTemplateVarString = (str) => {
|
|
|
1075
1938
|
isEditFlow={isEditFlow}
|
|
1076
1939
|
isFullMode={isFullMode}
|
|
1077
1940
|
maxButtons={MAX_BUTTONS}
|
|
1078
|
-
|
|
1941
|
+
host={hostName}
|
|
1942
|
+
/>
|
|
1079
1943
|
</>
|
|
1080
|
-
);
|
|
1081
|
-
};
|
|
1082
|
-
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1944
|
+
);
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
const getRcsValueMap = (fieldTemplateString, fieldType) => {
|
|
1948
|
+
if (!fieldTemplateString) return {};
|
|
1949
|
+
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
1950
|
+
const slotOffset = fieldType === TITLE_TEXT ? 0 : titleVarTokenMatches.length;
|
|
1951
|
+
const templateSegments = splitTemplateVarStringRcs(fieldTemplateString);
|
|
1952
|
+
const segmentIdToResolvedValue = {};
|
|
1953
|
+
let varOrdinal = 0;
|
|
1954
|
+
templateSegments.forEach((segmentToken, segmentIndexInField) => {
|
|
1955
|
+
if (rcsVarTestRegex.test(segmentToken)) {
|
|
1956
|
+
const varSegmentCompositeId = `${segmentToken}_${segmentIndexInField}`;
|
|
1957
|
+
const varName = getVarNameFromToken(segmentToken);
|
|
1958
|
+
const globalSlot = slotOffset + varOrdinal;
|
|
1959
|
+
varOrdinal += 1;
|
|
1960
|
+
segmentIdToResolvedValue[varSegmentCompositeId] = resolveCardVarMappedSlotValue(
|
|
1961
|
+
cardVarMapped,
|
|
1962
|
+
varName,
|
|
1963
|
+
globalSlot,
|
|
1964
|
+
isEditLike,
|
|
1965
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
return segmentIdToResolvedValue;
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
const titleVarSegmentValueMapById = useMemo(
|
|
1973
|
+
() => getRcsValueMap(templateTitle, TITLE_TEXT),
|
|
1974
|
+
[templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
1975
|
+
);
|
|
1976
|
+
const descriptionVarSegmentValueMapById = useMemo(
|
|
1977
|
+
() => getRcsValueMap(templateDesc, MESSAGE_TEXT),
|
|
1978
|
+
[templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
1979
|
+
);
|
|
1980
|
+
|
|
1981
|
+
const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
|
|
1982
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
1983
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
1984
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
1985
|
+
const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
1986
|
+
if (variableName === undefined || variableName === null || variableName === '') return;
|
|
1987
|
+
const isInvalidValue = value?.trim() === '';
|
|
1988
|
+
const coercedSlotValue = isInvalidValue ? '' : value;
|
|
1989
|
+
const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
1990
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
1991
|
+
varSegmentCompositeDomId,
|
|
1992
|
+
templateStringForField,
|
|
1993
|
+
type,
|
|
1994
|
+
);
|
|
1995
|
+
const fieldOffset = type === TITLE_TEXT ? 0 : (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
1996
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
1997
|
+
const updatedCardVarMapped = { ...previousCardVarMapped };
|
|
1998
|
+
// Remove stale semantic key: keeping it causes every other slot sharing the same
|
|
1999
|
+
// variable name (e.g. {{adv}} in both title and description) to read the same value
|
|
2000
|
+
// via the semantic-key fallback in resolveCardVarMappedSlotValue.
|
|
2001
|
+
delete updatedCardVarMapped[variableName];
|
|
2002
|
+
if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
|
|
2003
|
+
updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
|
|
2004
|
+
// Sync all other intra-field occurrences of the same variable (cross-field slots stay independent)
|
|
2005
|
+
const fieldSegments = splitTemplateVarStringRcs(templateStringForField ?? '');
|
|
2006
|
+
let varOrdinal = 0;
|
|
2007
|
+
for (let i = 0; i < fieldSegments.length; i++) {
|
|
2008
|
+
if (rcsVarTestRegex.test(fieldSegments[i])) {
|
|
2009
|
+
const slotVarName = getVarNameFromToken(fieldSegments[i]);
|
|
2010
|
+
const globalSlot = fieldOffset + varOrdinal;
|
|
2011
|
+
varOrdinal += 1;
|
|
2012
|
+
if (slotVarName === variableName && globalSlot !== globalVarSlotIndexZeroBased) {
|
|
2013
|
+
updatedCardVarMapped[String(globalSlot + 1)] = coercedSlotValue;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
1112
2016
|
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
|
|
2017
|
+
}
|
|
2018
|
+
return updatedCardVarMapped;
|
|
2019
|
+
});
|
|
1116
2020
|
};
|
|
1117
2021
|
|
|
1118
2022
|
const renderTextComponent = () => {
|
|
@@ -1132,6 +2036,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1132
2036
|
}
|
|
1133
2037
|
suffix={
|
|
1134
2038
|
<>
|
|
2039
|
+
|
|
1135
2040
|
{(isEditFlow || !isFullMode) ? (
|
|
1136
2041
|
<TagList
|
|
1137
2042
|
label={formatMessage(globalMessages.addLabels)}
|
|
@@ -1148,18 +2053,28 @@ const splitTemplateVarString = (str) => {
|
|
|
1148
2053
|
type="flat"
|
|
1149
2054
|
isAddBtn
|
|
1150
2055
|
onClick={onTitleAddVar}
|
|
1151
|
-
disabled={
|
|
2056
|
+
disabled={templateTitleError}
|
|
1152
2057
|
>
|
|
1153
2058
|
{formatMessage(messages.addVar)}
|
|
1154
2059
|
</CapButton>
|
|
1155
|
-
|
|
2060
|
+
)}
|
|
1156
2061
|
</>
|
|
1157
|
-
}
|
|
2062
|
+
}
|
|
2063
|
+
/>
|
|
2064
|
+
|
|
2065
|
+
{(isEditFlow || !isFullMode) ? (
|
|
2066
|
+
<VarSegmentMessageEditor
|
|
2067
|
+
key={`rcs-title-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
2068
|
+
templateString={templateTitle}
|
|
2069
|
+
valueMap={titleVarSegmentValueMapById}
|
|
2070
|
+
onChange={(id, value) => handleRcsVarChange(id, value, TITLE_TEXT)}
|
|
2071
|
+
onFocus={(id) => setTitleTextAreaId(id)}
|
|
2072
|
+
varRegex={rcsVarRegex}
|
|
2073
|
+
placeholderPrefix=""
|
|
2074
|
+
getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
|
|
1158
2075
|
/>
|
|
1159
|
-
<div className="rcs_text_area_wrapper">
|
|
1160
|
-
{(isEditFlow || !isFullMode) ? (
|
|
1161
|
-
renderedRCSEditMessage(splitTemplateVarString(templateTitle), TITLE_TEXT)
|
|
1162
2076
|
) : (
|
|
2077
|
+
<div className="rcs_text_area_wrapper">
|
|
1163
2078
|
<CapInput
|
|
1164
2079
|
className={`rcs-template-title-input ${
|
|
1165
2080
|
!isTemplateApproved ? "rcs-edit-disabled" : ""
|
|
@@ -1172,8 +2087,8 @@ const splitTemplateVarString = (str) => {
|
|
|
1172
2087
|
errorMessage={templateTitleError}
|
|
1173
2088
|
disabled={isEditFlow || !isFullMode}
|
|
1174
2089
|
/>
|
|
2090
|
+
</div>
|
|
1175
2091
|
)}
|
|
1176
|
-
</div>
|
|
1177
2092
|
{(isEditFlow || !isFullMode) && templateTitleError && (
|
|
1178
2093
|
<CapError className="rcs-template-title-error">
|
|
1179
2094
|
{templateTitleError}
|
|
@@ -1181,7 +2096,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1181
2096
|
)}
|
|
1182
2097
|
{!isEditFlow && isFullMode && renderTitleCharacterCount()}
|
|
1183
2098
|
</>
|
|
1184
|
-
|
|
2099
|
+
)}
|
|
1185
2100
|
|
|
1186
2101
|
{/* Template Message */}
|
|
1187
2102
|
<CapRow useLegacy id="rcs-template-message-label">
|
|
@@ -1219,9 +2134,21 @@ const splitTemplateVarString = (str) => {
|
|
|
1219
2134
|
/>
|
|
1220
2135
|
</CapRow>
|
|
1221
2136
|
<CapRow useLegacy className="rcs-create-template-message-input">
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
2137
|
+
{/* 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. */}
|
|
2138
|
+
<CapRow className="rcs_text_area_wrapper">
|
|
2139
|
+
{(isEditFlow || !isFullMode)?
|
|
2140
|
+
(
|
|
2141
|
+
<VarSegmentMessageEditor
|
|
2142
|
+
key={`rcs-desc-vars-${rcsVarSegmentEditorRemountKey}`}
|
|
2143
|
+
templateString={templateDesc}
|
|
2144
|
+
valueMap={descriptionVarSegmentValueMapById}
|
|
2145
|
+
onChange={(id, value) => handleRcsVarChange(id, value, MESSAGE_TEXT)}
|
|
2146
|
+
onFocus={(id) => setDescTextAreaId(id)}
|
|
2147
|
+
varRegex={rcsVarRegex}
|
|
2148
|
+
placeholderPrefix=""
|
|
2149
|
+
getPlaceholder={() => formatMessage(messages.rcsVarSlotPlaceholder)}
|
|
2150
|
+
/>
|
|
2151
|
+
)
|
|
1225
2152
|
: (
|
|
1226
2153
|
<>
|
|
1227
2154
|
<TextArea
|
|
@@ -1259,13 +2186,15 @@ const splitTemplateVarString = (str) => {
|
|
|
1259
2186
|
</>
|
|
1260
2187
|
)
|
|
1261
2188
|
}
|
|
1262
|
-
</
|
|
2189
|
+
</CapRow>
|
|
1263
2190
|
{(isEditFlow || !isFullMode) && templateDescError && (
|
|
1264
2191
|
<CapError className="rcs-template-message-error">
|
|
1265
2192
|
{templateDescError}
|
|
1266
2193
|
</CapError>
|
|
1267
2194
|
)}
|
|
1268
|
-
{
|
|
2195
|
+
{(isEditFlow || !isFullMode)
|
|
2196
|
+
? renderDescriptionCharacterCount('rcs-character-count rcs-character-count--compact')
|
|
2197
|
+
: (!isEditFlow && isFullMode && renderDescriptionCharacterCount())}
|
|
1269
2198
|
{!isFullMode && hasTag() && (
|
|
1270
2199
|
<CapAlert
|
|
1271
2200
|
message={
|
|
@@ -1279,24 +2208,13 @@ const splitTemplateVarString = (str) => {
|
|
|
1279
2208
|
/>
|
|
1280
2209
|
)}
|
|
1281
2210
|
</CapRow>
|
|
1282
|
-
{
|
|
2211
|
+
{((!isEditFlow && isFullMode) || (isEditFlow && (suggestions?.length ?? 0) > 0)) &&
|
|
2212
|
+
renderButtonComponent()}
|
|
1283
2213
|
</>
|
|
1284
2214
|
|
|
1285
2215
|
);
|
|
1286
2216
|
};
|
|
1287
2217
|
|
|
1288
|
-
|
|
1289
|
-
const fallbackSmsLength = () => (
|
|
1290
|
-
<CapLabel type="label1" className="fallback-sms-length">
|
|
1291
|
-
{formatMessage(messages.totalCharacters, {
|
|
1292
|
-
smsCount: Math.ceil(
|
|
1293
|
-
fallbackMessage?.length / FALLBACK_MESSAGE_MAX_LENGTH,
|
|
1294
|
-
),
|
|
1295
|
-
number: fallbackMessage?.length,
|
|
1296
|
-
})}
|
|
1297
|
-
</CapLabel>
|
|
1298
|
-
);
|
|
1299
|
-
|
|
1300
2218
|
// Get character count for title (rich card only)
|
|
1301
2219
|
const getTitleCharacterCount = () => {
|
|
1302
2220
|
if (templateType === contentType.text_message) return 0;
|
|
@@ -1311,7 +2229,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1311
2229
|
// Get max length for description based on template type
|
|
1312
2230
|
const getDescriptionMaxLength = () => {
|
|
1313
2231
|
return templateType === contentType.text_message
|
|
1314
|
-
? RCS_TEXT_MESSAGE_MAX_LENGTH // 160 for text message
|
|
2232
|
+
? (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH) // 160 for text message
|
|
1315
2233
|
: RCS_RICH_CARD_MAX_LENGTH; // 2000 for rich card description
|
|
1316
2234
|
};
|
|
1317
2235
|
|
|
@@ -1374,287 +2292,34 @@ const splitTemplateVarString = (str) => {
|
|
|
1374
2292
|
return false;
|
|
1375
2293
|
};
|
|
1376
2294
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
base: data?.value?.base || {},
|
|
1402
|
-
},
|
|
1403
|
-
};
|
|
1404
|
-
const fallMsg = get(tempData, `versions.base.updated-sms-editor`, []).join(
|
|
1405
|
-
'',
|
|
1406
|
-
);
|
|
1407
|
-
closeDltContainerHandler();
|
|
1408
|
-
setDltEditData(tempData);
|
|
1409
|
-
setFallbackMessage(fallMsg);
|
|
1410
|
-
setFallbackMessageError(fallMsg?.length > FALLBACK_MESSAGE_MAX_LENGTH);
|
|
1411
|
-
setShowDltCard(true);
|
|
1412
|
-
};
|
|
1413
|
-
|
|
1414
|
-
const rcsDltCardDeleteHandler = () => {
|
|
1415
|
-
closeDltContainerHandler();
|
|
1416
|
-
setDltEditData({});
|
|
1417
|
-
setFallbackMessage('');
|
|
1418
|
-
setFallbackMessageError(false);
|
|
1419
|
-
setShowDltCard(false);
|
|
1420
|
-
};
|
|
1421
|
-
|
|
1422
|
-
const dltFallbackListingPreviewhandler = (data) => {
|
|
1423
|
-
const {
|
|
1424
|
-
'updated-sms-editor': updatedSmsEditor = [],
|
|
1425
|
-
'sms-editor': smsEditor = '',
|
|
1426
|
-
} = data.versions.base || {};
|
|
1427
|
-
setFallbackPreviewmode(true);
|
|
1428
|
-
setDltPreviewData(
|
|
1429
|
-
updatedSmsEditor === '' ? smsEditor : updatedSmsEditor.join(''),
|
|
1430
|
-
);
|
|
1431
|
-
};
|
|
1432
|
-
|
|
1433
|
-
const getDltContentCardList = (content, channel) => {
|
|
1434
|
-
const extra = [
|
|
1435
|
-
<CapIcon
|
|
1436
|
-
type="edit"
|
|
1437
|
-
style={{ marginRight: '8px' }}
|
|
1438
|
-
onClick={() => rcsDltEditSelectHandler(dltEditData)}
|
|
1439
|
-
/>,
|
|
1440
|
-
<CapDropdown
|
|
1441
|
-
overlay={(
|
|
1442
|
-
<CapMenu>
|
|
1443
|
-
<>
|
|
1444
|
-
<CapMenu.Item
|
|
1445
|
-
className="ant-dropdown-menu-item"
|
|
1446
|
-
onClick={() => setFallbackPreviewmode(true)}
|
|
1447
|
-
>
|
|
1448
|
-
{formatMessage(globalMessages.preview)}
|
|
1449
|
-
</CapMenu.Item>
|
|
1450
|
-
<CapMenu.Item
|
|
1451
|
-
className="ant-dropdown-menu-item"
|
|
1452
|
-
onClick={rcsDltCardDeleteHandler}
|
|
1453
|
-
>
|
|
1454
|
-
{formatMessage(globalMessages.delete)}
|
|
1455
|
-
</CapMenu.Item>
|
|
1456
|
-
</>
|
|
1457
|
-
</CapMenu>
|
|
1458
|
-
)}
|
|
1459
|
-
>
|
|
1460
|
-
<CapIcon type="more" />
|
|
1461
|
-
</CapDropdown>,
|
|
1462
|
-
];
|
|
1463
|
-
return {
|
|
1464
|
-
title: channel,
|
|
1465
|
-
content,
|
|
1466
|
-
cardType: channel,
|
|
1467
|
-
extra,
|
|
1468
|
-
};
|
|
1469
|
-
};
|
|
1470
|
-
|
|
1471
|
-
const getDltSlideBoxContent = () => {
|
|
1472
|
-
const loc = {
|
|
1473
|
-
pathname: `/sms/edit`,
|
|
1474
|
-
query: { type: EMBEDDED, module: LIBRARY },
|
|
1475
|
-
search: '',
|
|
1476
|
-
};
|
|
1477
|
-
const route = { name: SMS.toLowerCase() };
|
|
1478
|
-
let dltHeader = '';
|
|
1479
|
-
let dltContent = '';
|
|
1480
|
-
if (dltMode === RCS_DLT_MODE.TEMPLATES) {
|
|
1481
|
-
dltHeader = (
|
|
1482
|
-
<CapHeader
|
|
1483
|
-
title={formatMessage(messages.rcsDltTitle)}
|
|
1484
|
-
description={formatMessage(messages.rcsDltDesc)}
|
|
1485
|
-
/>
|
|
1486
|
-
);
|
|
1487
|
-
dltContent = (
|
|
1488
|
-
<Templates
|
|
1489
|
-
location={loc}
|
|
1490
|
-
route={route}
|
|
1491
|
-
smsRegister={smsRegister}
|
|
1492
|
-
isFullMode={isFullMode}
|
|
1493
|
-
isDltFromRcs
|
|
1494
|
-
onSelectTemplate={rcsDltEditSelectHandler}
|
|
1495
|
-
handlePeviewTemplate={dltFallbackListingPreviewhandler}
|
|
1496
|
-
/>
|
|
1497
|
-
);
|
|
1498
|
-
} else if (dltMode === RCS_DLT_MODE.EDIT) {
|
|
1499
|
-
dltHeader = formatMessage(messages.dltEditHeader);
|
|
1500
|
-
dltContent = (
|
|
1501
|
-
<SmsTraiEdit
|
|
1502
|
-
location={loc}
|
|
1503
|
-
route={route}
|
|
1504
|
-
getDefaultTags="outbound"
|
|
1505
|
-
handleClose={closeDltContainerHandler}
|
|
1506
|
-
templateData={dltEditData}
|
|
1507
|
-
getFormSubscriptionData={rcsDltEditCompletionHandler}
|
|
1508
|
-
isFullMode={false}
|
|
1509
|
-
selectedOfferDetails={selectedOfferDetails}
|
|
1510
|
-
/>
|
|
1511
|
-
);
|
|
1512
|
-
}
|
|
1513
|
-
return { dltHeader, dltContent };
|
|
1514
|
-
};
|
|
1515
|
-
|
|
1516
|
-
const renderFallBackSmsComponent = () => {
|
|
1517
|
-
// Completely disable fallback functionality when DLT is disabled
|
|
1518
|
-
return null;
|
|
1519
|
-
|
|
1520
|
-
// const contentArr = [];
|
|
1521
|
-
// const showAddCreativeBtnForDlt = isDltEnabled && !showDltCard;
|
|
1522
|
-
// const showCardForDlt = isDltEnabled && showDltCard;
|
|
1523
|
-
// const showNonDltFallbackComp = !showAddCreativeBtnForDlt && !showCardForDlt;
|
|
1524
|
-
// //pushing common fallback sms headings
|
|
1525
|
-
// contentArr.push(
|
|
1526
|
-
// <CapRow
|
|
1527
|
-
// style={{
|
|
1528
|
-
// marginBottom: isDltEnabled ? '20px' : '10px',
|
|
1529
|
-
// }}
|
|
1530
|
-
// >
|
|
1531
|
-
// <CapHeader
|
|
1532
|
-
// title={(
|
|
1533
|
-
// <CapRow type="flex">
|
|
1534
|
-
// <CapHeading type="h4">
|
|
1535
|
-
// {formatMessage(messages.fallbackLabel)}
|
|
1536
|
-
// </CapHeading>
|
|
1537
|
-
// <CapTooltipWithInfo
|
|
1538
|
-
// placement="right"
|
|
1539
|
-
// infoIconProps={{
|
|
1540
|
-
// style: { marginLeft: '4px', marginTop: '3px' },
|
|
1541
|
-
// }}
|
|
1542
|
-
// title={formatMessage(messages.fallbackToolTip)}
|
|
1543
|
-
// />
|
|
1544
|
-
// </CapRow>
|
|
1545
|
-
// )}
|
|
1546
|
-
// description={formatMessage(messages.fallbackDesc)}
|
|
1547
|
-
// suffix={
|
|
1548
|
-
// isDltEnabled ? null : (
|
|
1549
|
-
// <CapButton
|
|
1550
|
-
// type="flat"
|
|
1551
|
-
// className="fallback-preview-btn"
|
|
1552
|
-
// prefix={<CapIcon type="eye" />}
|
|
1553
|
-
// style={{ color: CAP_SECONDARY.base }}
|
|
1554
|
-
// onClick={() => setFallbackPreviewmode(true)}
|
|
1555
|
-
// disabled={fallbackMessage === '' || fallbackMessageError}
|
|
1556
|
-
// >
|
|
1557
|
-
// {formatMessage(globalMessages.preview)}
|
|
1558
|
-
// </CapButton>
|
|
1559
|
-
// )
|
|
1560
|
-
// }
|
|
1561
|
-
// />
|
|
1562
|
-
// </CapRow>,
|
|
1563
|
-
// );
|
|
1564
|
-
|
|
1565
|
-
//dlt is enabled, and dlt content is not yet added, show button to add dlt creative
|
|
1566
|
-
// showAddCreativeBtnForDlt
|
|
1567
|
-
// && contentArr.push(
|
|
1568
|
-
// <CapCard className="rcs-dlt-fallback-card">
|
|
1569
|
-
// <CapRow type="flex" justify="center" align="middle">
|
|
1570
|
-
// <CapColumn span={10}>
|
|
1571
|
-
// <CapImage src={addCreativesIcon} />
|
|
1572
|
-
// </CapColumn>
|
|
1573
|
-
// <CapColumn span={14}>
|
|
1574
|
-
// <CapButton
|
|
1575
|
-
// className="add-dlt-btn"
|
|
1576
|
-
// type="secondary"
|
|
1577
|
-
// onClick={addDltMsgHandler}
|
|
1578
|
-
// >
|
|
1579
|
-
// {formatMessage(messages.addSmsCreative)}
|
|
1580
|
-
// </CapButton>
|
|
1581
|
-
// </CapColumn>
|
|
1582
|
-
// </CapRow>
|
|
1583
|
-
// </CapCard>,
|
|
1584
|
-
// );
|
|
1585
|
-
|
|
1586
|
-
// //dlt is enabled and dlt content is added, show it in a card
|
|
1587
|
-
// showCardForDlt
|
|
1588
|
-
// && contentArr.push(
|
|
1589
|
-
// <CapCustomCardList
|
|
1590
|
-
// cardList={[getDltContentCardList(fallbackMessage, SMS)]}
|
|
1591
|
-
// className="rcs-dlt-card"
|
|
1592
|
-
// />,
|
|
1593
|
-
// fallbackMessageError && (
|
|
1594
|
-
// <CapError className="rcs-fallback-len-error">
|
|
1595
|
-
// {formatMessage(messages.fallbackMsgLenError)}
|
|
1596
|
-
// </CapError>
|
|
1597
|
-
// ),
|
|
1598
|
-
// );
|
|
1599
|
-
|
|
1600
|
-
// //dlt is not enabled, show non dlt text area
|
|
1601
|
-
// showNonDltFallbackComp
|
|
1602
|
-
// && contentArr.push(
|
|
1603
|
-
// <>
|
|
1604
|
-
// <CapRow>
|
|
1605
|
-
// <CapHeader
|
|
1606
|
-
// title={(
|
|
1607
|
-
// <CapHeading type="h4">
|
|
1608
|
-
// {formatMessage(messages.fallbackTextAreaLabel)}
|
|
1609
|
-
// </CapHeading>
|
|
1610
|
-
// )}
|
|
1611
|
-
// suffix={(
|
|
1612
|
-
// <TagList
|
|
1613
|
-
// label={formatMessage(globalMessages.addLabels)}
|
|
1614
|
-
// onTagSelect={onTagSelectFallback}
|
|
1615
|
-
// location={location}
|
|
1616
|
-
// tags={tags || []}
|
|
1617
|
-
// onContextChange={handleOnTagsContextChange}
|
|
1618
|
-
// injectedTags={injectedTags || {}}
|
|
1619
|
-
// selectedOfferDetails={selectedOfferDetails}
|
|
1620
|
-
// />
|
|
1621
|
-
// )}
|
|
1622
|
-
// />
|
|
1623
|
-
// </CapRow>
|
|
1624
|
-
// <div className="rcs_fallback_msg_textarea_wrapper">
|
|
1625
|
-
// <TextArea
|
|
1626
|
-
// id="rcs_fallback_message_textarea"
|
|
1627
|
-
// autosize={{ minRows: 3, maxRows: 5 }}
|
|
1628
|
-
// placeholder={formatMessage(messages.fallbackMsgPlaceholder)}
|
|
1629
|
-
// onChange={onFallbackMessageChange}
|
|
1630
|
-
// errorMessage={fallbackMessageError}
|
|
1631
|
-
// value={fallbackMessage || ""}
|
|
1632
|
-
// />
|
|
1633
|
-
// {!aiContentBotDisabled && (
|
|
1634
|
-
// <CapAskAira.ContentGenerationBot
|
|
1635
|
-
// text={fallbackMessage || ""}
|
|
1636
|
-
// setText={(text) => {
|
|
1637
|
-
// onFallbackMessageChange({ target: { value: text } });
|
|
1638
|
-
// }}
|
|
1639
|
-
// iconPlacement="float-br"
|
|
1640
|
-
// rootStyle={{
|
|
1641
|
-
// bottom: "0.5rem",
|
|
1642
|
-
// right: "0.5rem",
|
|
1643
|
-
// position: "absolute",
|
|
1644
|
-
// }}
|
|
1645
|
-
// />
|
|
1646
|
-
// )}
|
|
1647
|
-
// </div>
|
|
1648
|
-
// <CapRow>{fallbackSmsLength()}</CapRow>
|
|
1649
|
-
// </>
|
|
1650
|
-
// );
|
|
1651
|
-
|
|
1652
|
-
// return <>{contentArr}</>;
|
|
1653
|
-
};
|
|
2295
|
+
const renderFallBackSmsComponent = () => (
|
|
2296
|
+
<SmsFallback
|
|
2297
|
+
value={smsFallbackData}
|
|
2298
|
+
onChange={setSmsFallbackData}
|
|
2299
|
+
parentLocation={location}
|
|
2300
|
+
smsRegister={smsRegister}
|
|
2301
|
+
isFullMode={isFullMode}
|
|
2302
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
2303
|
+
channelsToHide={CHANNELS_TO_HIDE_FOR_SMS_ONLY}
|
|
2304
|
+
sectionTitle={
|
|
2305
|
+
smsFallbackData
|
|
2306
|
+
? formatMessage(messages.fallbackLabel)
|
|
2307
|
+
: formatMessage(messages.smsFallbackOptional)
|
|
2308
|
+
}
|
|
2309
|
+
templateListTitle={formatMessage(creativesMessages.creativeTemplates)}
|
|
2310
|
+
templateListDescription={formatMessage(creativesMessages.creativeTemplatesDesc)}
|
|
2311
|
+
/* Full-mode: card layout only while drafting a new template; after send for approval or when a template is loaded, use inline layout. */
|
|
2312
|
+
showAsCard={isFullMode && !isEditFlow && templateStatus === ''}
|
|
2313
|
+
disableSelectTemplate={isEditFlow}
|
|
2314
|
+
eventContextTags={eventContextTags}
|
|
2315
|
+
onRcsFallbackEditorStateChange={handleSmsFallbackEditorStateChange}
|
|
2316
|
+
isRcsEditFlow={isEditFlow}
|
|
2317
|
+
/>
|
|
2318
|
+
);
|
|
1654
2319
|
|
|
1655
2320
|
const uploadRcsImage = useCallback((file, type, fileParams, index) => {
|
|
1656
2321
|
setImageError(null);
|
|
1657
|
-
const isRcsThumbnail = index
|
|
2322
|
+
const isRcsThumbnail = isThumbnailAssetIndex(index);
|
|
1658
2323
|
actions.uploadRcsAsset(file, type, {
|
|
1659
2324
|
isRcsThumbnail,
|
|
1660
2325
|
...fileParams,
|
|
@@ -1692,21 +2357,15 @@ const splitTemplateVarString = (str) => {
|
|
|
1692
2357
|
const updateOnRcsImageReUpload = useCallback(() => {
|
|
1693
2358
|
setUpdateRcsImageSrc('');
|
|
1694
2359
|
}, []);
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
const uploadRcsVideo = (file, type, fileParams) => {
|
|
1698
|
-
setImageError(null);
|
|
2360
|
+
const uploadRcsVideo = (file, type, fileParams, index) => {
|
|
1699
2361
|
actions.uploadRcsAsset(file, type, {
|
|
1700
2362
|
...fileParams,
|
|
1701
2363
|
type: 'video',
|
|
1702
2364
|
channel: RCS,
|
|
1703
2365
|
isGenerateVideoThumbnail: false,
|
|
1704
|
-
});
|
|
2366
|
+
}, index);
|
|
1705
2367
|
};
|
|
1706
2368
|
|
|
1707
|
-
const updateRcsVideoSrc = (val) => {
|
|
1708
|
-
setRcsVideoSrc(val);
|
|
1709
|
-
};
|
|
1710
2369
|
const setUpdateRcsVideoSrc = useCallback((index, val) => {
|
|
1711
2370
|
setRcsVideoSrc(val);
|
|
1712
2371
|
setAssetList(val);
|
|
@@ -1729,13 +2388,13 @@ const splitTemplateVarString = (str) => {
|
|
|
1729
2388
|
updateRcsThumbnailSrc('');
|
|
1730
2389
|
};
|
|
1731
2390
|
|
|
1732
|
-
|
|
2391
|
+
const renderThumbnailComponent = () => {
|
|
1733
2392
|
const currentDimension = selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
1734
2393
|
return !isEditFlow && (
|
|
1735
2394
|
<>
|
|
1736
2395
|
<CapHeading type="h4" className="rcs-image-dimensions-label">Upload Thumbnail</CapHeading>
|
|
1737
2396
|
<CapImageUpload
|
|
1738
|
-
|
|
2397
|
+
className="rcs-image-upload-with-top-padding"
|
|
1739
2398
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1740
2399
|
imgWidth={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].width}
|
|
1741
2400
|
imgHeight={RCS_VIDEO_THUMBNAIL_DIMENSIONS[currentDimension].height}
|
|
@@ -1753,6 +2412,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1753
2412
|
channel={RCS}
|
|
1754
2413
|
channelSpecificStyle={!isFullMode}
|
|
1755
2414
|
skipDimensionValidation={true}
|
|
2415
|
+
showReUploadButton={!isEditFlow && isFullMode}
|
|
1756
2416
|
/>
|
|
1757
2417
|
</>
|
|
1758
2418
|
)
|
|
@@ -1778,7 +2438,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1778
2438
|
value: dim.type,
|
|
1779
2439
|
label: `${dim.label}`
|
|
1780
2440
|
}))}
|
|
1781
|
-
|
|
2441
|
+
className="rcs-dimension-select--bottom-spacing"
|
|
1782
2442
|
/>
|
|
1783
2443
|
</>
|
|
1784
2444
|
)}
|
|
@@ -1792,7 +2452,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1792
2452
|
</div>
|
|
1793
2453
|
) : (
|
|
1794
2454
|
<CapImageUpload
|
|
1795
|
-
|
|
2455
|
+
className="rcs-image-upload-with-top-padding"
|
|
1796
2456
|
allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
|
|
1797
2457
|
imgWidth={RCS_IMAGE_DIMENSIONS[selectedDimension].width}
|
|
1798
2458
|
imgHeight={RCS_IMAGE_DIMENSIONS[selectedDimension].height}
|
|
@@ -1803,7 +2463,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1803
2463
|
updateImageSrc={setUpdateRcsImageSrc}
|
|
1804
2464
|
updateOnReUpload={updateOnRcsImageReUpload}
|
|
1805
2465
|
index={0}
|
|
1806
|
-
className="cap-custom-image-upload"
|
|
2466
|
+
className="cap-custom-image-upload rcs-image-upload--top-spacing"
|
|
1807
2467
|
key={`rcs-uploaded-image-${selectedDimension}`}
|
|
1808
2468
|
imageData={rcsData}
|
|
1809
2469
|
channel={RCS}
|
|
@@ -1814,7 +2474,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1814
2474
|
|
|
1815
2475
|
</>
|
|
1816
2476
|
);
|
|
1817
|
-
|
|
2477
|
+
}
|
|
1818
2478
|
|
|
1819
2479
|
const renderVideoComponent = () => {
|
|
1820
2480
|
const currentDimension =selectedDimension || Object.keys(RCS_VIDEO_THUMBNAIL_DIMENSIONS)[0];
|
|
@@ -1833,7 +2493,7 @@ const splitTemplateVarString = (str) => {
|
|
|
1833
2493
|
value: dim.type,
|
|
1834
2494
|
label: `${dim.label}`
|
|
1835
2495
|
}))}
|
|
1836
|
-
|
|
2496
|
+
className="rcs-dimension-select--bottom-spacing"
|
|
1837
2497
|
/>
|
|
1838
2498
|
)}
|
|
1839
2499
|
{(isEditFlow || !isFullMode) ? (
|
|
@@ -1891,10 +2551,48 @@ const splitTemplateVarString = (str) => {
|
|
|
1891
2551
|
};
|
|
1892
2552
|
|
|
1893
2553
|
const getRcsPreview = () => {
|
|
2554
|
+
|
|
2555
|
+
if (templateType === contentType.carousel) {
|
|
2556
|
+
const cardsForPreview = buildCarouselCardsForPreview(carouselData);
|
|
2557
|
+
const carouselDimKey = getCarouselDimensionKey(selectedCarouselHeight, selectedCarouselWidth);
|
|
2558
|
+
const carouselImgDims =
|
|
2559
|
+
RCS_CAROUSEL_IMAGE_DIMENSIONS[carouselDimKey] || RCS_CAROUSEL_IMAGE_DIMENSIONS.MEDIUM_MEDIUM;
|
|
2560
|
+
const carouselVidDims =
|
|
2561
|
+
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
2562
|
+
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
2563
|
+
// Debug log for embedded/library mode preview payload (carousel)
|
|
2564
|
+
// eslint-disable-next-line no-console
|
|
2565
|
+
return (
|
|
2566
|
+
<UnifiedPreview
|
|
2567
|
+
channel={RCS}
|
|
2568
|
+
content={{
|
|
2569
|
+
carouselData: cardsForPreview,
|
|
2570
|
+
carouselPreviewDimensions: {
|
|
2571
|
+
imageWidth: carouselImgDims.width,
|
|
2572
|
+
imageHeight: carouselImgDims.height,
|
|
2573
|
+
videoThumbWidth: carouselVidDims.width,
|
|
2574
|
+
videoThumbHeight: carouselVidDims.height,
|
|
2575
|
+
},
|
|
2576
|
+
}}
|
|
2577
|
+
device={ANDROID}
|
|
2578
|
+
showDeviceToggle={false}
|
|
2579
|
+
showHeader={false}
|
|
2580
|
+
formatMessage={formatMessage}
|
|
2581
|
+
/>
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
1894
2584
|
|
|
1895
2585
|
const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
|
|
1896
|
-
const
|
|
1897
|
-
const
|
|
2586
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
2587
|
+
const titleVarCountForResolve = isMediaTypeText
|
|
2588
|
+
? 0
|
|
2589
|
+
: ((templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []).length);
|
|
2590
|
+
const resolvedTitle = isMediaTypeText
|
|
2591
|
+
? ''
|
|
2592
|
+
: (isSlotMappingMode ? resolveTemplateWithMap(templateTitle, 0) : templateTitle);
|
|
2593
|
+
const resolvedDesc = isSlotMappingMode
|
|
2594
|
+
? resolveTemplateWithMap(templateDesc, titleVarCountForResolve)
|
|
2595
|
+
: templateDesc;
|
|
1898
2596
|
return (
|
|
1899
2597
|
<UnifiedPreview
|
|
1900
2598
|
channel={RCS}
|
|
@@ -1917,51 +2615,39 @@ const splitTemplateVarString = (str) => {
|
|
|
1917
2615
|
);
|
|
1918
2616
|
};
|
|
1919
2617
|
|
|
1920
|
-
const getUnmappedDesc = (str, mapping) => {
|
|
1921
|
-
if (!str) return '';
|
|
1922
|
-
if (!mapping || Object.keys(mapping).length === 0) return str;
|
|
1923
|
-
let result = str;
|
|
1924
|
-
const replacements = [];
|
|
1925
|
-
Object.entries(mapping).forEach(([key, value]) => {
|
|
1926
|
-
const raw = (value ?? '').toString();
|
|
1927
|
-
if (!raw || raw?.trim?.() === '') return;
|
|
1928
|
-
const braced = /^\{\{[\s\S]*\}\}$/.test(raw) ? raw : `{{${raw}}}`;
|
|
1929
|
-
replacements.push({ key, needle: raw });
|
|
1930
|
-
if (braced !== raw) replacements.push({ key, needle: braced });
|
|
1931
|
-
});
|
|
1932
|
-
const seen = new Set();
|
|
1933
|
-
const uniq = replacements
|
|
1934
|
-
.filter(({ key, needle }) => {
|
|
1935
|
-
const id = `${key}::${needle}`;
|
|
1936
|
-
if (seen.has(id)) return false;
|
|
1937
|
-
seen.add(id);
|
|
1938
|
-
return true;
|
|
1939
|
-
})
|
|
1940
|
-
.sort((a, b) => (b.needle.length - a.needle.length));
|
|
1941
|
-
|
|
1942
|
-
uniq.forEach(({ key, needle }) => {
|
|
1943
|
-
if (!needle) return;
|
|
1944
|
-
const escaped = needle.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
1945
|
-
const regex = new RegExp(escaped, 'g');
|
|
1946
|
-
result = result.replace(regex, `{{${key}}}`);
|
|
1947
|
-
});
|
|
1948
|
-
return result;
|
|
1949
|
-
};
|
|
1950
|
-
|
|
1951
2618
|
const createPayload = () => {
|
|
1952
|
-
const
|
|
1953
|
-
const {
|
|
1954
|
-
template_id: templateId = '',
|
|
1955
|
-
template_name = '',
|
|
1956
|
-
'sms-editor': template = '',
|
|
1957
|
-
header: registeredSenderIds = [],
|
|
1958
|
-
} = base;
|
|
1959
|
-
const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
|
|
1960
|
-
const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
|
|
2619
|
+
const isSlotMappingMode = isEditFlow || !isFullMode;
|
|
1961
2620
|
const alignment = isMediaTypeImage
|
|
1962
2621
|
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
|
|
1963
2622
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
|
|
1964
2623
|
|
|
2624
|
+
const heightTypeForCardWidth = isMediaTypeText
|
|
2625
|
+
? undefined
|
|
2626
|
+
: isMediaTypeImage
|
|
2627
|
+
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType
|
|
2628
|
+
: isMediaTypeVideo
|
|
2629
|
+
? RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType
|
|
2630
|
+
: undefined;
|
|
2631
|
+
const cardWidthFromSelection =
|
|
2632
|
+
heightTypeForCardWidth === MEDIUM ? MEDIUM : SMALL;
|
|
2633
|
+
|
|
2634
|
+
/** Library: merge props + state so SMS fallback is not dropped when local state is empty but templateData has consumer data. */
|
|
2635
|
+
const smsFallbackPayloadContent = buildSmsFallBackContentForPayload({
|
|
2636
|
+
smsFallbackData,
|
|
2637
|
+
templateData,
|
|
2638
|
+
isFullMode,
|
|
2639
|
+
smsRegister,
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
const carouselCardContent = isCarouselType
|
|
2643
|
+
? buildCarouselCardContentForPayload(carouselData, {
|
|
2644
|
+
isSlotMappingMode,
|
|
2645
|
+
cardVarMapped,
|
|
2646
|
+
selectedCarouselHeight,
|
|
2647
|
+
rcsVarRegex,
|
|
2648
|
+
})
|
|
2649
|
+
: null;
|
|
2650
|
+
|
|
1965
2651
|
const payload = {
|
|
1966
2652
|
name: templateName,
|
|
1967
2653
|
versions: {
|
|
@@ -1970,16 +2656,22 @@ const splitTemplateVarString = (str) => {
|
|
|
1970
2656
|
RCS: {
|
|
1971
2657
|
rcsContent: {
|
|
1972
2658
|
...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
|
|
1973
|
-
cardType: STANDALONE,
|
|
1974
|
-
cardSettings:
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2659
|
+
cardType: isCarouselType ? contentType.carousel : STANDALONE,
|
|
2660
|
+
cardSettings: isCarouselType
|
|
2661
|
+
? { cardOrientation: VERTICAL, cardWidth: selectedCarouselWidth || SMALL }
|
|
2662
|
+
: {
|
|
2663
|
+
cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
|
|
2664
|
+
...(alignment && { mediaAlignment: alignment }),
|
|
2665
|
+
cardWidth: SMALL,
|
|
2666
|
+
},
|
|
2667
|
+
cardContent: isCarouselType
|
|
2668
|
+
? carouselCardContent
|
|
2669
|
+
: [
|
|
1980
2670
|
{
|
|
1981
|
-
|
|
1982
|
-
|
|
2671
|
+
// Persist raw template copy + cardVarMapped — not resolveTemplateWithMap output — so library
|
|
2672
|
+
// / getFormData round-trip keeps {{…}} and slot values (resolved strings broke reopen hydration).
|
|
2673
|
+
title: templateTitle,
|
|
2674
|
+
description: templateDesc,
|
|
1983
2675
|
mediaType: templateMediaType,
|
|
1984
2676
|
...(!isMediaTypeText && {media: {
|
|
1985
2677
|
mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
|
|
@@ -1988,23 +2680,32 @@ const splitTemplateVarString = (str) => {
|
|
|
1988
2680
|
? RCS_IMAGE_DIMENSIONS[selectedDimension]?.heightType || MEDIUM
|
|
1989
2681
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
|
|
1990
2682
|
}}),
|
|
1991
|
-
...(
|
|
1992
|
-
const
|
|
1993
|
-
...(templateTitle
|
|
1994
|
-
...(templateDesc
|
|
2683
|
+
...(isSlotMappingMode && (() => {
|
|
2684
|
+
const templateVarTokens = [
|
|
2685
|
+
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
2686
|
+
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
1995
2687
|
];
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2688
|
+
const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
|
|
2689
|
+
cardVarMapped,
|
|
2690
|
+
);
|
|
2691
|
+
// Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
|
|
2692
|
+
// semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
|
|
2693
|
+
// and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
|
|
2694
|
+
const persistedSlotVarMap = {};
|
|
2695
|
+
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
2696
|
+
const varName = getVarNameFromToken(token);
|
|
2697
|
+
if (!varName) return;
|
|
2698
|
+
const resolvedRawValue = resolveCardVarMappedSlotValue(
|
|
2699
|
+
cardVarMappedForRcsCardOnly,
|
|
2700
|
+
varName,
|
|
2701
|
+
slotIndexZeroBased,
|
|
2702
|
+
isSlotMappingMode,
|
|
2703
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
2704
|
+
);
|
|
2705
|
+
const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
|
|
2706
|
+
persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
|
|
2006
2707
|
});
|
|
2007
|
-
return { cardVarMapped:
|
|
2708
|
+
return { cardVarMapped: persistedSlotVarMap };
|
|
2008
2709
|
})()),
|
|
2009
2710
|
...(suggestions.length > 0 && { suggestions }),
|
|
2010
2711
|
}
|
|
@@ -2012,17 +2713,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2012
2713
|
contentType: isFullMode ? templateType : RICHCARD,
|
|
2013
2714
|
...(isFullMode && {accountId:accountId, accessToken: accessToken, accountName: accountName, hostName: hostName}),
|
|
2014
2715
|
},
|
|
2015
|
-
|
|
2016
|
-
message: fallbackMessage,
|
|
2017
|
-
...(isDltEnabled && {
|
|
2018
|
-
templateConfigs: {
|
|
2019
|
-
templateId,
|
|
2020
|
-
templateName: template_name,
|
|
2021
|
-
template,
|
|
2022
|
-
registeredSenderIds,
|
|
2023
|
-
},
|
|
2024
|
-
}),
|
|
2025
|
-
},
|
|
2716
|
+
...smsFallbackPayloadContent,
|
|
2026
2717
|
},
|
|
2027
2718
|
},
|
|
2028
2719
|
},
|
|
@@ -2032,6 +2723,132 @@ const splitTemplateVarString = (str) => {
|
|
|
2032
2723
|
return payload;
|
|
2033
2724
|
};
|
|
2034
2725
|
|
|
2726
|
+
/** Shape expected by CommonTestAndPreview buildRcsTestMessagePayload (versions.base.content.RCS). */
|
|
2727
|
+
const testPreviewFormData = useMemo(() => {
|
|
2728
|
+
const payload = createPayload();
|
|
2729
|
+
const rcs = payload?.versions?.base?.content?.RCS;
|
|
2730
|
+
if (!rcs) return null;
|
|
2731
|
+
// createMessageMeta uses WeCRM `id` when present; else template API account id (sourceAccountIdentifier).
|
|
2732
|
+
const accountIdForCreateMessageMeta =
|
|
2733
|
+
(wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
|
|
2734
|
+
? String(wecrmAccountId)
|
|
2735
|
+
: accountId;
|
|
2736
|
+
const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
|
|
2737
|
+
let rcsForTest = {
|
|
2738
|
+
...rcs,
|
|
2739
|
+
rcsContent: {
|
|
2740
|
+
...rcs.rcsContent,
|
|
2741
|
+
...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
|
|
2742
|
+
},
|
|
2743
|
+
};
|
|
2744
|
+
/** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
|
|
2745
|
+
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
2746
|
+
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
2747
|
+
if (isCarouselType) {
|
|
2748
|
+
// Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values the
|
|
2749
|
+
// user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
2750
|
+
// - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
|
|
2751
|
+
// - prepareTagExtractionPayload can extract tag metadata from the card content.
|
|
2752
|
+
// Track cumulative slotOffset across cards: each card's title/description vars occupy
|
|
2753
|
+
// sequential global slot indices ({{1}},{{2}} in card 0; {{3}},{{4}} in card 1, …).
|
|
2754
|
+
// Without the offset, every card restarts at slotKey="1" and resolves against card 0's
|
|
2755
|
+
// mappings, causing tags from card 1+ to be missing or wrong.
|
|
2756
|
+
let carouselSlotOffset = 0;
|
|
2757
|
+
rcsForTest = {
|
|
2758
|
+
...rcsForTest,
|
|
2759
|
+
rcsContent: {
|
|
2760
|
+
...rcsForTest.rcsContent,
|
|
2761
|
+
cardContent: cardContent.map((card) => {
|
|
2762
|
+
const rawTitle = card.title || '';
|
|
2763
|
+
const rawDesc = card.description || '';
|
|
2764
|
+
const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
|
|
2765
|
+
const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
|
|
2766
|
+
const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
|
|
2767
|
+
const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
|
|
2768
|
+
carouselSlotOffset += titleVarCount + descVarCount;
|
|
2769
|
+
return { ...card, title: resolvedTitle, description: resolvedDesc };
|
|
2770
|
+
}),
|
|
2771
|
+
},
|
|
2772
|
+
};
|
|
2773
|
+
} else if (isSlotMappingModeForPreview) {
|
|
2774
|
+
// Standalone card: coalesce cardVarMapped with the template's slot names so the preview
|
|
2775
|
+
// API receives a correctly-keyed var map for non-carousel templates.
|
|
2776
|
+
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
2777
|
+
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
2778
|
+
templateTitle,
|
|
2779
|
+
templateDesc,
|
|
2780
|
+
rcsVarRegex,
|
|
2781
|
+
);
|
|
2782
|
+
rcsForTest = {
|
|
2783
|
+
...rcsForTest,
|
|
2784
|
+
rcsContent: {
|
|
2785
|
+
...rcsForTest.rcsContent,
|
|
2786
|
+
cardContent: [
|
|
2787
|
+
{ ...cardContent[0], cardVarMapped: fullCardVarMapped },
|
|
2788
|
+
...cardContent.slice(1),
|
|
2789
|
+
],
|
|
2790
|
+
},
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
const out = {
|
|
2795
|
+
versions: {
|
|
2796
|
+
base: {
|
|
2797
|
+
content: {
|
|
2798
|
+
RCS: rcsForTest,
|
|
2799
|
+
},
|
|
2800
|
+
},
|
|
2801
|
+
},
|
|
2802
|
+
};
|
|
2803
|
+
// Use the smsFallbackTemplateConfigs already built by createPayload() / buildSmsFallBackContentForPayload —
|
|
2804
|
+
// it merges the API baseline (templateId, registeredSenderIds, templateName, etc.) with local
|
|
2805
|
+
// smsFallbackData state. Reading raw smsFallbackData here would miss the API-sourced smsTemplateId
|
|
2806
|
+
// in campaign mode. This also keeps templateName (and all other fields) in sync with the approval
|
|
2807
|
+
// payload automatically.
|
|
2808
|
+
const smsFallbackTcFromPayload = rcs?.rcsContent?.smsFallBackContent?.templateConfigs;
|
|
2809
|
+
if (smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') {
|
|
2810
|
+
out.templateConfigs = {
|
|
2811
|
+
...smsFallbackTcFromPayload,
|
|
2812
|
+
traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister),
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
return out;
|
|
2816
|
+
}, [
|
|
2817
|
+
templateName,
|
|
2818
|
+
templateTitle,
|
|
2819
|
+
templateDesc,
|
|
2820
|
+
templateMediaType,
|
|
2821
|
+
cardVarMapped,
|
|
2822
|
+
carouselData,
|
|
2823
|
+
suggestions,
|
|
2824
|
+
rcsImageSrc,
|
|
2825
|
+
rcsVideoSrc,
|
|
2826
|
+
rcsThumbnailSrc,
|
|
2827
|
+
selectedDimension,
|
|
2828
|
+
smsFallbackData,
|
|
2829
|
+
isFullMode,
|
|
2830
|
+
isEditFlow,
|
|
2831
|
+
templateType,
|
|
2832
|
+
accountId,
|
|
2833
|
+
wecrmAccountId,
|
|
2834
|
+
accessToken,
|
|
2835
|
+
accountName,
|
|
2836
|
+
hostName,
|
|
2837
|
+
smsRegister,
|
|
2838
|
+
]);
|
|
2839
|
+
|
|
2840
|
+
/**
|
|
2841
|
+
* Library/campaign: `createPayload` merges root + nested `smsFallBackContent` from `templateData`
|
|
2842
|
+
* with `smsFallbackData`. Done/slot validation must use the same merge — otherwise local state can
|
|
2843
|
+
* miss `templateContent` / var map while the parent payload still has them (DLT campaigns).
|
|
2844
|
+
*/
|
|
2845
|
+
const librarySmsFallbackMergedForValidation = useMemo(
|
|
2846
|
+
() => mergeSmsFallbackForLibrary(templateData, smsFallbackData, isFullMode),
|
|
2847
|
+
[isFullMode, templateData, smsFallbackData],
|
|
2848
|
+
);
|
|
2849
|
+
|
|
2850
|
+
|
|
2851
|
+
|
|
2035
2852
|
const actionCallback = ({ errorMessage, resp }, isEdit) => {
|
|
2036
2853
|
// eslint-disable-next-line no-undef
|
|
2037
2854
|
const error = errorMessage?.message || errorMessage;
|
|
@@ -2061,6 +2878,9 @@ const splitTemplateVarString = (str) => {
|
|
|
2061
2878
|
_id: params?.id,
|
|
2062
2879
|
validity: true,
|
|
2063
2880
|
type: RCS,
|
|
2881
|
+
// CreativesContainer closes the slide box *after* getCreativesData runs so the parent receives
|
|
2882
|
+
// the RCS payload first (closing immediately used to skip getCreativesData → empty "Add creative").
|
|
2883
|
+
closeSlideBoxAfterSubmit: !isFullMode,
|
|
2064
2884
|
};
|
|
2065
2885
|
getFormData(formDataParams);
|
|
2066
2886
|
};
|
|
@@ -2074,6 +2894,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2074
2894
|
actionCallback({ resp, errorMessage });
|
|
2075
2895
|
setSpin(false); // Always turn off spinner
|
|
2076
2896
|
if (!errorMessage) {
|
|
2897
|
+
setTemplateStatus(RCS_STATUSES.pending);
|
|
2077
2898
|
onCreateComplete();
|
|
2078
2899
|
}
|
|
2079
2900
|
});
|
|
@@ -2085,6 +2906,68 @@ const splitTemplateVarString = (str) => {
|
|
|
2085
2906
|
}
|
|
2086
2907
|
};
|
|
2087
2908
|
|
|
2909
|
+
const smsFallbackBlocksDone = () => checkSmsFallbackBlocksDone({
|
|
2910
|
+
isFullMode,
|
|
2911
|
+
smsRegister,
|
|
2912
|
+
smsFallbackData,
|
|
2913
|
+
templateData,
|
|
2914
|
+
smsFallbackMergedForValidation: librarySmsFallbackMergedForValidation,
|
|
2915
|
+
});
|
|
2916
|
+
|
|
2917
|
+
/**
|
|
2918
|
+
* Library / campaigns (`!isFullMode`): card slots are often stored on numeric keys (`1`,`2`,…) while
|
|
2919
|
+
* semantic keys stay `""` from API round-trip. `resolveCardVarMappedSlotValue` matches createPayload
|
|
2920
|
+
* / preview — naive `cardVarMapped[name]` wrongly kept Done disabled for DLT.
|
|
2921
|
+
*/
|
|
2922
|
+
const isLibraryCampaignCardVarMappingIncomplete = () => {
|
|
2923
|
+
if (isFullMode) return false;
|
|
2924
|
+
const titleTokens = splitTemplateVarStringRcs(templateTitle).filter((elem) =>
|
|
2925
|
+
rcsVarTestRegex.test(elem),
|
|
2926
|
+
);
|
|
2927
|
+
const descTokens = splitTemplateVarStringRcs(templateDesc).filter((elem) =>
|
|
2928
|
+
rcsVarTestRegex.test(elem),
|
|
2929
|
+
);
|
|
2930
|
+
const orderedVarNames = [
|
|
2931
|
+
...titleTokens.map((token) => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
|
|
2932
|
+
...descTokens.map((token) => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '')),
|
|
2933
|
+
];
|
|
2934
|
+
if (orderedVarNames.length > 0 && isEmpty(cardVarMapped)) {
|
|
2935
|
+
return true;
|
|
2936
|
+
}
|
|
2937
|
+
return orderedVarNames.some((name, globalIdx) => {
|
|
2938
|
+
const slotValue = resolveCardVarMappedSlotValue(
|
|
2939
|
+
cardVarMapped,
|
|
2940
|
+
name,
|
|
2941
|
+
globalIdx,
|
|
2942
|
+
true,
|
|
2943
|
+
rcsSpanningSemanticVarNames.has(name),
|
|
2944
|
+
);
|
|
2945
|
+
const stringValue = slotValue == null ? '' : String(slotValue);
|
|
2946
|
+
return stringValue.trim() === '';
|
|
2947
|
+
});
|
|
2948
|
+
};
|
|
2949
|
+
|
|
2950
|
+
const isCarouselLibraryIncomplete = () => {
|
|
2951
|
+
if (!isCarouselType || isFullMode) return false;
|
|
2952
|
+
if ((carouselErrors || []).some((err) => err?.title || err?.description)) return true;
|
|
2953
|
+
// Block if any card's title or description has no content at all (no static text and no variable tokens).
|
|
2954
|
+
const hasEmptyField = (carouselData || []).some((card) =>
|
|
2955
|
+
['title', 'description'].some((field) => !(card?.[field] || '').trim())
|
|
2956
|
+
);
|
|
2957
|
+
if (hasEmptyField) return true;
|
|
2958
|
+
const unfilledVar = (carouselData || []).some((card) =>
|
|
2959
|
+
['title', 'description'].some((field) => {
|
|
2960
|
+
const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
|
|
2961
|
+
return tokens.some((token) => {
|
|
2962
|
+
const name = token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
2963
|
+
const slotValue = cardVarMapped?.[name];
|
|
2964
|
+
return slotValue == null || String(slotValue).trim() === '';
|
|
2965
|
+
});
|
|
2966
|
+
})
|
|
2967
|
+
);
|
|
2968
|
+
return unfilledVar;
|
|
2969
|
+
};
|
|
2970
|
+
|
|
2088
2971
|
const isDisableDone = () => {
|
|
2089
2972
|
if(isEditFlow){
|
|
2090
2973
|
return false;
|
|
@@ -2095,46 +2978,26 @@ const splitTemplateVarString = (str) => {
|
|
|
2095
2978
|
}
|
|
2096
2979
|
}
|
|
2097
2980
|
|
|
2098
|
-
if(
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
|
|
2102
|
-
|
|
2103
|
-
if (allVars.length > 0 && isEmpty(cardVarMapped)) {
|
|
2104
|
-
return true;
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
const hasEmptyMapping =
|
|
2108
|
-
cardVarMapped &&
|
|
2109
|
-
Object.keys(cardVarMapped).length > 0 &&
|
|
2110
|
-
Object.entries(cardVarMapped).some(([_, v]) => {
|
|
2111
|
-
if (typeof v !== 'string') return !v; // null/undefined
|
|
2112
|
-
return v.trim() === ''; // empty string
|
|
2113
|
-
});
|
|
2114
|
-
|
|
2115
|
-
if (hasEmptyMapping) {
|
|
2116
|
-
return true;
|
|
2117
|
-
}
|
|
2981
|
+
if (isCarouselLibraryIncomplete()) {
|
|
2982
|
+
return true;
|
|
2983
|
+
}
|
|
2118
2984
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
if (typeof v !== 'string') return !v;
|
|
2122
|
-
return v.trim() === '';
|
|
2123
|
-
});
|
|
2124
|
-
if (anyMissing) {
|
|
2125
|
-
return true;
|
|
2126
|
-
}
|
|
2985
|
+
if (isLibraryCampaignCardVarMappingIncomplete()) {
|
|
2986
|
+
return true;
|
|
2127
2987
|
}
|
|
2128
2988
|
|
|
2129
|
-
|
|
2989
|
+
if (smsFallbackBlocksDone()) {
|
|
2130
2990
|
return true;
|
|
2991
|
+
}
|
|
2131
2992
|
|
|
2993
|
+
if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
|
|
2994
|
+
return true;
|
|
2132
2995
|
}
|
|
2133
|
-
if (isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2996
|
+
if (!isCarouselType && isMediaTypeImage && (rcsImageSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2134
2997
|
return true;
|
|
2135
2998
|
}
|
|
2136
2999
|
|
|
2137
|
-
if (isMediaTypeVideo && (rcsVideoSrc.videoSrc
|
|
3000
|
+
if (!isCarouselType && isMediaTypeVideo && (!rcsVideoSrc.videoSrc || rcsThumbnailSrc === '' || templateTitle === '' || templateDesc === '' )) {
|
|
2138
3001
|
return true;
|
|
2139
3002
|
}
|
|
2140
3003
|
if (buttonType.includes(CTA)) {
|
|
@@ -2146,72 +3009,68 @@ const splitTemplateVarString = (str) => {
|
|
|
2146
3009
|
return true;
|
|
2147
3010
|
}
|
|
2148
3011
|
}
|
|
2149
|
-
if (templateDescError || templateTitleError
|
|
3012
|
+
if (templateDescError || templateTitleError) {
|
|
3013
|
+
return true;
|
|
3014
|
+
}
|
|
3015
|
+
if (isSmsFallbackContentTooLong(smsFallbackData, FALLBACK_MESSAGE_MAX_LENGTH)) {
|
|
2150
3016
|
return true;
|
|
2151
3017
|
}
|
|
2152
3018
|
return false;
|
|
2153
3019
|
};
|
|
2154
3020
|
|
|
2155
3021
|
const isEditDisableDone = () => {
|
|
2156
|
-
|
|
2157
|
-
if (templateStatus !== RCS_STATUSES.approved) {
|
|
3022
|
+
if (isFullMode && !isHostInfoBip && templateStatus !== RCS_STATUSES.approved) {
|
|
2158
3023
|
return true;
|
|
2159
3024
|
}
|
|
2160
3025
|
|
|
2161
|
-
if (
|
|
2162
|
-
|
|
2163
|
-
return true;
|
|
2164
|
-
}
|
|
3026
|
+
if (isCarouselLibraryIncomplete()) {
|
|
3027
|
+
return true;
|
|
2165
3028
|
}
|
|
2166
|
-
if(!isFullMode){
|
|
2167
|
-
const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
|
|
2168
|
-
const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem)).map(v => v.replace(/^\{\{|\}\}$/g, ''));
|
|
2169
|
-
const allVars = Array.from(new Set([ ...titleVars, ...descVars ]));
|
|
2170
|
-
|
|
2171
|
-
if (allVars.length > 0 && isEmpty(cardVarMapped)) {
|
|
2172
|
-
return true;
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
const hasEmptyMapping =
|
|
2176
|
-
cardVarMapped &&
|
|
2177
|
-
Object.keys(cardVarMapped).length > 0 &&
|
|
2178
|
-
Object.entries(cardVarMapped).some(([_, v]) => {
|
|
2179
|
-
if (typeof v !== 'string') return !v; // null/undefined
|
|
2180
|
-
return v.trim() === ''; // empty string
|
|
2181
|
-
});
|
|
2182
3029
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
3030
|
+
if (isLibraryCampaignCardVarMappingIncomplete()) {
|
|
3031
|
+
return true;
|
|
3032
|
+
}
|
|
2186
3033
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
if (typeof v !== 'string') return !v;
|
|
2190
|
-
return v.trim() === '';
|
|
2191
|
-
});
|
|
2192
|
-
if (anyMissing) {
|
|
2193
|
-
return true;
|
|
2194
|
-
}
|
|
3034
|
+
if (smsFallbackBlocksDone()) {
|
|
3035
|
+
return true;
|
|
2195
3036
|
}
|
|
2196
|
-
|
|
3037
|
+
|
|
3038
|
+
if (!isCarouselType && isMediaTypeText && templateDesc.trim() === '') {
|
|
2197
3039
|
return true;
|
|
2198
3040
|
}
|
|
2199
|
-
if (isMediaTypeImage && rcsImageSrc === '') {
|
|
3041
|
+
if (!isCarouselType && isMediaTypeImage && rcsImageSrc === '') {
|
|
2200
3042
|
return true;
|
|
2201
3043
|
}
|
|
2202
|
-
if(isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
|
|
3044
|
+
if (!isCarouselType && isMediaTypeVideo && (rcsThumbnailSrc === '' || rcsVideoSrc.videoSrc === '')) {
|
|
2203
3045
|
return true;
|
|
2204
3046
|
}
|
|
2205
3047
|
|
|
2206
3048
|
if (buttonType.includes(CTA)) {
|
|
2207
|
-
const hasValidButtons = suggestions.every(suggestion =>
|
|
3049
|
+
const hasValidButtons = suggestions.every(suggestion =>
|
|
2208
3050
|
suggestion.text && suggestion.url && !suggestionError && !forbiddenCharactersValidation(suggestion.text)
|
|
2209
3051
|
);
|
|
2210
3052
|
if (!hasValidButtons) {
|
|
2211
3053
|
return true;
|
|
2212
3054
|
}
|
|
2213
3055
|
}
|
|
2214
|
-
|
|
3056
|
+
// Mirror WhatsApp's isEditDoneDisabled: block Done while a CTA button's dynamic URL still
|
|
3057
|
+
// holds the unresolved {{1}} placeholder (no real personalization tag assigned yet).
|
|
3058
|
+
const hasUnresolvedDynamicCtaUrl = isCarouselType
|
|
3059
|
+
? (carouselData || []).some((card) =>
|
|
3060
|
+
(card?.suggestions || []).some((suggestion) =>
|
|
3061
|
+
suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
|
|
3062
|
+
)
|
|
3063
|
+
)
|
|
3064
|
+
: suggestions.some((suggestion) =>
|
|
3065
|
+
suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
|
|
3066
|
+
);
|
|
3067
|
+
if (hasUnresolvedDynamicCtaUrl) {
|
|
3068
|
+
return true;
|
|
3069
|
+
}
|
|
3070
|
+
if (templateTitleError || templateDescError) {
|
|
3071
|
+
return true;
|
|
3072
|
+
}
|
|
3073
|
+
if (isSmsFallbackContentTooLong(smsFallbackData, FALLBACK_MESSAGE_MAX_LENGTH)) {
|
|
2215
3074
|
return true;
|
|
2216
3075
|
}
|
|
2217
3076
|
return false;
|
|
@@ -2261,85 +3120,108 @@ const splitTemplateVarString = (str) => {
|
|
|
2261
3120
|
};
|
|
2262
3121
|
|
|
2263
3122
|
const getMainContent = () => {
|
|
2264
|
-
if (showDltContainer && !fallbackPreviewmode) {
|
|
2265
|
-
const dltSlideBoxContent = showDltContainer && getDltSlideBoxContent();
|
|
2266
|
-
const { dltHeader = '', dltContent = '' } = dltSlideBoxContent;
|
|
2267
|
-
return (
|
|
2268
|
-
<CapSlideBox
|
|
2269
|
-
show={showDltContainer}
|
|
2270
|
-
header={dltHeader}
|
|
2271
|
-
content={dltContent}
|
|
2272
|
-
handleClose={closeDltContainerHandler}
|
|
2273
|
-
size="size-xl"
|
|
2274
|
-
/>
|
|
2275
|
-
);
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
3123
|
return (
|
|
2279
3124
|
<>
|
|
2280
|
-
{templateStatus !== '' && (
|
|
2281
|
-
<
|
|
2282
|
-
{
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
3125
|
+
{templateStatus !== '' && (
|
|
3126
|
+
<CapRow useLegacy className="template-status-container">
|
|
3127
|
+
<CapColumn span={14}>
|
|
3128
|
+
<CapLabel type="label2">
|
|
3129
|
+
{formatMessage(messages.templateStatusLabel)}
|
|
3130
|
+
</CapLabel>
|
|
3131
|
+
|
|
3132
|
+
{!isHostInfoBip && templateStatus && (
|
|
3133
|
+
<CapAlert
|
|
3134
|
+
message={getTemplateStatusMessage()}
|
|
3135
|
+
type={getTemplateStatusType(templateStatus)}
|
|
3136
|
+
/>
|
|
3137
|
+
)}
|
|
3138
|
+
</CapColumn>
|
|
3139
|
+
</CapRow>
|
|
2292
3140
|
)}
|
|
2293
|
-
<CapRow className=
|
|
3141
|
+
<CapRow className={`cap-rcs-creatives ${isEditLike ? 'rcs-edit-mode' : ''}`}>
|
|
2294
3142
|
<CapColumn span={14}>
|
|
2295
3143
|
{/* template name */}
|
|
2296
3144
|
{isFullMode && (
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
3145
|
+
isEditFlow ? (
|
|
3146
|
+
<div className="rcs-creative-name-readonly">
|
|
3147
|
+
<CapHeading type="h4">
|
|
3148
|
+
{formatMessage(globalMessages.creativeNameLabel)}
|
|
3149
|
+
</CapHeading>
|
|
3150
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
3151
|
+
{templateName || '-'}
|
|
3152
|
+
</CapHeading>
|
|
3153
|
+
</div>
|
|
3154
|
+
) : (
|
|
3155
|
+
<CapInput
|
|
3156
|
+
id="rcs_template_name_input"
|
|
3157
|
+
data-testid="template_name"
|
|
3158
|
+
onChange={onTemplateNameChange}
|
|
3159
|
+
errorMessage={templateNameError}
|
|
3160
|
+
placeholder={formatMessage(
|
|
3161
|
+
globalMessages.templateNamePlaceholder,
|
|
3162
|
+
)}
|
|
3163
|
+
value={templateName || ''}
|
|
3164
|
+
size="default"
|
|
3165
|
+
label={formatMessage(globalMessages.creativeNameLabel)}
|
|
3166
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
3167
|
+
/>
|
|
3168
|
+
)
|
|
3169
|
+
)}
|
|
3170
|
+
{isEditFlow ? (
|
|
3171
|
+
<div className="rcs-creative-name-readonly">
|
|
3172
|
+
<CapHeading type="h4">
|
|
3173
|
+
{formatMessage(messages.templateTypeLabel)}
|
|
3174
|
+
</CapHeading>
|
|
3175
|
+
<CapHeading type="h5" className="rcs-creative-name-value">
|
|
3176
|
+
{TEMPLATE_TYPE_OPTIONS.find((option) => option.value === templateType)?.label || '-'}
|
|
3177
|
+
</CapHeading>
|
|
3178
|
+
</div>
|
|
3179
|
+
) : (
|
|
3180
|
+
<>
|
|
3181
|
+
{renderLabel('templateTypeLabel')}
|
|
3182
|
+
<ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
|
|
3183
|
+
<CapRadioGroup
|
|
3184
|
+
id="select-rcs-template-type"
|
|
3185
|
+
options={TEMPLATE_TYPE_OPTIONS}
|
|
3186
|
+
onChange={onTemplateTypeChange}
|
|
3187
|
+
value={templateType}
|
|
3188
|
+
disabled={!isFullMode}
|
|
3189
|
+
/>
|
|
3190
|
+
</ConfigProvider>
|
|
3191
|
+
</>
|
|
2310
3192
|
)}
|
|
2311
|
-
{renderLabel('templateTypeLabel')}
|
|
2312
|
-
<ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
|
|
2313
|
-
<CapRadioGroup
|
|
2314
|
-
id="select-rcs-template-type"
|
|
2315
|
-
options={TEMPLATE_TYPE_OPTIONS}
|
|
2316
|
-
onChange={onTemplateTypeChange}
|
|
2317
|
-
value={templateType}
|
|
2318
|
-
disabled={(isEditFlow || !isFullMode)}
|
|
2319
|
-
/>
|
|
2320
|
-
</ConfigProvider>
|
|
2321
3193
|
|
|
2322
|
-
{
|
|
2323
|
-
|
|
3194
|
+
{templateType === contentType.carousel ? (
|
|
3195
|
+
renderCarouselSection()
|
|
3196
|
+
) : (
|
|
2324
3197
|
<>
|
|
2325
|
-
{
|
|
2326
|
-
|
|
3198
|
+
{/* Show media only for rich_card */}
|
|
3199
|
+
{templateType === contentType.rich_card && (
|
|
3200
|
+
<>
|
|
3201
|
+
{renderLabel('mediaLabel')}
|
|
3202
|
+
<ConfigProvider theme={{ components: { Radio: { radioSize: 20, dotSize: 8 } } }}>
|
|
2327
3203
|
<CapRadioGroup
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
3204
|
+
options={mediaRadioOptions || []}
|
|
3205
|
+
value={templateMediaType}
|
|
3206
|
+
onChange={onTemplateMediaTypeChange}
|
|
3207
|
+
disabled={(isEditFlow || !isFullMode)}
|
|
3208
|
+
className="rcs-radio"
|
|
3209
|
+
/>
|
|
3210
|
+
</ConfigProvider>
|
|
2335
3211
|
<div className="rcs-container-image">
|
|
2336
|
-
|
|
2337
|
-
|
|
3212
|
+
{getMediaBasedComponent()}
|
|
3213
|
+
</div>
|
|
3214
|
+
</>
|
|
3215
|
+
)}
|
|
3216
|
+
{renderTextComponent()}
|
|
3217
|
+
</>
|
|
3218
|
+
)}
|
|
3219
|
+
{(!isEditFlow || hasMeaningfulSmsFallbackShape(smsFallbackData)) && (
|
|
3220
|
+
<>
|
|
3221
|
+
<CapDivider className="rcs-fallback-section-divider" />
|
|
3222
|
+
{renderFallBackSmsComponent()}
|
|
2338
3223
|
</>
|
|
2339
3224
|
)}
|
|
2340
|
-
{renderTextComponent()}
|
|
2341
|
-
<CapDivider style={{ margin: `${CAP_SPACE_28} 0` }} />
|
|
2342
|
-
{renderFallBackSmsComponent()}
|
|
2343
3225
|
<div className="rcs-scroll-div" />
|
|
2344
3226
|
</CapColumn>
|
|
2345
3227
|
<CapColumn span={8} offset={2}>
|
|
@@ -2350,7 +3232,8 @@ const splitTemplateVarString = (str) => {
|
|
|
2350
3232
|
|
|
2351
3233
|
|
|
2352
3234
|
<div className="rcs-footer">
|
|
2353
|
-
{!
|
|
3235
|
+
{/* 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). */}
|
|
3236
|
+
{!isEditFlow && isFullMode && (
|
|
2354
3237
|
<>
|
|
2355
3238
|
<div className="button-disabled-tooltip-wrapper">
|
|
2356
3239
|
<CapButton
|
|
@@ -2358,7 +3241,9 @@ const splitTemplateVarString = (str) => {
|
|
|
2358
3241
|
disabled={isDisableDone()}
|
|
2359
3242
|
className="rcs-done-btn"
|
|
2360
3243
|
>
|
|
2361
|
-
<FormattedMessage
|
|
3244
|
+
<FormattedMessage
|
|
3245
|
+
{...(isHostInfoBip ? messages.doneButtonLabel : messages.sendForApprovalButtonLabel)}
|
|
3246
|
+
/>
|
|
2362
3247
|
</CapButton>
|
|
2363
3248
|
</div>
|
|
2364
3249
|
<CapTooltip
|
|
@@ -2371,7 +3256,6 @@ const splitTemplateVarString = (str) => {
|
|
|
2371
3256
|
className="rcs-test-preview-btn"
|
|
2372
3257
|
type="secondary"
|
|
2373
3258
|
disabled={true}
|
|
2374
|
-
style={{ marginLeft: "8px" }}
|
|
2375
3259
|
>
|
|
2376
3260
|
<FormattedMessage {...creativesMessages.testAndPreview} />
|
|
2377
3261
|
</CapButton>
|
|
@@ -2392,7 +3276,7 @@ const splitTemplateVarString = (str) => {
|
|
|
2392
3276
|
</div>
|
|
2393
3277
|
</>
|
|
2394
3278
|
)}
|
|
2395
|
-
{isEditFlow && templateStatus === RCS_STATUSES.approved && (
|
|
3279
|
+
{isEditFlow && (isHostInfoBip || templateStatus === RCS_STATUSES.approved) && (
|
|
2396
3280
|
<>
|
|
2397
3281
|
<CapButton
|
|
2398
3282
|
onClick={handleTestAndPreview}
|
|
@@ -2404,51 +3288,6 @@ const splitTemplateVarString = (str) => {
|
|
|
2404
3288
|
</>
|
|
2405
3289
|
)}
|
|
2406
3290
|
</div>
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
{fallbackPreviewmode && (
|
|
2410
|
-
<CapSlideBox
|
|
2411
|
-
className="rcs-fallback-preview"
|
|
2412
|
-
show={fallbackPreviewmode}
|
|
2413
|
-
header={(
|
|
2414
|
-
<CapHeading type="h7" style={{ color: CAP_G01 }}>
|
|
2415
|
-
{formatMessage(messages.fallbackPreviewtitle)}
|
|
2416
|
-
</CapHeading>
|
|
2417
|
-
)}
|
|
2418
|
-
content={(
|
|
2419
|
-
<>
|
|
2420
|
-
<UnifiedPreview
|
|
2421
|
-
channel={RCS}
|
|
2422
|
-
content={{
|
|
2423
|
-
rcsPreviewContent: {
|
|
2424
|
-
rcsDesc: tempMsg,
|
|
2425
|
-
},
|
|
2426
|
-
}}
|
|
2427
|
-
device={ANDROID}
|
|
2428
|
-
showDeviceToggle={false}
|
|
2429
|
-
showHeader={false}
|
|
2430
|
-
formatMessage={formatMessage}
|
|
2431
|
-
/>
|
|
2432
|
-
<CapHeading
|
|
2433
|
-
type="h3"
|
|
2434
|
-
style={{ textAlign: 'center', width: '100%' }}
|
|
2435
|
-
className="margin-t-16"
|
|
2436
|
-
>
|
|
2437
|
-
{formatMessage(messages.totalCharacters, {
|
|
2438
|
-
smsCount: Math.ceil(
|
|
2439
|
-
tempMsg?.length / FALLBACK_MESSAGE_MAX_LENGTH,
|
|
2440
|
-
),
|
|
2441
|
-
number: tempMsg.length,
|
|
2442
|
-
})}
|
|
2443
|
-
</CapHeading>
|
|
2444
|
-
</>
|
|
2445
|
-
)}
|
|
2446
|
-
handleClose={() => {
|
|
2447
|
-
setFallbackPreviewmode(false);
|
|
2448
|
-
setDltPreviewData('');
|
|
2449
|
-
}}
|
|
2450
|
-
/>
|
|
2451
|
-
)}
|
|
2452
3291
|
</>
|
|
2453
3292
|
);
|
|
2454
3293
|
};
|
|
@@ -2457,29 +3296,49 @@ const splitTemplateVarString = (str) => {
|
|
|
2457
3296
|
<CapSpin spinning={loadingTags || spin}>
|
|
2458
3297
|
{getMainContent()}
|
|
2459
3298
|
</CapSpin>
|
|
3299
|
+
|
|
2460
3300
|
<TestAndPreviewSlidebox
|
|
2461
3301
|
show={propsShowTestAndPreviewSlidebox || showTestAndPreviewSlidebox}
|
|
2462
3302
|
onClose={handleCloseTestAndPreview}
|
|
2463
|
-
formData={
|
|
2464
|
-
content={
|
|
3303
|
+
formData={testPreviewFormData}
|
|
3304
|
+
content={testAndPreviewContent}
|
|
2465
3305
|
currentChannel={RCS}
|
|
3306
|
+
orgUnitId={orgUnitId}
|
|
3307
|
+
rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
|
|
3308
|
+
smsFallbackContent={
|
|
3309
|
+
smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
|
|
3310
|
+
? {
|
|
3311
|
+
templateContent:
|
|
3312
|
+
smsFallbackData.templateContent || smsFallbackData.content || '',
|
|
3313
|
+
templateName: smsFallbackData.templateName || '',
|
|
3314
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
|
|
3315
|
+
? mergeRcsSmsFallbackVarMapLayers(
|
|
3316
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
3317
|
+
smsFallbackData,
|
|
3318
|
+
)
|
|
3319
|
+
: mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
|
|
3320
|
+
}
|
|
3321
|
+
: null
|
|
3322
|
+
}
|
|
3323
|
+
smsRegister={smsRegister}
|
|
2466
3324
|
/>
|
|
2467
3325
|
</>
|
|
2468
3326
|
);
|
|
2469
3327
|
};
|
|
2470
3328
|
|
|
3329
|
+
|
|
2471
3330
|
const mapStateToProps = createStructuredSelector({
|
|
2472
3331
|
rcsData: makeSelectRcs(),
|
|
2473
3332
|
accountData: makeSelectAccount(),
|
|
2474
3333
|
metaEntities: makeSelectMetaEntities(),
|
|
2475
3334
|
loadingTags: isLoadingMetaEntities(),
|
|
2476
3335
|
injectedTags: setInjectedTags(),
|
|
3336
|
+
currentOrgDetails: selectCurrentOrgDetails(),
|
|
2477
3337
|
});
|
|
2478
3338
|
|
|
2479
3339
|
const mapDispatchToProps = (dispatch) => ({
|
|
2480
3340
|
actions: bindActionCreators(RcsActions, dispatch),
|
|
2481
3341
|
globalActions: bindActionCreators(globalActions, dispatch),
|
|
2482
|
-
templatesActions: bindActionCreators(TemplatesActions, dispatch),
|
|
2483
3342
|
});
|
|
2484
3343
|
|
|
2485
3344
|
const withReducer = injectReducer({ key: 'rcs', reducer: v2RcsReducer });
|