@capillarytech/creatives-library 8.0.358 → 8.0.359-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +29 -0
- package/package.json +1 -1
- package/services/tests/api.test.js +35 -20
- package/utils/commonUtils.js +19 -1
- package/utils/rcsPayloadUtils.js +92 -0
- package/utils/templateVarUtils.js +201 -0
- package/utils/tests/rcsPayloadUtils.test.js +226 -0
- package/utils/tests/templateVarUtils.test.js +204 -0
- package/v2Components/CapActionButton/constants.js +7 -0
- package/v2Components/CapActionButton/index.js +166 -108
- package/v2Components/CapActionButton/index.scss +157 -6
- package/v2Components/CapActionButton/messages.js +19 -3
- package/v2Components/CapActionButton/tests/index.test.js +41 -17
- package/v2Components/CapImageUpload/index.js +2 -2
- package/v2Components/CapTagList/index.js +10 -0
- package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
- package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
- package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
- package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
- package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -76
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +150 -4
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
- package/v2Components/CommonTestAndPreview/constants.js +38 -2
- package/v2Components/CommonTestAndPreview/index.js +810 -222
- package/v2Components/CommonTestAndPreview/messages.js +45 -3
- package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
- package/v2Components/CommonTestAndPreview/sagas.js +25 -6
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
- package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
- package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
- package/v2Components/FormBuilder/index.js +5 -4
- package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
- package/v2Components/SmsFallback/constants.js +73 -0
- package/v2Components/SmsFallback/index.js +956 -0
- package/v2Components/SmsFallback/index.scss +265 -0
- package/v2Components/SmsFallback/messages.js +78 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
- package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
- package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
- package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
- package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
- package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
- package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +37 -22
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +143 -31
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
- package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
- package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
- package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
- package/v2Components/VarSegmentMessageEditor/index.js +125 -0
- package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
- package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
- package/v2Containers/CreativesContainer/constants.js +9 -0
- package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
- package/v2Containers/CreativesContainer/index.js +322 -103
- package/v2Containers/CreativesContainer/index.scss +83 -1
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
- package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
- package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
- package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
- package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
- package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
- package/v2Containers/Rcs/constants.js +120 -11
- package/v2Containers/Rcs/index.js +2577 -812
- package/v2Containers/Rcs/index.scss +281 -8
- package/v2Containers/Rcs/messages.js +34 -3
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98036 -70145
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
- package/v2Containers/Rcs/tests/index.test.js +152 -121
- package/v2Containers/Rcs/tests/mockData.js +38 -0
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
- package/v2Containers/Rcs/tests/utils.test.js +646 -30
- package/v2Containers/Rcs/utils.js +478 -11
- package/v2Containers/Sms/Create/index.js +106 -40
- package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
- package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
- package/v2Containers/SmsTrai/Create/index.js +9 -4
- package/v2Containers/SmsTrai/Edit/constants.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +640 -130
- package/v2Containers/SmsTrai/Edit/index.scss +121 -0
- package/v2Containers/SmsTrai/Edit/messages.js +14 -4
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
- package/v2Containers/SmsWrapper/index.js +37 -8
- package/v2Containers/TagList/index.js +6 -0
- package/v2Containers/Templates/TemplatesActionBar.js +101 -0
- package/v2Containers/Templates/_templates.scss +166 -9
- package/v2Containers/Templates/actions.js +11 -0
- package/v2Containers/Templates/constants.js +2 -0
- package/v2Containers/Templates/index.js +121 -53
- package/v2Containers/Templates/sagas.js +56 -12
- package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
- package/v2Containers/Templates/tests/sagas.test.js +199 -16
- package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
- package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
- package/v2Containers/TemplatesV2/index.js +86 -23
- package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
- package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
- package/v2Containers/Whatsapp/index.js +3 -20
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
|
@@ -31,7 +31,8 @@ import CustomValuesEditor from './CustomValuesEditor';
|
|
|
31
31
|
import SendTestMessage from './SendTestMessage';
|
|
32
32
|
import PreviewSection from './PreviewSection';
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
import * as Api from '../../services/api';
|
|
35
|
+
import { extractTemplateVariables } from '../../utils/templateVarUtils';
|
|
35
36
|
import AddTestCustomerButton from './AddTestCustomer';
|
|
36
37
|
import ExistingCustomerModal from './ExistingCustomerModal';
|
|
37
38
|
// Import constants
|
|
@@ -81,12 +82,24 @@ import {
|
|
|
81
82
|
IMAGE,
|
|
82
83
|
VIDEO,
|
|
83
84
|
URL,
|
|
85
|
+
PREVIEW_TAB_RCS,
|
|
86
|
+
PREVIEW_TAB_SMS_FALLBACK,
|
|
84
87
|
CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
|
|
85
88
|
DAYS,
|
|
89
|
+
RCS_TEST_META_CONTENT_TYPE_RICHCARD,
|
|
90
|
+
RCS_TEST_META_CARD_TYPE_STANDALONE,
|
|
91
|
+
RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
|
|
92
|
+
RCS_TEST_META_CARD_WIDTH_SMALL,
|
|
93
|
+
SMS_MUSTACHE_TAG_PATTERN,
|
|
86
94
|
} from './constants';
|
|
87
|
-
|
|
88
|
-
// Import utilities
|
|
89
95
|
import { getCdnUrl } from '../../utils/cdnTransformation';
|
|
96
|
+
import {
|
|
97
|
+
normalizePreviewApiPayload,
|
|
98
|
+
extractPreviewFromLiquidResponse,
|
|
99
|
+
getSmsFallbackTextForTagExtraction,
|
|
100
|
+
} from './previewApiUtils';
|
|
101
|
+
import { pickFirstSmsFallbackTemplateString } from '../../v2Containers/Rcs/rcsLibraryHydrationUtils';
|
|
102
|
+
|
|
90
103
|
import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
|
|
91
104
|
import { getMembersLookup } from '../../services/api';
|
|
92
105
|
|
|
@@ -109,6 +122,85 @@ const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEch
|
|
|
109
122
|
});
|
|
110
123
|
};
|
|
111
124
|
|
|
125
|
+
/** Preview payload from Redux may be an Immutable Map — normalize for React state. */
|
|
126
|
+
const toPlainPreviewData = (data) => {
|
|
127
|
+
if (data == null) return null;
|
|
128
|
+
const plain = typeof data.toJS === 'function' ? data.toJS() : data;
|
|
129
|
+
return normalizePreviewApiPayload(plain);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Merge existing customValues with tag keys from categorized groups.
|
|
134
|
+
* Each group is { required, optional } (arrays of tag objects with fullPath).
|
|
135
|
+
* Preserves existing values; adds '' for any tag key not yet present.
|
|
136
|
+
* Reusable for RCS+fallback and any flow that needs to ensure customValues has keys for tags.
|
|
137
|
+
*/
|
|
138
|
+
const mergeCustomValuesWithTagKeys = (prev, ...categorizedGroups) => {
|
|
139
|
+
const next = { ...(prev || {}) };
|
|
140
|
+
categorizedGroups.forEach((group) => {
|
|
141
|
+
[...(group.required || []), ...(group.optional || [])].forEach((tag) => {
|
|
142
|
+
const key = tag?.fullPath;
|
|
143
|
+
if (key && next[key] === undefined) next[key] = '';
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
return next;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** True when `body` contains `{{name}}` mustache tokens (user-fillable personalization tags).
|
|
150
|
+
* DLT `{#name#}` slots are pre-bound template variables and are intentionally excluded. */
|
|
151
|
+
const smsTemplateHasMustacheTags = (body) =>
|
|
152
|
+
typeof body === 'string' && SMS_MUSTACHE_TAG_PATTERN.test(body);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build tag rows from `{{…}}` mustache tokens only — DLT `{#…#}` slots are excluded because
|
|
156
|
+
* they are pre-bound template variables, not user-fillable personalization tags.
|
|
157
|
+
* Passing a mustache-only captureRegex to extractTemplateVariables skips the DLT branch.
|
|
158
|
+
* A non-global regex is used so ensureGlobalRegexForExecLoop creates a fresh instance on each call.
|
|
159
|
+
*/
|
|
160
|
+
const buildSyntheticSmsMustacheTags = (body = '') => {
|
|
161
|
+
if (!body || typeof body !== 'string') return [];
|
|
162
|
+
return extractTemplateVariables(body, /\{\{([^}]+)\}\}/).map((name) => ({
|
|
163
|
+
name,
|
|
164
|
+
metaData: { userDriven: false },
|
|
165
|
+
children: [],
|
|
166
|
+
}));
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** RCS createMessageMeta: media shape (mediaUrl, thumbnailUrl, height string). */
|
|
170
|
+
const normalizeRcsTestCardMedia = (media) => {
|
|
171
|
+
if (!media || typeof media !== 'object') return undefined;
|
|
172
|
+
const mediaUrl =
|
|
173
|
+
media.mediaUrl != null && String(media.mediaUrl).trim() !== ''
|
|
174
|
+
? String(media.mediaUrl)
|
|
175
|
+
: media.url != null && String(media.url).trim() !== ''
|
|
176
|
+
? String(media.url)
|
|
177
|
+
: '';
|
|
178
|
+
const thumbnailUrl = media.thumbnailUrl != null ? String(media.thumbnailUrl) : '';
|
|
179
|
+
const height = media.height != null ? String(media.height) : undefined;
|
|
180
|
+
const out = { mediaUrl, thumbnailUrl };
|
|
181
|
+
if (height) out.height = height;
|
|
182
|
+
return out;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** RCS createMessageMeta: suggestion shape (index, type, text, phoneNumber, url, postback). */
|
|
186
|
+
const mapRcsSuggestionForTestMeta = (suggestionRow, index) => ({
|
|
187
|
+
index,
|
|
188
|
+
type: suggestionRow?.type ?? '',
|
|
189
|
+
text: suggestionRow?.text != null ? String(suggestionRow.text) : '',
|
|
190
|
+
phoneNumber:
|
|
191
|
+
suggestionRow?.phoneNumber != null
|
|
192
|
+
? String(suggestionRow.phoneNumber)
|
|
193
|
+
: suggestionRow?.phone_number != null
|
|
194
|
+
? String(suggestionRow.phone_number)
|
|
195
|
+
: '',
|
|
196
|
+
url: suggestionRow?.url !== undefined ? suggestionRow.url : null,
|
|
197
|
+
postback:
|
|
198
|
+
suggestionRow?.postback != null
|
|
199
|
+
? String(suggestionRow.postback)
|
|
200
|
+
: suggestionRow?.text != null
|
|
201
|
+
? String(suggestionRow.text)
|
|
202
|
+
: '',
|
|
203
|
+
});
|
|
112
204
|
|
|
113
205
|
/**
|
|
114
206
|
* CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
|
|
@@ -134,7 +226,7 @@ const testEntityIdsEqual = (a, b) => {
|
|
|
134
226
|
*/
|
|
135
227
|
const CommonTestAndPreview = (props) => {
|
|
136
228
|
const {
|
|
137
|
-
intl: { formatMessage },
|
|
229
|
+
intl: { formatMessage, locale: userLocale = 'en' },
|
|
138
230
|
show,
|
|
139
231
|
onClose,
|
|
140
232
|
channel, // The channel: 'EMAIL', 'SMS', 'RCS', etc.
|
|
@@ -170,13 +262,23 @@ const CommonTestAndPreview = (props) => {
|
|
|
170
262
|
...additionalProps
|
|
171
263
|
} = props;
|
|
172
264
|
|
|
265
|
+
const smsFallbackContent = additionalProps?.smsFallbackContent;
|
|
266
|
+
const smsFallbackTextForTagExtraction = useMemo(
|
|
267
|
+
() => getSmsFallbackTextForTagExtraction(smsFallbackContent),
|
|
268
|
+
[smsFallbackContent],
|
|
269
|
+
);
|
|
173
270
|
// ============================================
|
|
174
271
|
// STATE MANAGEMENT
|
|
175
272
|
// ============================================
|
|
176
273
|
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
|
177
274
|
const [requiredTags, setRequiredTags] = useState([]);
|
|
178
275
|
const [optionalTags, setOptionalTags] = useState([]);
|
|
276
|
+
const [smsFallbackExtractedTags, setSmsFallbackExtractedTags] = useState([]);
|
|
277
|
+
const [smsFallbackRequiredTags, setSmsFallbackRequiredTags] = useState([]);
|
|
278
|
+
const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
|
|
279
|
+
const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
|
|
179
280
|
const [customValues, setCustomValues] = useState({});
|
|
281
|
+
const previewCustomValuesRef = useRef({});
|
|
180
282
|
const [showJSON, setShowJSON] = useState(false);
|
|
181
283
|
const [tagsExtracted, setTagsExtracted] = useState(false);
|
|
182
284
|
|
|
@@ -187,6 +289,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
187
289
|
const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
|
|
188
290
|
|
|
189
291
|
const [previewDevice, setPreviewDevice] = useState(initialDevice);
|
|
292
|
+
const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
|
|
293
|
+
const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
|
|
190
294
|
// Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
|
|
191
295
|
const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
|
|
192
296
|
const [previewDataHtml, setPreviewDataHtml] = useState(() => {
|
|
@@ -219,15 +323,22 @@ const CommonTestAndPreview = (props) => {
|
|
|
219
323
|
[CHANNELS.WHATSAPP]: {
|
|
220
324
|
domainId: null, senderMobNum: '', sourceAccountIdentifier: '',
|
|
221
325
|
},
|
|
326
|
+
[CHANNELS.RCS]: {
|
|
327
|
+
domainId: null,
|
|
328
|
+
domainGatewayMapId: null,
|
|
329
|
+
gsmSenderId: '',
|
|
330
|
+
smsFallbackDomainId: null,
|
|
331
|
+
cdmaSenderId: '', // gsmSenderId = RCS sender (domainId|senderId), cdmaSenderId = SMS fallback
|
|
332
|
+
},
|
|
222
333
|
});
|
|
223
334
|
|
|
224
|
-
const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP];
|
|
335
|
+
const channelsWithDeliverySettings = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS];
|
|
225
336
|
const formDataForSendTest = formData ?? (content && typeof content === 'object' && !Array.isArray(content) ? content : formData);
|
|
226
337
|
const smsTemplateConfigs = formDataForSendTest?.templateConfigs || {};
|
|
227
338
|
const smsTraiDltEnabled = !!smsTemplateConfigs?.traiDltEnabled;
|
|
228
339
|
const registeredSenderIds = smsTemplateConfigs?.registeredSenderIds || [];
|
|
229
340
|
|
|
230
|
-
// Fetch sender details and WeCRM accounts when Test & Preview opens
|
|
341
|
+
// Fetch sender details and WeCRM accounts when Test & Preview opens (SMS, Email, WhatsApp, RCS — same process)
|
|
231
342
|
useEffect(() => {
|
|
232
343
|
if (!show || !channel) {
|
|
233
344
|
return;
|
|
@@ -236,39 +347,99 @@ const CommonTestAndPreview = (props) => {
|
|
|
236
347
|
if (actions.getSenderDetailsRequested) {
|
|
237
348
|
actions.getSenderDetailsRequested({ channel, orgUnitId: orgUnitId ?? -1 });
|
|
238
349
|
}
|
|
350
|
+
// SMS domains/senders are needed for RCS delivery UI (fallback row + slidebox) whenever RCS is open — not only when fallback body exists.
|
|
351
|
+
if (channel === CHANNELS.RCS && actions.getSenderDetailsRequested) {
|
|
352
|
+
actions.getSenderDetailsRequested({ channel: CHANNELS.SMS, orgUnitId: orgUnitId ?? -1 });
|
|
353
|
+
}
|
|
239
354
|
if (channel === CHANNELS.WHATSAPP && actions.getWeCrmAccountsRequested) {
|
|
240
355
|
actions.getWeCrmAccountsRequested({ sourceName: CHANNELS.WHATSAPP });
|
|
241
356
|
}
|
|
242
357
|
}
|
|
243
358
|
}, [show, channel, orgUnitId, actions]);
|
|
244
359
|
|
|
245
|
-
useEffect(() => {
|
|
246
|
-
if (!show) {
|
|
247
|
-
setCustomerModal([false, '']);
|
|
248
|
-
setSearchValue('');
|
|
249
|
-
setCustomerData({ name: '', email: '', mobile: '', customerId: '' });
|
|
250
|
-
}
|
|
251
|
-
}, [show]);
|
|
252
|
-
|
|
253
360
|
const findDefault = (arr) => (arr && arr.find((x) => x.default)) || (arr && arr[0]) || {};
|
|
254
361
|
|
|
255
362
|
// Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
|
|
256
363
|
useEffect(() => {
|
|
257
364
|
if (!channel || !channelsWithDeliverySettings.includes(channel)) return;
|
|
365
|
+
|
|
366
|
+
if (channel === CHANNELS.RCS) {
|
|
367
|
+
const rcsDomainRows = senderDetailsByChannel?.[CHANNELS.RCS] || [];
|
|
368
|
+
const smsFallbackDomainRows = senderDetailsByChannel?.[CHANNELS.SMS] || [];
|
|
369
|
+
if (!rcsDomainRows.length) return;
|
|
370
|
+
|
|
371
|
+
const currentRcsDeliverySettings = testPreviewDeliverySettings?.[CHANNELS.RCS] || {};
|
|
372
|
+
const isRcsGsmSenderUnset = !currentRcsDeliverySettings?.gsmSenderId;
|
|
373
|
+
const isSmsFallbackSenderUnset = !currentRcsDeliverySettings?.cdmaSenderId;
|
|
374
|
+
const isRcsDeliveryFullyUnset = isRcsGsmSenderUnset && isSmsFallbackSenderUnset;
|
|
375
|
+
const shouldOnlyFillSmsFallbackSender =
|
|
376
|
+
!isRcsGsmSenderUnset && isSmsFallbackSenderUnset && smsFallbackDomainRows.length > 0;
|
|
377
|
+
|
|
378
|
+
if (!isRcsDeliveryFullyUnset && !shouldOnlyFillSmsFallbackSender) return;
|
|
379
|
+
|
|
380
|
+
const firstRcsDomain = rcsDomainRows[0];
|
|
381
|
+
const firstSmsFallbackDomain = smsFallbackDomainRows[0];
|
|
382
|
+
const usableRcsGsmSenders = filterUsableGsmSendersForDomain(
|
|
383
|
+
firstRcsDomain,
|
|
384
|
+
firstRcsDomain?.gsmSenders,
|
|
385
|
+
{ skipDomainNameEchoFilter: true },
|
|
386
|
+
);
|
|
387
|
+
const usableSmsFallbackGsmSenders = firstSmsFallbackDomain
|
|
388
|
+
? filterUsableGsmSendersForDomain(firstSmsFallbackDomain, firstSmsFallbackDomain?.gsmSenders)
|
|
389
|
+
: [];
|
|
390
|
+
const defaultRcsGsmSender = usableRcsGsmSenders[0];
|
|
391
|
+
const defaultSmsFallbackGsmSender = usableSmsFallbackGsmSenders[0];
|
|
392
|
+
const rcsSenderCompositeValue =
|
|
393
|
+
firstRcsDomain?.domainId != null && defaultRcsGsmSender?.value != null
|
|
394
|
+
? `${firstRcsDomain.domainId}|${defaultRcsGsmSender.value}`
|
|
395
|
+
: (defaultRcsGsmSender?.value || '');
|
|
396
|
+
const smsFallbackSenderCompositeValue =
|
|
397
|
+
firstSmsFallbackDomain?.domainId != null && defaultSmsFallbackGsmSender?.value != null
|
|
398
|
+
? `${firstSmsFallbackDomain.domainId}|${defaultSmsFallbackGsmSender.value}`
|
|
399
|
+
: (defaultSmsFallbackGsmSender?.value || '');
|
|
400
|
+
|
|
401
|
+
setTestPreviewDeliverySettings((prev) => {
|
|
402
|
+
const previousRcsSettings = prev?.[CHANNELS.RCS] || {};
|
|
403
|
+
if (shouldOnlyFillSmsFallbackSender) {
|
|
404
|
+
if (!smsFallbackSenderCompositeValue) return prev;
|
|
405
|
+
return {
|
|
406
|
+
...prev,
|
|
407
|
+
[CHANNELS.RCS]: {
|
|
408
|
+
...previousRcsSettings,
|
|
409
|
+
smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
|
|
410
|
+
cdmaSenderId: smsFallbackSenderCompositeValue,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
...prev,
|
|
416
|
+
[CHANNELS.RCS]: {
|
|
417
|
+
domainId: firstRcsDomain?.domainId ?? null,
|
|
418
|
+
domainGatewayMapId: firstRcsDomain?.dgmId ?? null,
|
|
419
|
+
gsmSenderId: rcsSenderCompositeValue,
|
|
420
|
+
smsFallbackDomainId: firstSmsFallbackDomain?.domainId ?? null,
|
|
421
|
+
cdmaSenderId: smsFallbackSenderCompositeValue,
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
258
428
|
const domains = senderDetailsByChannel[channel];
|
|
259
429
|
if (!domains || domains.length === 0) return;
|
|
260
430
|
const {
|
|
261
|
-
domainId = '', gsmSenderId = '', senderEmail = '', senderMobNum = '',
|
|
431
|
+
domainId = '', gsmSenderId = '', cdmaSenderId = '', senderEmail = '', senderMobNum = '',
|
|
262
432
|
} = testPreviewDeliverySettings[channel] || {};
|
|
263
|
-
const isEmptySelection = !domainId && !gsmSenderId && !senderEmail && !senderMobNum;
|
|
433
|
+
const isEmptySelection = !domainId && !gsmSenderId && !cdmaSenderId && !senderEmail && !senderMobNum;
|
|
264
434
|
if (!isEmptySelection) return;
|
|
265
435
|
|
|
266
436
|
const whatsappAccountFromForm = channel === CHANNELS.WHATSAPP ? formData?.accountName : undefined;
|
|
267
437
|
const matchedWhatsappAccount = whatsappAccountFromForm
|
|
268
438
|
? (wecrmAccounts || []).find((account) => account?.name === whatsappAccountFromForm)
|
|
269
439
|
: null;
|
|
270
|
-
const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled
|
|
271
|
-
? domains.filter((domain) => (domain
|
|
440
|
+
const smsDomains = channel === CHANNELS.SMS && smsTraiDltEnabled && registeredSenderIds?.length
|
|
441
|
+
? domains.filter((domain) => (domain?.gsmSenders || []).some((gsm) =>
|
|
442
|
+
registeredSenderIds?.includes(gsm?.value)))
|
|
272
443
|
: domains;
|
|
273
444
|
const [defaultDomain] = domains;
|
|
274
445
|
const [firstSmsDomain] = smsDomains;
|
|
@@ -283,8 +454,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
283
454
|
const next = { ...prev };
|
|
284
455
|
if (channel === CHANNELS.SMS) {
|
|
285
456
|
const smsGsmSenders = smsTraiDltEnabled
|
|
286
|
-
? (firstDomain
|
|
287
|
-
: firstDomain
|
|
457
|
+
? (firstDomain?.gsmSenders || []).filter((gsm) => registeredSenderIds?.includes(gsm?.value))
|
|
458
|
+
: firstDomain?.gsmSenders;
|
|
288
459
|
next[channel] = {
|
|
289
460
|
domainId: firstDomain.domainId,
|
|
290
461
|
domainGatewayMapId: firstDomain.dgmId,
|
|
@@ -317,10 +488,29 @@ const CommonTestAndPreview = (props) => {
|
|
|
317
488
|
// MEMOIZED VALUES
|
|
318
489
|
// ============================================
|
|
319
490
|
|
|
491
|
+
const allTags = useMemo(
|
|
492
|
+
() => [...requiredTags, ...optionalTags, ...smsFallbackRequiredTags, ...smsFallbackOptionalTags],
|
|
493
|
+
[requiredTags, optionalTags, smsFallbackRequiredTags, smsFallbackOptionalTags]
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const allRequiredTags = useMemo(
|
|
497
|
+
() => [...requiredTags, ...smsFallbackRequiredTags],
|
|
498
|
+
[requiredTags, smsFallbackRequiredTags]
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const buildEmptyValues = useCallback(
|
|
502
|
+
() => allTags.reduce((acc, tag) => {
|
|
503
|
+
const key = tag?.fullPath;
|
|
504
|
+
if (key) acc[key] = '';
|
|
505
|
+
return acc;
|
|
506
|
+
}, {}),
|
|
507
|
+
[allTags]
|
|
508
|
+
);
|
|
509
|
+
|
|
320
510
|
// Check if update preview button should be disabled
|
|
321
511
|
const isUpdatePreviewDisabled = useMemo(() => (
|
|
322
|
-
|
|
323
|
-
), [
|
|
512
|
+
allRequiredTags.some((tag) => !customValues[tag.fullPath])
|
|
513
|
+
), [allRequiredTags, customValues]);
|
|
324
514
|
|
|
325
515
|
// Get current content based on channel and editor type
|
|
326
516
|
const getCurrentContent = useMemo(() => {
|
|
@@ -364,6 +554,13 @@ const CommonTestAndPreview = (props) => {
|
|
|
364
554
|
return currentTabData.base['sms-editor'];
|
|
365
555
|
}
|
|
366
556
|
}
|
|
557
|
+
// DLT / Test & Preview shape: { templateConfigs: { template, templateId, ... } }
|
|
558
|
+
if (formData.templateConfigs?.template) {
|
|
559
|
+
const smsDltTemplateValue = formData.templateConfigs.template;
|
|
560
|
+
if (typeof smsDltTemplateValue === 'string') return smsDltTemplateValue;
|
|
561
|
+
if (Array.isArray(smsDltTemplateValue)) return smsDltTemplateValue.join('');
|
|
562
|
+
return '';
|
|
563
|
+
}
|
|
367
564
|
}
|
|
368
565
|
|
|
369
566
|
// SMS channel fallback - if formData is not available, use content directly
|
|
@@ -409,7 +606,70 @@ const CommonTestAndPreview = (props) => {
|
|
|
409
606
|
return content || '';
|
|
410
607
|
}, [channel, formData, currentTab, beeContent, content, beeInstance]);
|
|
411
608
|
|
|
412
|
-
|
|
609
|
+
const leftPanelExtractedTags = useMemo(() => {
|
|
610
|
+
if (channel === CHANNELS.SMS) {
|
|
611
|
+
const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
|
|
612
|
+
if (!smsTemplateHasMustacheTags(smsEditorBody)) return [];
|
|
613
|
+
const extractTagsFromApi = extractedTags ?? [];
|
|
614
|
+
if (extractTagsFromApi.length > 0) return extractTagsFromApi;
|
|
615
|
+
return buildSyntheticSmsMustacheTags(smsEditorBody);
|
|
616
|
+
}
|
|
617
|
+
const hasFallbackSmsBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
|
|
618
|
+
if (channel === CHANNELS.RCS && hasFallbackSmsBody) {
|
|
619
|
+
const rcsPrimaryTags = extractedTags ?? [];
|
|
620
|
+
const fallbackSmsTextForTags = smsFallbackTextForTagExtraction ?? '';
|
|
621
|
+
const fallbackSmsTagRows = smsTemplateHasMustacheTags(fallbackSmsTextForTags)
|
|
622
|
+
? (smsFallbackExtractedTags?.length > 0
|
|
623
|
+
? smsFallbackExtractedTags
|
|
624
|
+
: buildSyntheticSmsMustacheTags(fallbackSmsTextForTags))
|
|
625
|
+
: [];
|
|
626
|
+
const mergedRcsAndFallbackTags = [...rcsPrimaryTags, ...fallbackSmsTagRows];
|
|
627
|
+
if (mergedRcsAndFallbackTags.length > 0) return mergedRcsAndFallbackTags;
|
|
628
|
+
return buildSyntheticSmsMustacheTags(fallbackSmsTextForTags);
|
|
629
|
+
}
|
|
630
|
+
return extractedTags ?? [];
|
|
631
|
+
}, [
|
|
632
|
+
channel,
|
|
633
|
+
extractedTags,
|
|
634
|
+
getCurrentContent,
|
|
635
|
+
smsFallbackContent,
|
|
636
|
+
smsFallbackExtractedTags,
|
|
637
|
+
smsFallbackTextForTagExtraction,
|
|
638
|
+
]);
|
|
639
|
+
|
|
640
|
+
const isRcsSmsFallbackPreviewEnabled =
|
|
641
|
+
channel === CHANNELS.RCS
|
|
642
|
+
&& !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
|
|
643
|
+
// Only treat as SMS when user is on the Fallback SMS tab — not whenever fallback exists (RCS tab needs RCS preview API).
|
|
644
|
+
const isSmsFallbackTabActive = isRcsSmsFallbackPreviewEnabled && activePreviewTab === PREVIEW_TAB_SMS_FALLBACK;
|
|
645
|
+
const activeChannelForActions = isSmsFallbackTabActive ? CHANNELS.SMS : channel;
|
|
646
|
+
// VarSegment slot values live in rcsSmsFallbackVarMapped; raw templateContent alone is stale for /preview Body.
|
|
647
|
+
const resolvedSmsFallbackBodyForPreviewTab =
|
|
648
|
+
smsFallbackTextForTagExtraction
|
|
649
|
+
|| smsFallbackContent?.templateContent
|
|
650
|
+
|| smsFallbackContent?.content
|
|
651
|
+
|| '';
|
|
652
|
+
const activeContentForActions = isSmsFallbackTabActive
|
|
653
|
+
? resolvedSmsFallbackBodyForPreviewTab
|
|
654
|
+
: getCurrentContent;
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* SMS fallback pane must show /preview API result when user updated preview on that tab (plain text).
|
|
658
|
+
* Skip when resolvedBody is RCS-shaped (e.g. user last previewed on RCS tab).
|
|
659
|
+
*/
|
|
660
|
+
// smsFallbackPreviewText is the single source of truth for the resolved SMS fallback preview.
|
|
661
|
+
// It is set only by syncSmsFallbackPreview (called from handleUpdatePreview and the
|
|
662
|
+
// prefilled-values effect) and reset to undefined on discard / slidebox close.
|
|
663
|
+
// Using previewDataHtml as a fallback is unsafe because that state is shared with the primary
|
|
664
|
+
// RCS preview and can contain stale SMS or RCS content.
|
|
665
|
+
const smsFallbackResolvedText = useMemo(() => {
|
|
666
|
+
const hasFallbackBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
|
|
667
|
+
if (channel !== CHANNELS.RCS || !hasFallbackBody) return undefined;
|
|
668
|
+
if (smsFallbackPreviewText != null) return smsFallbackPreviewText;
|
|
669
|
+
return undefined;
|
|
670
|
+
}, [channel, smsFallbackContent, smsFallbackPreviewText]);
|
|
671
|
+
|
|
672
|
+
// Build test entities tree data from testCustomers prop
|
|
413
673
|
// Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
|
|
414
674
|
const testEntitiesTreeData = useMemo(() => {
|
|
415
675
|
const groupsNode = {
|
|
@@ -418,7 +678,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
418
678
|
selectable: false,
|
|
419
679
|
children: testGroups?.map((group) => ({
|
|
420
680
|
title: group?.groupName,
|
|
421
|
-
value:
|
|
681
|
+
value: normalizeTestEntityId(group?.groupId),
|
|
422
682
|
})),
|
|
423
683
|
};
|
|
424
684
|
|
|
@@ -428,7 +688,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
428
688
|
selectable: false,
|
|
429
689
|
children: testCustomers?.map((customer) => ({
|
|
430
690
|
title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
|
|
431
|
-
value:
|
|
691
|
+
value: normalizeTestEntityId(customer?.userId ?? customer?.customerId),
|
|
432
692
|
})) || [],
|
|
433
693
|
};
|
|
434
694
|
|
|
@@ -508,11 +768,10 @@ const CommonTestAndPreview = (props) => {
|
|
|
508
768
|
email: customerData?.email || '',
|
|
509
769
|
mobile: customerData?.mobile || '',
|
|
510
770
|
});
|
|
511
|
-
const prefixedAddedId = 'customer:' + normalizedAddedId;
|
|
512
771
|
setSelectedTestEntities((prev) => (
|
|
513
|
-
prev.some((id) => id
|
|
772
|
+
prev.some((id) => testEntityIdsEqual(id, normalizedAddedId))
|
|
514
773
|
? prev
|
|
515
|
-
: [...prev,
|
|
774
|
+
: [...prev, normalizedAddedId]
|
|
516
775
|
));
|
|
517
776
|
}
|
|
518
777
|
handleCloseCustomerModal();
|
|
@@ -576,13 +835,33 @@ const CommonTestAndPreview = (props) => {
|
|
|
576
835
|
whatsappContent: formDataObj?.whatsappContent,
|
|
577
836
|
};
|
|
578
837
|
|
|
579
|
-
case CHANNELS.RCS:
|
|
838
|
+
case CHANNELS.RCS: {
|
|
839
|
+
// For carousel, replace {{N}} tokens in carouselData with resolved semantic vars from
|
|
840
|
+
// formData.cardContent so the preview API can resolve {{member.firstName}} etc.
|
|
841
|
+
const formDataCardContent =
|
|
842
|
+
formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
|
|
843
|
+
?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
|
|
844
|
+
let parsedRcs = {};
|
|
845
|
+
try { parsedRcs = JSON.parse(contentStr); } catch (e) { parsedRcs = typeof contentStr === 'object' ? contentStr : {}; }
|
|
846
|
+
if (Array.isArray(parsedRcs.carouselData) && Array.isArray(formDataCardContent) && formDataCardContent.length > 0) {
|
|
847
|
+
const resolvedCarouselData = parsedRcs.carouselData.map((card, idx) => {
|
|
848
|
+
const formCard = formDataCardContent[idx] || {};
|
|
849
|
+
return {
|
|
850
|
+
...card,
|
|
851
|
+
title: formCard.title || card.title,
|
|
852
|
+
bodyText: formCard.description || card.bodyText,
|
|
853
|
+
};
|
|
854
|
+
});
|
|
855
|
+
return {
|
|
856
|
+
...basePayload,
|
|
857
|
+
messageBody: JSON.stringify({ ...parsedRcs, carouselData: resolvedCarouselData }),
|
|
858
|
+
};
|
|
859
|
+
}
|
|
580
860
|
return {
|
|
581
861
|
...basePayload,
|
|
582
862
|
messageBody: contentStr,
|
|
583
|
-
// messageTitle: resolveTagsInText(formDataObj?.rcsTitle || '', customValuesObj),
|
|
584
|
-
// rcsDesc: formDataObj?.rcsDesc,
|
|
585
863
|
};
|
|
864
|
+
}
|
|
586
865
|
|
|
587
866
|
case CHANNELS.INAPP:
|
|
588
867
|
return {
|
|
@@ -622,6 +901,37 @@ const CommonTestAndPreview = (props) => {
|
|
|
622
901
|
}
|
|
623
902
|
};
|
|
624
903
|
|
|
904
|
+
/**
|
|
905
|
+
* When RCS has SMS fallback, refresh fallback preview text via the same Liquid /preview API
|
|
906
|
+
* (separate call with SMS channel + fallback template body). Used after primary preview updates.
|
|
907
|
+
*/
|
|
908
|
+
const syncSmsFallbackPreview = async (customValuesForResolve, selectedCustomerObj) => {
|
|
909
|
+
const fallbackBodyForLiquidPreview =
|
|
910
|
+
getSmsFallbackTextForTagExtraction(smsFallbackContent)
|
|
911
|
+
|| smsFallbackContent?.templateContent
|
|
912
|
+
|| smsFallbackContent?.content
|
|
913
|
+
|| '';
|
|
914
|
+
if (channel !== CHANNELS.RCS || !String(fallbackBodyForLiquidPreview).trim()) return;
|
|
915
|
+
try {
|
|
916
|
+
const smsFallbackPayload = preparePreviewPayload(
|
|
917
|
+
CHANNELS.SMS,
|
|
918
|
+
formData || {},
|
|
919
|
+
fallbackBodyForLiquidPreview,
|
|
920
|
+
customValuesForResolve,
|
|
921
|
+
selectedCustomerObj
|
|
922
|
+
);
|
|
923
|
+
const fallbackResponse = await Api.updateEmailPreview(smsFallbackPayload);
|
|
924
|
+
const fallbackPreview = extractPreviewFromLiquidResponse(fallbackResponse);
|
|
925
|
+
setSmsFallbackPreviewText(
|
|
926
|
+
typeof fallbackPreview?.resolvedBody === 'string'
|
|
927
|
+
? fallbackPreview.resolvedBody
|
|
928
|
+
: undefined
|
|
929
|
+
);
|
|
930
|
+
} catch (e) {
|
|
931
|
+
/* keep existing smsFallbackPreviewText on failure */
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
625
935
|
/**
|
|
626
936
|
* Prepare payload for tag extraction based on channel
|
|
627
937
|
*/
|
|
@@ -645,11 +955,32 @@ const CommonTestAndPreview = (props) => {
|
|
|
645
955
|
templateContent: contentStr,
|
|
646
956
|
};
|
|
647
957
|
|
|
648
|
-
case CHANNELS.RCS:
|
|
958
|
+
case CHANNELS.RCS: {
|
|
959
|
+
// Carousel templates don't have rcsTitle/rcsDesc — their content lives in cardContent[].
|
|
960
|
+
// Use the resolved card strings from formDataObj (resolved by resolveTemplateWithMap in
|
|
961
|
+
// testPreviewFormData) so the extraction API receives actual Capillary tag expressions
|
|
962
|
+
// instead of numeric {{1}}/{{2}} placeholders.
|
|
963
|
+
const rcsCardContentArr =
|
|
964
|
+
formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
|
|
965
|
+
?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
|
|
966
|
+
if (Array.isArray(rcsCardContentArr) && rcsCardContentArr.length > 0) {
|
|
967
|
+
const carouselTagText = rcsCardContentArr
|
|
968
|
+
.map((c) => `${c.title || ''} ${c.description || ''}`)
|
|
969
|
+
.join(' ')
|
|
970
|
+
.trim();
|
|
971
|
+
if (carouselTagText) {
|
|
972
|
+
return {
|
|
973
|
+
templateSubject: formDataObj?.rcsTitle || '',
|
|
974
|
+
templateContent: carouselTagText,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// Standalone (non-carousel) card: use rcsTitle/rcsDesc directly.
|
|
649
979
|
return {
|
|
650
980
|
templateSubject: formDataObj?.rcsTitle || '',
|
|
651
981
|
templateContent: formDataObj?.rcsDesc || contentStr,
|
|
652
982
|
};
|
|
983
|
+
}
|
|
653
984
|
|
|
654
985
|
case CHANNELS.INAPP:
|
|
655
986
|
return {
|
|
@@ -722,7 +1053,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
722
1053
|
} = carousel || {};
|
|
723
1054
|
const buttonData = buttons.map((button, index) => {
|
|
724
1055
|
const {
|
|
725
|
-
type, text, phone_number, urlType, url,
|
|
1056
|
+
type, text, phone_number: phoneNumber, urlType, url,
|
|
726
1057
|
} = button || {};
|
|
727
1058
|
const buttonObj = {
|
|
728
1059
|
type,
|
|
@@ -730,7 +1061,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
730
1061
|
index,
|
|
731
1062
|
};
|
|
732
1063
|
if (type === PHONE_NUMBER) {
|
|
733
|
-
buttonObj.phoneNumber =
|
|
1064
|
+
buttonObj.phoneNumber = phoneNumber;
|
|
734
1065
|
}
|
|
735
1066
|
if (type === URL) {
|
|
736
1067
|
buttonObj.url = url;
|
|
@@ -759,7 +1090,159 @@ const CommonTestAndPreview = (props) => {
|
|
|
759
1090
|
};
|
|
760
1091
|
});
|
|
761
1092
|
|
|
762
|
-
|
|
1093
|
+
/**
|
|
1094
|
+
* Build createMessageMeta payload for RCS (test message).
|
|
1095
|
+
* rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
|
|
1096
|
+
* Then rcsDeliverySettings, executionParams, clientName last.
|
|
1097
|
+
*/
|
|
1098
|
+
const buildRcsTestMessagePayload = (
|
|
1099
|
+
creativeFormData,
|
|
1100
|
+
_unusedEditorContentString,
|
|
1101
|
+
_customValuesObj,
|
|
1102
|
+
deliverySettingsOverride,
|
|
1103
|
+
basePayload,
|
|
1104
|
+
_rcsTestMetaExtras = {},
|
|
1105
|
+
) => {
|
|
1106
|
+
const rcsSectionFromForm =
|
|
1107
|
+
creativeFormData?.versions?.base?.content?.RCS ?? creativeFormData?.content?.RCS ?? {};
|
|
1108
|
+
const rcsContentFromForm = rcsSectionFromForm?.rcsContent || {};
|
|
1109
|
+
const smsFallbackFromCreativeForm = rcsSectionFromForm?.smsFallBackContent || {};
|
|
1110
|
+
let rcsCardPayloadList = [];
|
|
1111
|
+
if (Array.isArray(rcsContentFromForm?.cardContent)) {
|
|
1112
|
+
rcsCardPayloadList = rcsContentFromForm.cardContent;
|
|
1113
|
+
} else if (rcsContentFromForm?.cardContent) {
|
|
1114
|
+
rcsCardPayloadList = [rcsContentFromForm.cardContent];
|
|
1115
|
+
}
|
|
1116
|
+
// Raw title/description with template tags; SMS fallback uses tagged template fields (pickFirst…).
|
|
1117
|
+
const cardContentForTestMetaApi = rcsCardPayloadList.map((singleRcsCardPayload) => {
|
|
1118
|
+
const normalizedCardMediaForTestApi = singleRcsCardPayload?.media
|
|
1119
|
+
? normalizeRcsTestCardMedia(singleRcsCardPayload.media)
|
|
1120
|
+
: undefined;
|
|
1121
|
+
const suggestionsFromCard = Array.isArray(singleRcsCardPayload?.suggestions)
|
|
1122
|
+
? singleRcsCardPayload.suggestions
|
|
1123
|
+
: [];
|
|
1124
|
+
const suggestionsFormattedForTestMeta = suggestionsFromCard.map((suggestionItem, index) =>
|
|
1125
|
+
mapRcsSuggestionForTestMeta(suggestionItem, index));
|
|
1126
|
+
return {
|
|
1127
|
+
title: singleRcsCardPayload?.title ?? '',
|
|
1128
|
+
description: singleRcsCardPayload?.description ?? '',
|
|
1129
|
+
mediaType: singleRcsCardPayload?.mediaType ?? MEDIA_TYPE_TEXT,
|
|
1130
|
+
...(normalizedCardMediaForTestApi && { media: normalizedCardMediaForTestApi }),
|
|
1131
|
+
...(suggestionsFormattedForTestMeta.length > 0 && {
|
|
1132
|
+
suggestions: suggestionsFormattedForTestMeta,
|
|
1133
|
+
}),
|
|
1134
|
+
};
|
|
1135
|
+
});
|
|
1136
|
+
// Use the component-level smsFallbackContent prop (has rcsSmsFallbackVarMapped) so DLT
|
|
1137
|
+
// {#var#} slots are converted to {{tagName}} mustache tags before sending to createMessageMeta.
|
|
1138
|
+
const smsFallbackTaggedTemplateBody =
|
|
1139
|
+
getSmsFallbackTextForTagExtraction(smsFallbackContent)
|
|
1140
|
+
|| pickFirstSmsFallbackTemplateString(smsFallbackFromCreativeForm)
|
|
1141
|
+
|| '';
|
|
1142
|
+
const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
|
|
1143
|
+
? deliverySettingsOverride.cdmaSenderId.split('|')[1]
|
|
1144
|
+
: deliverySettingsOverride?.cdmaSenderId;
|
|
1145
|
+
const deliveryFallbackSmsId =
|
|
1146
|
+
typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
|
|
1147
|
+
const creativeFallbackSmsId =
|
|
1148
|
+
smsFallbackFromCreativeForm?.senderId != null
|
|
1149
|
+
? String(smsFallbackFromCreativeForm.senderId).trim()
|
|
1150
|
+
: '';
|
|
1151
|
+
const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
|
|
1152
|
+
|
|
1153
|
+
// For DLT orgs: include templateConfigs inside smsFallBackContent so createMessageMeta
|
|
1154
|
+
// receives the DLT template ID, registered sender IDs, and body alongside the message.
|
|
1155
|
+
// Priority: smsFallBackContent.templateConfigs from the RCS payload (clean API shape,
|
|
1156
|
+
// built by createPayload) → fallback to root-level formData.templateConfigs (strip the
|
|
1157
|
+
// UI-only traiDltEnabled flag before sending to the API).
|
|
1158
|
+
const smsFallbackTemplateConfigsForApi = (() => {
|
|
1159
|
+
if (
|
|
1160
|
+
smsFallbackFromCreativeForm?.templateConfigs
|
|
1161
|
+
&& typeof smsFallbackFromCreativeForm.templateConfigs === 'object'
|
|
1162
|
+
) {
|
|
1163
|
+
return smsFallbackFromCreativeForm.templateConfigs;
|
|
1164
|
+
}
|
|
1165
|
+
const rootTc = creativeFormData?.templateConfigs;
|
|
1166
|
+
if (rootTc && typeof rootTc === 'object') {
|
|
1167
|
+
// eslint-disable-next-line no-unused-vars
|
|
1168
|
+
const { traiDltEnabled: _uiFlag, ...tcForApi } = rootTc;
|
|
1169
|
+
return Object.keys(tcForApi).length > 0 ? tcForApi : null;
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1172
|
+
})();
|
|
1173
|
+
|
|
1174
|
+
const smsFallBackContent =
|
|
1175
|
+
smsFallbackTaggedTemplateBody.trim() !== ''
|
|
1176
|
+
? {
|
|
1177
|
+
message: smsFallbackTaggedTemplateBody,
|
|
1178
|
+
...(smsFallbackTemplateConfigsForApi && {
|
|
1179
|
+
templateConfigs: smsFallbackTemplateConfigsForApi,
|
|
1180
|
+
}),
|
|
1181
|
+
}
|
|
1182
|
+
: undefined;
|
|
1183
|
+
|
|
1184
|
+
// accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
|
|
1185
|
+
const accountIdForMeta =
|
|
1186
|
+
rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
|
|
1187
|
+
? String(rcsContentFromForm.accountId)
|
|
1188
|
+
: undefined;
|
|
1189
|
+
|
|
1190
|
+
const rcsRichCardContent = {
|
|
1191
|
+
contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
|
|
1192
|
+
cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
|
|
1193
|
+
cardSettings: rcsContentFromForm?.cardSettings ?? {
|
|
1194
|
+
cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
|
|
1195
|
+
cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
|
|
1196
|
+
},
|
|
1197
|
+
...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
const rcsMessageContent = {
|
|
1201
|
+
channel: CHANNELS.RCS,
|
|
1202
|
+
...(accountIdForMeta && { accountId: accountIdForMeta }),
|
|
1203
|
+
rcsRichCardContent,
|
|
1204
|
+
...(smsFallBackContent && { smsFallBackContent }),
|
|
1205
|
+
};
|
|
1206
|
+
const rcsComposite = deliverySettingsOverride?.gsmSenderId ?? '';
|
|
1207
|
+
const [rcsDomainId, rcsSenderId] = rcsComposite.includes('|') ? rcsComposite.split('|') : ['', rcsComposite];
|
|
1208
|
+
const rcsDeliverySettings = {
|
|
1209
|
+
channelSettings: {
|
|
1210
|
+
channel: CHANNELS.RCS,
|
|
1211
|
+
rcsSender: (rcsSenderId || deliverySettingsOverride?.rcsSender) ?? '',
|
|
1212
|
+
domainId:
|
|
1213
|
+
rcsDomainId !== '' && rcsDomainId !== undefined && !Number.isNaN(Number(rcsDomainId))
|
|
1214
|
+
? Number(rcsDomainId)
|
|
1215
|
+
: (deliverySettingsOverride?.domainId ?? 0),
|
|
1216
|
+
fallbackSmsSenderId: fallbackSmsSenderIdForChannel,
|
|
1217
|
+
},
|
|
1218
|
+
additionalSettings: {
|
|
1219
|
+
useTinyUrl: false,
|
|
1220
|
+
encryptUrl: false,
|
|
1221
|
+
linkTrackingEnabled: false,
|
|
1222
|
+
bypassControlUser: false,
|
|
1223
|
+
userSubscriptionDisabled: false,
|
|
1224
|
+
},
|
|
1225
|
+
};
|
|
1226
|
+
const { clientName: baseClientName = CLIENT_NAME_CREATIVES, ...restBase } = basePayload;
|
|
1227
|
+
return {
|
|
1228
|
+
...restBase,
|
|
1229
|
+
rcsMessageContent,
|
|
1230
|
+
rcsDeliverySettings,
|
|
1231
|
+
executionParams: {},
|
|
1232
|
+
clientName: baseClientName,
|
|
1233
|
+
};
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
const prepareTestMessagePayload = (
|
|
1237
|
+
channelType,
|
|
1238
|
+
formDataObj,
|
|
1239
|
+
contentStr,
|
|
1240
|
+
customValuesObj,
|
|
1241
|
+
recipientDetails,
|
|
1242
|
+
previewDataObj,
|
|
1243
|
+
deliverySettingsOverride,
|
|
1244
|
+
rcsExtra = {},
|
|
1245
|
+
) => {
|
|
763
1246
|
// Base payload structure common to all channels
|
|
764
1247
|
const basePayload = {
|
|
765
1248
|
ouId: -1,
|
|
@@ -840,12 +1323,12 @@ const CommonTestAndPreview = (props) => {
|
|
|
840
1323
|
},
|
|
841
1324
|
smsDeliverySettings: {
|
|
842
1325
|
channelSettings: {
|
|
1326
|
+
channel: CHANNELS.SMS,
|
|
843
1327
|
gsmSenderId: deliverySettingsOverride?.gsmSenderId ?? '',
|
|
844
1328
|
domainId: deliverySettingsOverride?.domainId ?? null,
|
|
845
1329
|
domainGatewayMapId: deliverySettingsOverride?.domainGatewayMapId ?? '',
|
|
846
1330
|
targetNdnc: false,
|
|
847
1331
|
cdmaSenderId: deliverySettingsOverride?.cdmaSenderId ?? '',
|
|
848
|
-
channel: CHANNELS.SMS,
|
|
849
1332
|
},
|
|
850
1333
|
additionalSettings: {
|
|
851
1334
|
useTinyUrl: false,
|
|
@@ -989,7 +1472,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
989
1472
|
return {
|
|
990
1473
|
...basePayload,
|
|
991
1474
|
whatsappMessageContent: {
|
|
992
|
-
messageBody: templateEditorValue || '',
|
|
1475
|
+
messageBody: resolvedMessageBody || templateEditorValue || '',
|
|
993
1476
|
accountId: formDataObj?.accountId || '',
|
|
994
1477
|
sourceAccountIdentifier: sourceAccountIdentifier || formDataObj?.sourceAccountIdentifier || '',
|
|
995
1478
|
accountName: formDataObj?.accountName || '',
|
|
@@ -1014,16 +1497,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
1014
1497
|
}
|
|
1015
1498
|
|
|
1016
1499
|
case CHANNELS.RCS:
|
|
1017
|
-
return
|
|
1018
|
-
...basePayload,
|
|
1019
|
-
rcsMessageContent: {
|
|
1020
|
-
channel: CHANNELS.RCS,
|
|
1021
|
-
messageBody: contentStr,
|
|
1022
|
-
rcsType: additionalProps?.rcsType,
|
|
1023
|
-
rcsImageSrc: formDataObj?.rcsImageSrc,
|
|
1024
|
-
rcsSuggestions: formDataObj?.rcsSuggestions,
|
|
1025
|
-
},
|
|
1026
|
-
};
|
|
1500
|
+
return buildRcsTestMessagePayload(formDataObj, contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra);
|
|
1027
1501
|
|
|
1028
1502
|
case CHANNELS.INAPP: {
|
|
1029
1503
|
// InApp payload structure similar to MobilePush
|
|
@@ -1862,6 +2336,46 @@ const CommonTestAndPreview = (props) => {
|
|
|
1862
2336
|
*
|
|
1863
2337
|
* IMPORTANT: Use raw content/formData initially, only use previewDataHtml if preview call was made
|
|
1864
2338
|
*/
|
|
2339
|
+
// Memoized RCS carousel content: only recomputes when the API response or raw template changes,
|
|
2340
|
+
// NOT on every customValues keystroke. Uses previewCustomValuesRef captured at Update Preview time.
|
|
2341
|
+
const rcsCarouselContentObj = useMemo(() => {
|
|
2342
|
+
if (channel !== CHANNELS.RCS) return null;
|
|
2343
|
+
let resolvedContent = null;
|
|
2344
|
+
if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
|
|
2345
|
+
resolvedContent = previewDataHtml.resolvedBody;
|
|
2346
|
+
if (typeof resolvedContent === 'string') {
|
|
2347
|
+
try { resolvedContent = JSON.parse(resolvedContent); } catch (e) { resolvedContent = null; }
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
if (resolvedContent && typeof resolvedContent === 'object') return resolvedContent;
|
|
2351
|
+
|
|
2352
|
+
let rawRcs = content;
|
|
2353
|
+
if (typeof rawRcs === 'string') {
|
|
2354
|
+
try { rawRcs = JSON.parse(rawRcs); } catch (e) { rawRcs = content; }
|
|
2355
|
+
}
|
|
2356
|
+
const snapCustomValues = previewCustomValuesRef.current;
|
|
2357
|
+
const hasEnteredValues = snapCustomValues && Object.keys(snapCustomValues).some((k) => snapCustomValues[k]);
|
|
2358
|
+
if (hasPreviewCallBeenMade && hasEnteredValues && Array.isArray(rawRcs?.carouselData)) {
|
|
2359
|
+
const formDataCardContent =
|
|
2360
|
+
formData?.versions?.base?.content?.RCS?.rcsContent?.cardContent
|
|
2361
|
+
?? formData?.content?.RCS?.rcsContent?.cardContent
|
|
2362
|
+
?? [];
|
|
2363
|
+
return {
|
|
2364
|
+
...rawRcs,
|
|
2365
|
+
carouselData: rawRcs.carouselData.map((card, idx) => {
|
|
2366
|
+
const formCard = formDataCardContent[idx] || {};
|
|
2367
|
+
return {
|
|
2368
|
+
...card,
|
|
2369
|
+
title: resolveTagsInText(formCard.title || card.title || '', snapCustomValues),
|
|
2370
|
+
bodyText: resolveTagsInText(formCard.description || card.bodyText || '', snapCustomValues),
|
|
2371
|
+
};
|
|
2372
|
+
}),
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
return rawRcs || {};
|
|
2376
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2377
|
+
}, [channel, content, hasPreviewCallBeenMade, previewDataHtml, formData]);
|
|
2378
|
+
|
|
1865
2379
|
const prepareUnifiedPreviewProps = () => {
|
|
1866
2380
|
// Prepare content based on channel
|
|
1867
2381
|
let contentObj = {};
|
|
@@ -1923,42 +2437,14 @@ const CommonTestAndPreview = (props) => {
|
|
|
1923
2437
|
contentObj = parsedContent || {};
|
|
1924
2438
|
}
|
|
1925
2439
|
} else if (channel === CHANNELS.RCS) {
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
// Only use previewDataHtml if preview call was made
|
|
1931
|
-
if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
|
|
1932
|
-
resolvedContent = previewDataHtml.resolvedBody;
|
|
1933
|
-
|
|
1934
|
-
// Handle case where resolvedBody might be a JSON string
|
|
1935
|
-
if (typeof resolvedContent === 'string') {
|
|
1936
|
-
try {
|
|
1937
|
-
resolvedContent = JSON.parse(resolvedContent);
|
|
1938
|
-
} catch (e) {
|
|
1939
|
-
// If parsing fails, treat as null so we fall back to content
|
|
1940
|
-
resolvedContent = null;
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
// Parse content if it's a string
|
|
2440
|
+
// rcsCarouselContentObj is a useMemo that handles both the API-response path and the
|
|
2441
|
+
// client-side fallback. It is stable across keystrokes (only recomputes when
|
|
2442
|
+
// hasPreviewCallBeenMade / previewDataHtml / content / formData change).
|
|
1946
2443
|
let parsedRcsContent = content;
|
|
1947
2444
|
if (typeof content === 'string') {
|
|
1948
|
-
try {
|
|
1949
|
-
parsedRcsContent = JSON.parse(content);
|
|
1950
|
-
} catch (e) {
|
|
1951
|
-
parsedRcsContent = content;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
// Use resolvedContent if available (from preview call), otherwise use raw content
|
|
1956
|
-
if (resolvedContent && typeof resolvedContent === 'object') {
|
|
1957
|
-
contentObj = { ...resolvedContent };
|
|
1958
|
-
} else {
|
|
1959
|
-
// Use raw content if no preview call was made or resolvedContent is not available
|
|
1960
|
-
contentObj = parsedRcsContent || {};
|
|
2445
|
+
try { parsedRcsContent = JSON.parse(content); } catch (e) { parsedRcsContent = content; }
|
|
1961
2446
|
}
|
|
2447
|
+
contentObj = rcsCarouselContentObj || parsedRcsContent || {};
|
|
1962
2448
|
} else if (channel === CHANNELS.INAPP) {
|
|
1963
2449
|
// For InApp, content is an object with androidContent and iosContent (similar to MobilePush)
|
|
1964
2450
|
// Content comes from InApp component state, passed via content prop
|
|
@@ -2161,6 +2647,10 @@ const CommonTestAndPreview = (props) => {
|
|
|
2161
2647
|
formatMessage,
|
|
2162
2648
|
lastModified: formData?.lastModified,
|
|
2163
2649
|
updatedByName: formData?.updatedByName,
|
|
2650
|
+
smsFallbackContent: isRcsSmsFallbackPreviewEnabled ? smsFallbackContent : null,
|
|
2651
|
+
smsFallbackResolvedText,
|
|
2652
|
+
activePreviewTab,
|
|
2653
|
+
onPreviewTabChange: setActivePreviewTab,
|
|
2164
2654
|
};
|
|
2165
2655
|
};
|
|
2166
2656
|
|
|
@@ -2200,7 +2690,12 @@ const CommonTestAndPreview = (props) => {
|
|
|
2200
2690
|
}, [show, beeInstance, currentTab, channel]);
|
|
2201
2691
|
|
|
2202
2692
|
/**
|
|
2203
|
-
* Initial data load when slidebox opens
|
|
2693
|
+
* Initial data load when slidebox opens.
|
|
2694
|
+
* EXTRACT TAGS CALL SITES (on open/edit RCS):
|
|
2695
|
+
* 1. Here (non-email): actions.extractTagsRequested() at line ~2161 when show is true.
|
|
2696
|
+
* 2. RCS SMS fallback useEffect below: Api.extractTagsWithMetaData() for fallback message.
|
|
2697
|
+
* 3. handleExtractTags() (user clicks "Enter custom values for tags") – not from effects.
|
|
2698
|
+
* The "Process extracted tags" effect only processes API results and must not call extract again.
|
|
2204
2699
|
*/
|
|
2205
2700
|
useEffect(() => {
|
|
2206
2701
|
if (show) {
|
|
@@ -2285,7 +2780,61 @@ const CommonTestAndPreview = (props) => {
|
|
|
2285
2780
|
actions.getTestGroupsRequested();
|
|
2286
2781
|
}
|
|
2287
2782
|
}
|
|
2288
|
-
|
|
2783
|
+
// getCurrentContent: RCS applies cardVarMapped → placeholder resolution; re-extract when it changes.
|
|
2784
|
+
}, [show, beeInstance, currentTab, channel, getCurrentContent]);
|
|
2785
|
+
|
|
2786
|
+
/**
|
|
2787
|
+
* RCS with SMS fallback: extract tags for fallback SMS content as well
|
|
2788
|
+
* (so we can show a separate "Fallback SMS tags" section in left panel).
|
|
2789
|
+
*/
|
|
2790
|
+
useEffect(() => {
|
|
2791
|
+
let cancelled = false;
|
|
2792
|
+
|
|
2793
|
+
if (!show || channel !== CHANNELS.RCS) {
|
|
2794
|
+
return () => {
|
|
2795
|
+
cancelled = true;
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
if (!smsFallbackContent?.templateContent && !smsFallbackContent?.content) {
|
|
2800
|
+
setSmsFallbackExtractedTags([]);
|
|
2801
|
+
setSmsFallbackRequiredTags([]);
|
|
2802
|
+
setSmsFallbackOptionalTags([]);
|
|
2803
|
+
return () => {
|
|
2804
|
+
cancelled = true;
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
setIsExtractingSmsFallbackTags(true);
|
|
2809
|
+
(async () => {
|
|
2810
|
+
try {
|
|
2811
|
+
const fallbackBodyForExtractApi = getSmsFallbackTextForTagExtraction(smsFallbackContent);
|
|
2812
|
+
const payload = {
|
|
2813
|
+
messageTitle: '',
|
|
2814
|
+
messageBody: fallbackBodyForExtractApi || '',
|
|
2815
|
+
};
|
|
2816
|
+
const response = await Api.extractTagsWithMetaData(payload); //not using saga action here because we dont store fallbacksms related data in store but only in useState since this is only used in RCS SMS fallback
|
|
2817
|
+
let smsFallbackTagTree = response?.data ?? [];
|
|
2818
|
+
if (!Array.isArray(smsFallbackTagTree)) smsFallbackTagTree = [];
|
|
2819
|
+
if (!smsTemplateHasMustacheTags(fallbackBodyForExtractApi)) {
|
|
2820
|
+
smsFallbackTagTree = [];
|
|
2821
|
+
} else if (smsFallbackTagTree.length === 0) {
|
|
2822
|
+
smsFallbackTagTree = buildSyntheticSmsMustacheTags(fallbackBodyForExtractApi);
|
|
2823
|
+
}
|
|
2824
|
+
if (cancelled) return;
|
|
2825
|
+
setSmsFallbackExtractedTags(smsFallbackTagTree);
|
|
2826
|
+
} catch (e) {
|
|
2827
|
+
if (cancelled) return;
|
|
2828
|
+
setSmsFallbackExtractedTags([]);
|
|
2829
|
+
} finally {
|
|
2830
|
+
if (!cancelled) setIsExtractingSmsFallbackTags(false);
|
|
2831
|
+
}
|
|
2832
|
+
})();
|
|
2833
|
+
|
|
2834
|
+
return () => {
|
|
2835
|
+
cancelled = true;
|
|
2836
|
+
};
|
|
2837
|
+
}, [show, channel, smsFallbackContent]);
|
|
2289
2838
|
|
|
2290
2839
|
/**
|
|
2291
2840
|
* Email-specific: Handle content updates for both BEE and CKEditor
|
|
@@ -2337,15 +2886,22 @@ const CommonTestAndPreview = (props) => {
|
|
|
2337
2886
|
setSelectedCustomer(null);
|
|
2338
2887
|
setRequiredTags([]);
|
|
2339
2888
|
setOptionalTags([]);
|
|
2889
|
+
setSmsFallbackExtractedTags([]);
|
|
2890
|
+
setSmsFallbackRequiredTags([]);
|
|
2891
|
+
setSmsFallbackOptionalTags([]);
|
|
2892
|
+
setIsExtractingSmsFallbackTags(false);
|
|
2340
2893
|
setCustomValues({});
|
|
2341
2894
|
setShowJSON(false);
|
|
2342
2895
|
setTagsExtracted(false);
|
|
2343
2896
|
setPreviewDevice(DESKTOP);
|
|
2897
|
+
setActivePreviewTab(PREVIEW_TAB_RCS);
|
|
2898
|
+
setSmsFallbackPreviewText(undefined);
|
|
2344
2899
|
setSelectedTestEntities([]);
|
|
2345
2900
|
actions.clearPrefilledValues();
|
|
2346
2901
|
} else {
|
|
2347
2902
|
// Reset device to initialDevice when opening (Android for mobile channels, Desktop for others)
|
|
2348
2903
|
setPreviewDevice(initialDevice);
|
|
2904
|
+
setActivePreviewTab(PREVIEW_TAB_RCS);
|
|
2349
2905
|
}
|
|
2350
2906
|
}, [show, initialDevice]);
|
|
2351
2907
|
|
|
@@ -2354,79 +2910,10 @@ const CommonTestAndPreview = (props) => {
|
|
|
2354
2910
|
*/
|
|
2355
2911
|
useEffect(() => {
|
|
2356
2912
|
if (previewData) {
|
|
2357
|
-
setPreviewDataHtml(previewData);
|
|
2913
|
+
setPreviewDataHtml(toPlainPreviewData(previewData));
|
|
2358
2914
|
}
|
|
2359
2915
|
}, [previewData]);
|
|
2360
2916
|
|
|
2361
|
-
/**
|
|
2362
|
-
* Process extracted tags and categorize them
|
|
2363
|
-
*/
|
|
2364
|
-
useEffect(() => {
|
|
2365
|
-
// Categorize tags into required and optional
|
|
2366
|
-
const required = [];
|
|
2367
|
-
const optional = [];
|
|
2368
|
-
let hasPersonalizationTags = false;
|
|
2369
|
-
|
|
2370
|
-
if (extractedTags?.length > 0) {
|
|
2371
|
-
const processTag = (tag, parentPath = '') => {
|
|
2372
|
-
const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
|
|
2373
|
-
|
|
2374
|
-
// Skip unsubscribe tag for input fields
|
|
2375
|
-
if (tag?.name === UNSUBSCRIBE_TAG_NAME) {
|
|
2376
|
-
return;
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
hasPersonalizationTags = true;
|
|
2380
|
-
|
|
2381
|
-
if (tag?.metaData?.userDriven === false) {
|
|
2382
|
-
required.push({
|
|
2383
|
-
...tag,
|
|
2384
|
-
fullPath: currentPath,
|
|
2385
|
-
});
|
|
2386
|
-
} else if (tag?.metaData?.userDriven === true) {
|
|
2387
|
-
optional.push({
|
|
2388
|
-
...tag,
|
|
2389
|
-
fullPath: currentPath,
|
|
2390
|
-
});
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
if (tag?.children?.length > 0) {
|
|
2394
|
-
tag.children.forEach((child) => processTag(child, currentPath));
|
|
2395
|
-
}
|
|
2396
|
-
};
|
|
2397
|
-
|
|
2398
|
-
extractedTags.forEach((tag) => processTag(tag));
|
|
2399
|
-
|
|
2400
|
-
if (hasPersonalizationTags) {
|
|
2401
|
-
setRequiredTags(required);
|
|
2402
|
-
setOptionalTags(optional);
|
|
2403
|
-
setTagsExtracted(true); // Mark tags as extracted and processed
|
|
2404
|
-
|
|
2405
|
-
// Initialize custom values for required tags
|
|
2406
|
-
const initialValues = {};
|
|
2407
|
-
required.forEach((tag) => {
|
|
2408
|
-
initialValues[tag?.fullPath] = '';
|
|
2409
|
-
});
|
|
2410
|
-
optional.forEach((tag) => {
|
|
2411
|
-
initialValues[tag?.fullPath] = '';
|
|
2412
|
-
});
|
|
2413
|
-
setCustomValues(initialValues);
|
|
2414
|
-
} else {
|
|
2415
|
-
// Reset all tag-related state if no personalization tags
|
|
2416
|
-
setRequiredTags([]);
|
|
2417
|
-
setOptionalTags([]);
|
|
2418
|
-
setCustomValues({});
|
|
2419
|
-
setTagsExtracted(false);
|
|
2420
|
-
}
|
|
2421
|
-
} else {
|
|
2422
|
-
// Reset all tag-related state if no tags
|
|
2423
|
-
setRequiredTags([]);
|
|
2424
|
-
setOptionalTags([]);
|
|
2425
|
-
setCustomValues({});
|
|
2426
|
-
setTagsExtracted(false);
|
|
2427
|
-
}
|
|
2428
|
-
}, [extractedTags]);
|
|
2429
|
-
|
|
2430
2917
|
/**
|
|
2431
2918
|
* Handle customer selection and fetch prefilled values
|
|
2432
2919
|
*/
|
|
@@ -2434,17 +2921,15 @@ const CommonTestAndPreview = (props) => {
|
|
|
2434
2921
|
if (selectedCustomer && config.enableCustomerSearch !== false) {
|
|
2435
2922
|
setTagsExtracted(true); // Auto-open custom values editor
|
|
2436
2923
|
|
|
2437
|
-
// Get all available tags
|
|
2438
|
-
const allTags = [...requiredTags, ...optionalTags];
|
|
2439
2924
|
const requiredTagObj = {};
|
|
2440
|
-
|
|
2925
|
+
allRequiredTags.forEach((tag) => {
|
|
2441
2926
|
requiredTagObj[tag?.fullPath] = '';
|
|
2442
2927
|
});
|
|
2443
2928
|
if (allTags.length > 0) {
|
|
2444
2929
|
const payload = preparePreviewPayload(
|
|
2445
|
-
|
|
2930
|
+
activeChannelForActions,
|
|
2446
2931
|
formData || {},
|
|
2447
|
-
|
|
2932
|
+
activeContentForActions,
|
|
2448
2933
|
{
|
|
2449
2934
|
...requiredTagObj,
|
|
2450
2935
|
},
|
|
@@ -2453,7 +2938,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
2453
2938
|
actions.getPrefilledValuesRequested(payload);
|
|
2454
2939
|
}
|
|
2455
2940
|
}
|
|
2456
|
-
}, [selectedCustomer]);
|
|
2941
|
+
}, [selectedCustomer, allTags.length, activeChannelForActions, activePreviewTab]);
|
|
2457
2942
|
|
|
2458
2943
|
/**
|
|
2459
2944
|
* Update custom values with prefilled values from API
|
|
@@ -2464,24 +2949,29 @@ const CommonTestAndPreview = (props) => {
|
|
|
2464
2949
|
if (prefilledValues && selectedCustomer) {
|
|
2465
2950
|
// Always replace all values with prefilled values
|
|
2466
2951
|
const updatedValues = {};
|
|
2467
|
-
|
|
2952
|
+
allTags.forEach((tag) => {
|
|
2468
2953
|
updatedValues[tag?.fullPath] = prefilledValues[tag?.fullPath] || '';
|
|
2469
2954
|
});
|
|
2470
2955
|
|
|
2471
2956
|
setCustomValues(updatedValues);
|
|
2472
2957
|
|
|
2473
2958
|
// Update preview with prefilled values (this is a valid preview call trigger)
|
|
2959
|
+
// For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
|
|
2960
|
+
// SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
|
|
2961
|
+
const previewChannelForPrefill = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
|
|
2962
|
+
const previewContentForPrefill = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
|
|
2474
2963
|
const payload = preparePreviewPayload(
|
|
2475
|
-
|
|
2964
|
+
previewChannelForPrefill,
|
|
2476
2965
|
formData || {},
|
|
2477
|
-
|
|
2966
|
+
previewContentForPrefill,
|
|
2478
2967
|
updatedValues,
|
|
2479
2968
|
selectedCustomer
|
|
2480
2969
|
);
|
|
2481
2970
|
actions.updatePreviewRequested(payload);
|
|
2482
2971
|
setHasPreviewCallBeenMade(true); // Mark that preview call was made
|
|
2972
|
+
void syncSmsFallbackPreview(updatedValues, selectedCustomer);
|
|
2483
2973
|
}
|
|
2484
|
-
}, [JSON.stringify(prefilledValues), selectedCustomer]);
|
|
2974
|
+
}, [JSON.stringify(prefilledValues), selectedCustomer, activeChannelForActions, activePreviewTab]);
|
|
2485
2975
|
|
|
2486
2976
|
/**
|
|
2487
2977
|
* Map channel constants to display names (lowercase for message)
|
|
@@ -2575,11 +3065,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
2575
3065
|
setTagsExtracted(true); // Auto-open custom values editor
|
|
2576
3066
|
|
|
2577
3067
|
// Clear any existing values while waiting for prefilled values
|
|
2578
|
-
|
|
2579
|
-
[...requiredTags, ...optionalTags].forEach((tag) => {
|
|
2580
|
-
emptyValues[tag?.fullPath] = '';
|
|
2581
|
-
});
|
|
2582
|
-
setCustomValues(emptyValues);
|
|
3068
|
+
setCustomValues(buildEmptyValues());
|
|
2583
3069
|
};
|
|
2584
3070
|
|
|
2585
3071
|
/**
|
|
@@ -2593,11 +3079,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
2593
3079
|
actions.clearPreviewErrors();
|
|
2594
3080
|
|
|
2595
3081
|
// Initialize empty values for all tags
|
|
2596
|
-
|
|
2597
|
-
[...requiredTags, ...optionalTags].forEach((tag) => {
|
|
2598
|
-
emptyValues[tag?.fullPath] = '';
|
|
2599
|
-
});
|
|
2600
|
-
setCustomValues(emptyValues);
|
|
3082
|
+
setCustomValues(buildEmptyValues());
|
|
2601
3083
|
|
|
2602
3084
|
// Don't make preview call when clearing selection - just reset to raw content
|
|
2603
3085
|
// Preview will be shown using raw formData/content
|
|
@@ -2633,17 +3115,20 @@ const CommonTestAndPreview = (props) => {
|
|
|
2633
3115
|
*/
|
|
2634
3116
|
const handleDiscardCustomValues = () => {
|
|
2635
3117
|
// Initialize empty values for all tags
|
|
2636
|
-
const emptyValues =
|
|
2637
|
-
[...requiredTags, ...optionalTags].forEach((tag) => {
|
|
2638
|
-
emptyValues[tag?.fullPath] = '';
|
|
2639
|
-
});
|
|
3118
|
+
const emptyValues = buildEmptyValues();
|
|
2640
3119
|
setCustomValues(emptyValues);
|
|
2641
3120
|
|
|
3121
|
+
// Reset SMS fallback preview so it shows raw template (with {{tags}} visible) after discard
|
|
3122
|
+
setSmsFallbackPreviewText(undefined);
|
|
3123
|
+
|
|
2642
3124
|
// Update preview with empty values (this is a valid preview call trigger)
|
|
3125
|
+
// For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped.
|
|
3126
|
+
const previewChannelForDiscard = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
|
|
3127
|
+
const previewContentForDiscard = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
|
|
2643
3128
|
const payload = preparePreviewPayload(
|
|
2644
|
-
|
|
3129
|
+
previewChannelForDiscard,
|
|
2645
3130
|
formData || {},
|
|
2646
|
-
|
|
3131
|
+
previewContentForDiscard,
|
|
2647
3132
|
emptyValues,
|
|
2648
3133
|
selectedCustomer
|
|
2649
3134
|
);
|
|
@@ -2657,14 +3142,24 @@ const CommonTestAndPreview = (props) => {
|
|
|
2657
3142
|
*/
|
|
2658
3143
|
const handleUpdatePreview = async () => {
|
|
2659
3144
|
try {
|
|
3145
|
+
// Capture customValues at click time so the carousel preview only updates here,
|
|
3146
|
+
// not on every subsequent keystroke.
|
|
3147
|
+
previewCustomValuesRef.current = customValues;
|
|
3148
|
+
// For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped,
|
|
3149
|
+
// even when the user triggers update from the SMS fallback tab.
|
|
3150
|
+
// SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
|
|
3151
|
+
const previewChannel = channel === CHANNELS.RCS ? CHANNELS.RCS : activeChannelForActions;
|
|
3152
|
+
const previewContent = channel === CHANNELS.RCS ? getCurrentContent : activeContentForActions;
|
|
2660
3153
|
const payload = preparePreviewPayload(
|
|
2661
|
-
|
|
3154
|
+
previewChannel,
|
|
2662
3155
|
formData || {},
|
|
2663
|
-
|
|
3156
|
+
previewContent,
|
|
2664
3157
|
customValues,
|
|
2665
3158
|
selectedCustomer
|
|
2666
3159
|
);
|
|
2667
3160
|
await actions.updatePreviewRequested(payload);
|
|
3161
|
+
|
|
3162
|
+
await syncSmsFallbackPreview(customValues, selectedCustomer);
|
|
2668
3163
|
setHasPreviewCallBeenMade(true); // Mark that preview call was made
|
|
2669
3164
|
} catch (error) {
|
|
2670
3165
|
CapNotification.error({
|
|
@@ -2674,25 +3169,115 @@ const CommonTestAndPreview = (props) => {
|
|
|
2674
3169
|
};
|
|
2675
3170
|
|
|
2676
3171
|
/**
|
|
2677
|
-
*
|
|
3172
|
+
* Categorize extracted tags into required/optional.
|
|
2678
3173
|
*/
|
|
2679
|
-
const
|
|
2680
|
-
|
|
2681
|
-
|
|
3174
|
+
const categorizeTags = (tagsTree = []) => {
|
|
3175
|
+
const required = [];
|
|
3176
|
+
const optional = [];
|
|
3177
|
+
let hasPersonalizationTags = false;
|
|
3178
|
+
const processTag = (tag, parentPath = '') => {
|
|
3179
|
+
const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
|
|
3180
|
+
|
|
3181
|
+
// Skip unsubscribe tag for input fields
|
|
3182
|
+
if (tag?.name === UNSUBSCRIBE_TAG_NAME) return;
|
|
3183
|
+
|
|
3184
|
+
hasPersonalizationTags = true;
|
|
3185
|
+
const userDriven = tag?.metaData?.userDriven;
|
|
3186
|
+
if (userDriven === true) {
|
|
3187
|
+
optional.push({ ...tag, fullPath: currentPath });
|
|
3188
|
+
} else {
|
|
3189
|
+
// false or missing (SMS/DLT extract often omits metaData) → required for test values
|
|
3190
|
+
required.push({ ...tag, fullPath: currentPath });
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
if (tag?.children?.length > 0) {
|
|
3194
|
+
tag.children.forEach((child) => processTag(child, currentPath));
|
|
3195
|
+
}
|
|
3196
|
+
};
|
|
3197
|
+
|
|
3198
|
+
(tagsTree || []).forEach((tag) => processTag(tag));
|
|
3199
|
+
return { required, optional, hasPersonalizationTags };
|
|
3200
|
+
};
|
|
3201
|
+
|
|
3202
|
+
/**
|
|
3203
|
+
* Apply tag extraction when content comes from RCS + SMS fallback (no API call).
|
|
3204
|
+
*/
|
|
3205
|
+
const applyRcsSmsFallbackTagExtraction = () => {
|
|
3206
|
+
const rcsPrimaryCategorized = categorizeTags(extractedTags ?? []);
|
|
3207
|
+
const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
|
|
3208
|
+
let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
|
|
3209
|
+
? smsFallbackExtractedTags
|
|
3210
|
+
: buildSyntheticSmsMustacheTags(fallbackSmsResolvedForTags);
|
|
3211
|
+
if (!smsTemplateHasMustacheTags(fallbackSmsResolvedForTags)) {
|
|
3212
|
+
fallbackSmsTagTree = [];
|
|
3213
|
+
}
|
|
3214
|
+
const fallbackSmsCategorized = categorizeTags(fallbackSmsTagTree);
|
|
3215
|
+
setRequiredTags(rcsPrimaryCategorized.required);
|
|
3216
|
+
setOptionalTags(rcsPrimaryCategorized.optional);
|
|
3217
|
+
setSmsFallbackRequiredTags(fallbackSmsCategorized.required);
|
|
3218
|
+
setSmsFallbackOptionalTags(fallbackSmsCategorized.optional);
|
|
3219
|
+
setTagsExtracted(
|
|
3220
|
+
rcsPrimaryCategorized.hasPersonalizationTags || fallbackSmsCategorized.hasPersonalizationTags,
|
|
3221
|
+
);
|
|
3222
|
+
setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, rcsPrimaryCategorized, fallbackSmsCategorized));
|
|
3223
|
+
};
|
|
2682
3224
|
|
|
3225
|
+
/**
|
|
3226
|
+
* When extract-tags API returns, map Redux `extractedTags` into required/optional so
|
|
3227
|
+
* CustomValuesEditor shows personalization fields (effect was previously commented out).
|
|
3228
|
+
* RCS + SMS fallback: merge primary + fallback tag trees when fallback template exists.
|
|
3229
|
+
*/
|
|
3230
|
+
useEffect(() => {
|
|
3231
|
+
if (!show) return;
|
|
3232
|
+
const hasFallbackSmsTemplate = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
|
|
3233
|
+
if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
|
|
3234
|
+
applyRcsSmsFallbackTagExtraction();
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
const smsEditorBody = typeof getCurrentContent === 'string' ? getCurrentContent : '';
|
|
3238
|
+
let smsTagSource = channel === CHANNELS.SMS && (!extractedTags || extractedTags.length === 0)
|
|
3239
|
+
? buildSyntheticSmsMustacheTags(smsEditorBody)
|
|
3240
|
+
: (extractedTags ?? []);
|
|
3241
|
+
if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
|
|
3242
|
+
smsTagSource = [];
|
|
3243
|
+
}
|
|
3244
|
+
const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
|
|
3245
|
+
setRequiredTags(required);
|
|
3246
|
+
setOptionalTags(optional);
|
|
3247
|
+
setTagsExtracted(hasPersonalizationTags);
|
|
3248
|
+
setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
|
|
3249
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
|
|
3250
|
+
}, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
|
|
3251
|
+
|
|
3252
|
+
/**
|
|
3253
|
+
* Get content to run tag extraction on (channel-specific).
|
|
3254
|
+
*/
|
|
3255
|
+
const getContentForTagExtraction = () => {
|
|
3256
|
+
let contentToExtract = activeContentForActions;
|
|
2683
3257
|
if (channel === CHANNELS.EMAIL && formData) {
|
|
2684
3258
|
const currentTabData = formData[currentTab - 1];
|
|
2685
3259
|
const activeTab = currentTabData?.activeTab;
|
|
2686
3260
|
const templateContent = currentTabData?.[activeTab]?.['template-content'];
|
|
2687
3261
|
contentToExtract = templateContent || contentToExtract;
|
|
2688
3262
|
}
|
|
3263
|
+
return contentToExtract;
|
|
3264
|
+
};
|
|
2689
3265
|
|
|
2690
|
-
|
|
3266
|
+
/**
|
|
3267
|
+
* Handle extract tags
|
|
3268
|
+
*/
|
|
3269
|
+
const handleExtractTags = () => {
|
|
3270
|
+
if (channel === CHANNELS.RCS) {
|
|
3271
|
+
applyRcsSmsFallbackTagExtraction();
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
const contentToExtract = getContentForTagExtraction();
|
|
2691
3276
|
const tags = contentToExtract.match(/{{[^}]+}}/g) || [];
|
|
2692
3277
|
const hasPersonalizationTags = tags.some((tag) => !tag.includes(UNSUBSCRIBE_TAG_NAME));
|
|
3278
|
+
const onlyUnsubscribe = !hasPersonalizationTags && tags.length === 1 && tags[0].includes(UNSUBSCRIBE_TAG_NAME);
|
|
2693
3279
|
|
|
2694
|
-
if (
|
|
2695
|
-
// If only unsubscribe tag is present, show noTagsExtracted message
|
|
3280
|
+
if (onlyUnsubscribe) {
|
|
2696
3281
|
setTagsExtracted(false);
|
|
2697
3282
|
setRequiredTags([]);
|
|
2698
3283
|
setOptionalTags([]);
|
|
@@ -2700,10 +3285,9 @@ const CommonTestAndPreview = (props) => {
|
|
|
2700
3285
|
return;
|
|
2701
3286
|
}
|
|
2702
3287
|
|
|
2703
|
-
// Extract tags
|
|
2704
3288
|
setTagsExtracted(true);
|
|
2705
3289
|
const { templateSubject, templateContent } = prepareTagExtractionPayload(
|
|
2706
|
-
|
|
3290
|
+
activeChannelForActions,
|
|
2707
3291
|
formData || {},
|
|
2708
3292
|
contentToExtract
|
|
2709
3293
|
);
|
|
@@ -2765,7 +3349,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
2765
3349
|
if (existingTestCustomer) {
|
|
2766
3350
|
const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
|
|
2767
3351
|
if (entityId != null) {
|
|
2768
|
-
const id =
|
|
3352
|
+
const id = normalizeTestEntityId(entityId);
|
|
2769
3353
|
setSelectedTestEntities((prev) => (
|
|
2770
3354
|
prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
|
|
2771
3355
|
));
|
|
@@ -2802,7 +3386,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
2802
3386
|
(c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
|
|
2803
3387
|
);
|
|
2804
3388
|
if (alreadyInTestListByCustomerId) {
|
|
2805
|
-
const id =
|
|
3389
|
+
const id = normalizeTestEntityId(customerIdFromLookup);
|
|
2806
3390
|
setSelectedTestEntities((prev) => (
|
|
2807
3391
|
prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
|
|
2808
3392
|
));
|
|
@@ -2839,26 +3423,21 @@ const CommonTestAndPreview = (props) => {
|
|
|
2839
3423
|
const handleSendTestMessage = () => {
|
|
2840
3424
|
const allUserIds = [];
|
|
2841
3425
|
selectedTestEntities.forEach((entityId) => {
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
if (group) {
|
|
2846
|
-
allUserIds.push(...group.userIds);
|
|
2847
|
-
}
|
|
3426
|
+
const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
|
|
3427
|
+
if (group) {
|
|
3428
|
+
allUserIds.push(...group.userIds);
|
|
2848
3429
|
} else {
|
|
2849
|
-
|
|
2850
|
-
? String(entityId).slice('customer:'.length)
|
|
2851
|
-
: String(entityId);
|
|
2852
|
-
allUserIds.push(Number(rawId));
|
|
3430
|
+
allUserIds.push(entityId);
|
|
2853
3431
|
}
|
|
2854
3432
|
});
|
|
2855
3433
|
const uniqueUserIds = [...new Set(allUserIds)];
|
|
2856
3434
|
|
|
2857
|
-
const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP].includes(channel)
|
|
3435
|
+
const deliveryOverride = [CHANNELS.SMS, CHANNELS.EMAIL, CHANNELS.WHATSAPP, CHANNELS.RCS].includes(channel)
|
|
2858
3436
|
? testPreviewDeliverySettings[channel]
|
|
2859
3437
|
: null;
|
|
2860
3438
|
|
|
2861
|
-
//
|
|
3439
|
+
// createMessageMeta must match the creative channel and full creative (RCS + SMS fallback in one meta).
|
|
3440
|
+
// Do not use activeChannelForActions / activeContentForActions — those follow the RCS vs Fallback SMS *preview* tab.
|
|
2862
3441
|
const initialPayload = prepareTestMessagePayload(
|
|
2863
3442
|
channel,
|
|
2864
3443
|
formData || content || {},
|
|
@@ -2866,7 +3445,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
2866
3445
|
customValues,
|
|
2867
3446
|
uniqueUserIds,
|
|
2868
3447
|
previewData,
|
|
2869
|
-
deliveryOverride
|
|
3448
|
+
deliveryOverride,
|
|
3449
|
+
{},
|
|
2870
3450
|
);
|
|
2871
3451
|
|
|
2872
3452
|
actions.createMessageMetaRequested(
|
|
@@ -2899,11 +3479,10 @@ const CommonTestAndPreview = (props) => {
|
|
|
2899
3479
|
// ============================================
|
|
2900
3480
|
// RENDER HELPER FUNCTIONS
|
|
2901
3481
|
// ============================================
|
|
2902
|
-
|
|
2903
3482
|
const renderLeftPanelContent = () => (
|
|
2904
3483
|
<LeftPanelContent
|
|
2905
|
-
isExtractingTags={isExtractingTags}
|
|
2906
|
-
extractedTags={
|
|
3484
|
+
isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
|
|
3485
|
+
extractedTags={leftPanelExtractedTags}
|
|
2907
3486
|
selectedCustomer={selectedCustomer}
|
|
2908
3487
|
handleCustomerSelect={handleCustomerSelect}
|
|
2909
3488
|
handleSearchCustomer={handleSearchCustomer}
|
|
@@ -2921,15 +3500,29 @@ const CommonTestAndPreview = (props) => {
|
|
|
2921
3500
|
|
|
2922
3501
|
const renderCustomValuesEditor = () => (
|
|
2923
3502
|
<CustomValuesEditor
|
|
2924
|
-
isExtractingTags={isExtractingTags}
|
|
3503
|
+
isExtractingTags={isExtractingTags || isExtractingSmsFallbackTags}
|
|
2925
3504
|
isUpdatePreviewDisabled={isUpdatePreviewDisabled}
|
|
2926
3505
|
showJSON={showJSON}
|
|
2927
3506
|
setShowJSON={setShowJSON}
|
|
2928
3507
|
customValues={customValues}
|
|
2929
3508
|
handleJSONTextChange={handleJSONTextChange}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3509
|
+
sections={[
|
|
3510
|
+
{
|
|
3511
|
+
key: channel,
|
|
3512
|
+
title:
|
|
3513
|
+
channel === CHANNELS.RCS
|
|
3514
|
+
? messages.rcsTagsSectionTitle
|
|
3515
|
+
: messages[`${channel}TagsSectionTitle`],
|
|
3516
|
+
requiredTags,
|
|
3517
|
+
optionalTags,
|
|
3518
|
+
},
|
|
3519
|
+
{
|
|
3520
|
+
key: PREVIEW_TAB_SMS_FALLBACK,
|
|
3521
|
+
title: isRcsSmsFallbackPreviewEnabled ? messages.smsFallbackTagsSectionTitle : null,
|
|
3522
|
+
requiredTags: smsFallbackRequiredTags,
|
|
3523
|
+
optionalTags: smsFallbackOptionalTags,
|
|
3524
|
+
},
|
|
3525
|
+
]}
|
|
2933
3526
|
handleCustomValueChange={handleCustomValueChange}
|
|
2934
3527
|
handleDiscardCustomValues={handleDiscardCustomValues}
|
|
2935
3528
|
handleUpdatePreview={handleUpdatePreview}
|
|
@@ -2945,18 +3538,13 @@ const CommonTestAndPreview = (props) => {
|
|
|
2945
3538
|
}));
|
|
2946
3539
|
};
|
|
2947
3540
|
|
|
2948
|
-
/** Trim pasted emails (trailing CR/LF).
|
|
3541
|
+
/** Trim pasted emails (trailing CR/LF). Allow any input for SMS; valid-only check is in renderAddTestCustomerButton. */
|
|
2949
3542
|
const handleTestCustomersSearch = useCallback((value) => {
|
|
2950
3543
|
if (value == null || value === '') {
|
|
2951
3544
|
setSearchValue('');
|
|
2952
3545
|
return;
|
|
2953
3546
|
}
|
|
2954
|
-
|
|
2955
|
-
if (channel === CHANNELS.SMS) {
|
|
2956
|
-
setSearchValue(formatPhoneNumber(raw));
|
|
2957
|
-
} else {
|
|
2958
|
-
setSearchValue(raw);
|
|
2959
|
-
}
|
|
3547
|
+
setSearchValue(String(value).trim());
|
|
2960
3548
|
}, [channel]);
|
|
2961
3549
|
|
|
2962
3550
|
const renderSendTestMessage = () => (
|
|
@@ -2974,12 +3562,13 @@ const CommonTestAndPreview = (props) => {
|
|
|
2974
3562
|
renderAddTestCustomerButton={renderAddTestCustomerButton}
|
|
2975
3563
|
formatMessage={formatMessage}
|
|
2976
3564
|
deliverySettings={testPreviewDeliverySettings[channel]}
|
|
2977
|
-
|
|
3565
|
+
senderDetailsByChannel={senderDetailsByChannel}
|
|
2978
3566
|
wecrmAccounts={wecrmAccounts}
|
|
2979
3567
|
onSaveDeliverySettings={handleSaveDeliverySettings}
|
|
2980
3568
|
isLoadingSenderDetails={isLoadingSenderDetails}
|
|
2981
3569
|
smsTraiDltEnabled={smsTraiDltEnabled}
|
|
2982
3570
|
registeredSenderIds={registeredSenderIds}
|
|
3571
|
+
isChannelSmsFallbackPreviewEnabled={isRcsSmsFallbackPreviewEnabled}
|
|
2983
3572
|
searchValue={searchValue}
|
|
2984
3573
|
setSearchValue={handleTestCustomersSearch}
|
|
2985
3574
|
/>
|
|
@@ -2993,14 +3582,13 @@ const CommonTestAndPreview = (props) => {
|
|
|
2993
3582
|
|
|
2994
3583
|
const renderAddTestCustomerButton = () => {
|
|
2995
3584
|
const raw = (searchValue || '').trim();
|
|
2996
|
-
const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
|
|
2997
3585
|
const showAddButton =
|
|
2998
3586
|
[CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
|
|
2999
|
-
(channel === CHANNELS.EMAIL ? isValidEmail(
|
|
3587
|
+
(channel === CHANNELS.EMAIL ? isValidEmail(raw) : isValidMobile(formatPhoneNumber(raw)));
|
|
3000
3588
|
if (!showAddButton) return null;
|
|
3001
3589
|
return (
|
|
3002
3590
|
<AddTestCustomerButton
|
|
3003
|
-
searchValue={
|
|
3591
|
+
searchValue={channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw}
|
|
3004
3592
|
handleAddTestCustomer={handleAddTestCustomer}
|
|
3005
3593
|
/>
|
|
3006
3594
|
);
|