@capillarytech/creatives-library 9.0.36 → 9.0.37-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/AppRoot.js +124 -0
- package/app-config.js +61 -0
- package/app.js +10 -175
- package/bootstrap.js +185 -0
- package/entry.js +67 -1
- package/mfe-exposed-components.js +2 -4
- package/package.json +2 -2
- package/services/api.js +9 -1
- package/styles/containers/layout/_layoutPage.scss +8 -6
- package/utils/getDataLayer.js +15 -0
- package/utils/gtmTrackers/gtmEvents/creativeDetails.js +2 -1
- package/utils/mfeDetect.js +1 -0
- package/utils/mfeFirstPaintReady.js +29 -0
- package/utils/mfeHistory.js +62 -0
- package/utils/rcsPayloadUtils.js +5 -2
- package/utils/tests/rcsPayloadUtils.test.js +54 -2
- package/v2Components/CapActionButton/constants.js +1 -0
- package/v2Components/CapActionButton/index.js +77 -9
- package/v2Components/CapActionButton/index.scss +13 -0
- package/v2Components/CapActionButton/messages.js +13 -0
- package/v2Components/CapActionButton/tests/index.test.js +32 -1
- package/v2Components/CommonTestAndPreview/index.js +44 -15
- package/v2Components/CommonTestAndPreview/tests/index.test.js +78 -0
- package/v2Components/CommonTestAndPreview/utils.js +34 -0
- package/v2Components/NavigationBar/index.js +9 -7
- package/v2Components/NavigationBar/mfeModuleHeader.config.js +16 -0
- package/v2Components/NavigationBar/tests/index.test.js +39 -19
- package/v2Components/SmsFallback/index.js +6 -0
- package/v2Containers/Cap/constants.js +1 -0
- package/v2Containers/Cap/index.js +57 -27
- package/v2Containers/CreativesContainer/index.js +11 -8
- package/v2Containers/CreativesContainer/tests/index.test.js +53 -0
- package/v2Containers/Rcs/components/CarouselCard.js +5 -0
- package/v2Containers/Rcs/components/CarouselCardButtons.js +20 -0
- package/v2Containers/Rcs/index.js +154 -79
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +55 -4
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +286 -0
- package/v2Containers/Rcs/tests/carouselUtils.test.js +13 -17
- package/v2Containers/Rcs/tests/index.test.js +141 -6
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +110 -0
- package/v2Containers/Rcs/tests/utils.test.js +36 -0
- package/v2Containers/Rcs/utils.js +16 -4
- package/v2Containers/SmsTrai/Edit/index.js +35 -24
- package/v2Containers/Templates/_templates.scss +1 -1
- package/v2Containers/Templates/index.js +1 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +8 -3
- package/v2Containers/TemplatesV2/index.js +22 -9
|
@@ -570,8 +570,8 @@ describe('RCS carousel label resolution in non-full mode', () => {
|
|
|
570
570
|
expect(card0NameAreas.length).toBe(2);
|
|
571
571
|
|
|
572
572
|
act(() => {
|
|
573
|
-
const slotId =
|
|
574
|
-
|
|
573
|
+
const slotId = card0NameAreas.at(0).prop('id') || card0NameAreas.at(0).key();
|
|
574
|
+
card0NameAreas.at(0).props().onChange({ target: { id: slotId, value: 'madhavi' } });
|
|
575
575
|
});
|
|
576
576
|
wrapper.update();
|
|
577
577
|
|
|
@@ -1189,6 +1189,130 @@ describe('RCS createPayload', () => {
|
|
|
1189
1189
|
expect(Object.prototype.hasOwnProperty.call(card, 'cardVarMapped')).toBe(true);
|
|
1190
1190
|
});
|
|
1191
1191
|
|
|
1192
|
+
it('disables Done in non-full mode when a CTA button URL still holds the unresolved {{1}} placeholder', () => {
|
|
1193
|
+
const templateData = {
|
|
1194
|
+
name: 'PromoCard',
|
|
1195
|
+
type: 'RCS',
|
|
1196
|
+
versions: {
|
|
1197
|
+
base: {
|
|
1198
|
+
content: {
|
|
1199
|
+
RCS: {
|
|
1200
|
+
rcsContent: {
|
|
1201
|
+
cardType: 'STANDALONE',
|
|
1202
|
+
cardSettings: { cardOrientation: 'VERTICAL' },
|
|
1203
|
+
cardContent: [
|
|
1204
|
+
{
|
|
1205
|
+
title: 'Title',
|
|
1206
|
+
description: 'Desc',
|
|
1207
|
+
mediaType: 'VIDEO',
|
|
1208
|
+
media: {
|
|
1209
|
+
mediaUrl: 'https://cdn.example.com/video.mp4',
|
|
1210
|
+
thumbnailUrl: 'https://cdn.example.com/video-thumb.jpg',
|
|
1211
|
+
},
|
|
1212
|
+
suggestions: [
|
|
1213
|
+
{ type: 'CTA', text: 'Visit here', url: 'https://example.com{{1}}' },
|
|
1214
|
+
],
|
|
1215
|
+
},
|
|
1216
|
+
],
|
|
1217
|
+
contentType: 'RICHCARD',
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1221
|
+
},
|
|
1222
|
+
},
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
const wrapper = mountWithIntl(
|
|
1226
|
+
<Provider store={store}>
|
|
1227
|
+
<Rcs
|
|
1228
|
+
actions={{
|
|
1229
|
+
clearCreateResponse: jest.fn(),
|
|
1230
|
+
getTemplateDetails: jest.fn(),
|
|
1231
|
+
uploadRcsAsset: jest.fn(),
|
|
1232
|
+
clearRcsMediaAsset: jest.fn(),
|
|
1233
|
+
editTemplate: jest.fn(),
|
|
1234
|
+
clearEditResponse: jest.fn(),
|
|
1235
|
+
}}
|
|
1236
|
+
globalActions={{ fetchSchemaForEntity }}
|
|
1237
|
+
onCreateComplete={onCreateComplete}
|
|
1238
|
+
handleClose={handleClose}
|
|
1239
|
+
intl={{ formatMessage }}
|
|
1240
|
+
location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
|
|
1241
|
+
params={params}
|
|
1242
|
+
templateData={templateData}
|
|
1243
|
+
rcsData={{}}
|
|
1244
|
+
isFullMode={false}
|
|
1245
|
+
isEditFlow={false}
|
|
1246
|
+
loadingTags={false}
|
|
1247
|
+
metaEntities={[]}
|
|
1248
|
+
isDltEnabled={false}
|
|
1249
|
+
smsRegister={'DLT'}
|
|
1250
|
+
getFormData={jest.fn()}
|
|
1251
|
+
/>
|
|
1252
|
+
</Provider>,
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
|
|
1256
|
+
expect(doneBtn.exists()).toBe(true);
|
|
1257
|
+
expect(doneBtn.prop('disabled')).toBe(true);
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
it('disables Send for Approval in full-mode creation when a CTA button URL still holds the unresolved {{1}} placeholder', () => {
|
|
1261
|
+
const wrapper = mountWithIntl(
|
|
1262
|
+
<Provider store={store}>
|
|
1263
|
+
<Rcs
|
|
1264
|
+
actions={{
|
|
1265
|
+
clearCreateResponse: jest.fn(),
|
|
1266
|
+
getTemplateDetails: jest.fn(),
|
|
1267
|
+
uploadRcsAsset: jest.fn(),
|
|
1268
|
+
clearRcsMediaAsset: jest.fn(),
|
|
1269
|
+
editTemplate: jest.fn(),
|
|
1270
|
+
clearEditResponse: jest.fn(),
|
|
1271
|
+
}}
|
|
1272
|
+
globalActions={{ fetchSchemaForEntity }}
|
|
1273
|
+
onCreateComplete={onCreateComplete}
|
|
1274
|
+
handleClose={handleClose}
|
|
1275
|
+
intl={{ formatMessage }}
|
|
1276
|
+
location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
|
|
1277
|
+
params={params}
|
|
1278
|
+
templateData={{}}
|
|
1279
|
+
rcsData={{}}
|
|
1280
|
+
isFullMode
|
|
1281
|
+
isEditFlow={false}
|
|
1282
|
+
loadingTags={false}
|
|
1283
|
+
metaEntities={[]}
|
|
1284
|
+
isDltEnabled={false}
|
|
1285
|
+
smsRegister={'DLT'}
|
|
1286
|
+
getFormData={jest.fn()}
|
|
1287
|
+
/>
|
|
1288
|
+
</Provider>,
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
act(() => {
|
|
1292
|
+
wrapper.find('#rcs_template_name_input').at(0).simulate('change', { target: { value: 'PromoCard' } });
|
|
1293
|
+
});
|
|
1294
|
+
wrapper.update();
|
|
1295
|
+
|
|
1296
|
+
act(() => {
|
|
1297
|
+
wrapper.find('#rcs_template_message_textarea').at(0).simulate('change', { target: { value: 'Hello there' } });
|
|
1298
|
+
});
|
|
1299
|
+
wrapper.update();
|
|
1300
|
+
|
|
1301
|
+
const actionButton = wrapper.find('.cap-action-button-mock').at(0);
|
|
1302
|
+
expect(actionButton.exists()).toBe(true);
|
|
1303
|
+
act(() => {
|
|
1304
|
+
actionButton.prop('updateButtonChange')(
|
|
1305
|
+
{ type: 'CTA', text: 'Visit here', url: 'https://example.com{{1}}' },
|
|
1306
|
+
0,
|
|
1307
|
+
);
|
|
1308
|
+
});
|
|
1309
|
+
wrapper.update();
|
|
1310
|
+
|
|
1311
|
+
const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
|
|
1312
|
+
expect(doneBtn.exists()).toBe(true);
|
|
1313
|
+
expect(doneBtn.prop('disabled')).toBe(true);
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1192
1316
|
it('should sync duplicate placeholders via cardVarMapped in non-full mode (typing once updates all occurrences)', () => {
|
|
1193
1317
|
const templateData = {
|
|
1194
1318
|
name: 'DupVarsTemplate',
|
|
@@ -1286,7 +1410,12 @@ describe('RCS createPayload', () => {
|
|
|
1286
1410
|
cardContent: [
|
|
1287
1411
|
{
|
|
1288
1412
|
title: 'Hello {{user_name}} and {{user_name}}',
|
|
1289
|
-
|
|
1413
|
+
// Distinct from the title's var name: VarSegmentMessageEditor ids are
|
|
1414
|
+
// `${token}_${localSegmentIndex}`, not scoped per-field, so reusing
|
|
1415
|
+
// {{user_name}} here would collide with the title's own id and get
|
|
1416
|
+
// picked up by the `{{user_name}}_` filter below once the tag-select
|
|
1417
|
+
// renames the title's first occurrence.
|
|
1418
|
+
description: 'Hi {{other_var}}',
|
|
1290
1419
|
mediaType: 'IMAGE',
|
|
1291
1420
|
media: { mediaUrl: '', thumbnailUrl: '' },
|
|
1292
1421
|
cardVarMapped: {},
|
|
@@ -1352,12 +1481,18 @@ describe('RCS createPayload', () => {
|
|
|
1352
1481
|
});
|
|
1353
1482
|
wrapper.update();
|
|
1354
1483
|
|
|
1355
|
-
|
|
1484
|
+
// The focused occurrence's own token is replaced with the picked tag, while the untouched
|
|
1485
|
+
// duplicate keeps its original {{user_name}} token but syncs to the same resolved value.
|
|
1486
|
+
const renamedVarArea = wrapper.find('CapInputTextArea').filterWhere((n) => {
|
|
1487
|
+
const id = n.prop('id') || '';
|
|
1488
|
+
return id.includes('{{first_name}}_');
|
|
1489
|
+
});
|
|
1490
|
+
const duplicateVarArea = wrapper.find('CapInputTextArea').filterWhere((n) => {
|
|
1356
1491
|
const id = n.prop('id') || '';
|
|
1357
1492
|
return id.includes('{{user_name}}_');
|
|
1358
1493
|
});
|
|
1359
|
-
expect(
|
|
1360
|
-
expect(
|
|
1494
|
+
expect(renamedVarArea.at(0).prop('value')).toBe('{{first_name}}');
|
|
1495
|
+
expect(duplicateVarArea.at(0).prop('value')).toBe('{{first_name}}');
|
|
1361
1496
|
});
|
|
1362
1497
|
|
|
1363
1498
|
it('should keep two tags + freetext inside the variable textarea in non-full mode edit (not merged into static text)', () => {
|
|
@@ -364,6 +364,14 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
364
364
|
expect(out).toEqual({ 1: 'x', user_name: 'y' });
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
+
it('keeps function-style tag keys with parentheses (voucher/offer/badge tags)', () => {
|
|
368
|
+
const input = {
|
|
369
|
+
'voucher(1190738)': 'SAVE20',
|
|
370
|
+
'badges_enroll_no_expiry(123)': 'Gold',
|
|
371
|
+
};
|
|
372
|
+
expect(pickRcsCardVarMappedEntries(input)).toEqual(input);
|
|
373
|
+
});
|
|
374
|
+
|
|
367
375
|
it('returns {} for an empty object', () => {
|
|
368
376
|
expect(pickRcsCardVarMappedEntries({})).toEqual({});
|
|
369
377
|
});
|
|
@@ -439,6 +447,7 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
439
447
|
template: 'Hello',
|
|
440
448
|
templateName: 'T1',
|
|
441
449
|
registeredSenderIds: ['S1'],
|
|
450
|
+
templateVariableMapping: { '{#a#}_0': { data: 'x', count: 1 } },
|
|
442
451
|
},
|
|
443
452
|
},
|
|
444
453
|
});
|
|
@@ -497,6 +506,7 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
497
506
|
expect(out.smsFallBackContent.templateConfigs).toEqual({
|
|
498
507
|
templateName: 'ConfigName',
|
|
499
508
|
registeredSenderIds: ['H1'],
|
|
509
|
+
templateVariableMapping: { a: { data: '1', count: 1 } },
|
|
500
510
|
});
|
|
501
511
|
expect(out.smsFallBackContent.rcsSmsFallbackVarMapped).toEqual({ a: '1' });
|
|
502
512
|
});
|
|
@@ -537,6 +547,25 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
537
547
|
});
|
|
538
548
|
});
|
|
539
549
|
|
|
550
|
+
it('non-full mode: DLT campaign with registeredSenderIds carries the var map into templateConfigs.templateVariableMapping (so it survives normalizeRcsMessageContentForApi stripping rcsSmsFallbackVarMapped)', () => {
|
|
551
|
+
enableTraiDlt();
|
|
552
|
+
const out = buildSmsFallBackContentForPayload({
|
|
553
|
+
smsFallbackData: {
|
|
554
|
+
message: 'Hi {#var#} your OTP is {#var#}.Capillary Tech.',
|
|
555
|
+
templateName: 'TEST_FALLBACK',
|
|
556
|
+
registeredSenderIds: ['CPLRYT'],
|
|
557
|
+
rcsSmsFallbackVarMapped: { '{#var#}_1': 'test1', '{#var#}_3': 'test2' },
|
|
558
|
+
},
|
|
559
|
+
templateData: {},
|
|
560
|
+
isFullMode: false,
|
|
561
|
+
smsRegister: 'DLT',
|
|
562
|
+
});
|
|
563
|
+
expect(out.smsFallBackContent.templateConfigs.templateVariableMapping).toEqual({
|
|
564
|
+
'{#var#}_1': { data: 'test1', count: 1 },
|
|
565
|
+
'{#var#}_3': { data: 'test2', count: 2 },
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
540
569
|
it('non-full mode: DLT campaign but no derivable templateConfigs omits templateConfigs', () => {
|
|
541
570
|
enableTraiDlt();
|
|
542
571
|
const out = buildSmsFallBackContentForPayload({
|
|
@@ -656,6 +685,45 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
656
685
|
expect(warnSpy).toHaveBeenCalled();
|
|
657
686
|
});
|
|
658
687
|
|
|
688
|
+
it('falls back to templateConfigs.templateName when root-level name fields are absent', () => {
|
|
689
|
+
const out = resolveSmsFallbackHydrationFromDetails({
|
|
690
|
+
smsFallBackContent: {
|
|
691
|
+
templateConfigs: { templateName: 'ConfigTemplateName' },
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
expect(out.nextSmsState.templateName).toBe('ConfigTemplateName');
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('falls back to templateConfigs.name when no other name field is present anywhere', () => {
|
|
698
|
+
const out = resolveSmsFallbackHydrationFromDetails({
|
|
699
|
+
smsFallBackContent: {
|
|
700
|
+
templateConfigs: { name: 'ConfigName' },
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
expect(out.nextSmsState.templateName).toBe('ConfigName');
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('falls back to the root-level templateName when templateConfigs has no name field', () => {
|
|
707
|
+
const out = resolveSmsFallbackHydrationFromDetails({
|
|
708
|
+
smsFallBackContent: {
|
|
709
|
+
templateName: 'RootName',
|
|
710
|
+
templateConfigs: { registeredSenderIds: ['123'] },
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
expect(out.nextSmsState.templateName).toBe('RootName');
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('prefers the root templateName over a conflicting nested templateConfigs.name', () => {
|
|
717
|
+
const out = resolveSmsFallbackHydrationFromDetails({
|
|
718
|
+
smsFallBackContent: {
|
|
719
|
+
templateName: 'RootName',
|
|
720
|
+
smsTemplateName: '',
|
|
721
|
+
templateConfigs: { name: 'NestedName', header: ['H1'] },
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
expect(out.nextSmsState.templateName).toBe('RootName');
|
|
725
|
+
});
|
|
726
|
+
|
|
659
727
|
it('is meaningful from var map alone, with no template name or message text', () => {
|
|
660
728
|
const out = resolveSmsFallbackHydrationFromDetails({
|
|
661
729
|
smsFallBackContent: { rcsSmsFallbackVarMapped: { '{#a#}_0': 'v' } },
|
|
@@ -705,6 +773,48 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
705
773
|
).toBe('Direct Template');
|
|
706
774
|
});
|
|
707
775
|
|
|
776
|
+
it('prefers templateConfigs.template (raw, {#var#} intact) over smsContent/message for DLT', () => {
|
|
777
|
+
const out = resolveSmsFallbackHydrationFromDetails({
|
|
778
|
+
smsFallBackContent: {
|
|
779
|
+
smsContent: 'hey user this is a test',
|
|
780
|
+
message: 'hey user this is a test',
|
|
781
|
+
templateConfigs: {
|
|
782
|
+
templateName: 'RCSDLT2',
|
|
783
|
+
template: 'hey{#var#}test',
|
|
784
|
+
templateVariableMapping: {
|
|
785
|
+
'{#var#}_1': { data: ' user this is a ', count: 1 },
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
expect(out.nextSmsState.content).toBe('hey{#var#}test');
|
|
791
|
+
expect(out.nextSmsState.templateContent).toBe('hey{#var#}test');
|
|
792
|
+
expect(out.nextSmsState.rcsSmsFallbackVarMapped).toEqual({ '{#var#}_1': ' user this is a ' });
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('merges templateVariableMapping-derived slots with an explicit rcsSmsFallbackVarMapped (explicit wins)', () => {
|
|
796
|
+
const out = resolveSmsFallbackHydrationFromDetails({
|
|
797
|
+
smsFallBackContent: {
|
|
798
|
+
smsContent: 'hey user this is a test',
|
|
799
|
+
rcsSmsFallbackVarMapped: { '{#var#}_1': 'explicit override' },
|
|
800
|
+
templateConfigs: {
|
|
801
|
+
template: 'hey{#var#}test',
|
|
802
|
+
templateVariableMapping: {
|
|
803
|
+
'{#var#}_1': { data: ' user this is a ', count: 1 },
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
expect(out.nextSmsState.rcsSmsFallbackVarMapped).toEqual({ '{#var#}_1': 'explicit override' });
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('falls back to smsContent when templateConfigs.template is absent (non-DLT)', () => {
|
|
812
|
+
const out = resolveSmsFallbackHydrationFromDetails({
|
|
813
|
+
smsFallBackContent: { smsContent: 'Hi {{first_name}}, your OTP is 1234' },
|
|
814
|
+
});
|
|
815
|
+
expect(out.nextSmsState.content).toBe('Hi {{first_name}}, your OTP is 1234');
|
|
816
|
+
});
|
|
817
|
+
|
|
708
818
|
it('joins an updated-sms-editor array into the fallback message', () => {
|
|
709
819
|
const out = resolveSmsFallbackHydrationFromDetails({
|
|
710
820
|
smsFallBackContent: {
|
|
@@ -781,6 +781,42 @@ describe('RCS utils', () => {
|
|
|
781
781
|
expect(rcsTitle).toBe('');
|
|
782
782
|
expect(rcsDesc).toBe('Visit Store for {{loyalty_points}} discount');
|
|
783
783
|
});
|
|
784
|
+
|
|
785
|
+
it('carousel card 1+: cardSlotOffset param resolves against its own global slots, not card 0\'s', () => {
|
|
786
|
+
// Full, correctly-populated carousel-wide map: slot 1 = card 0 title (user_id_b64), slot 2 =
|
|
787
|
+
// card 0 desc (voucher(1190738)), slot 3 = card 1 title (user_id_b64), slot 4 = card 1 desc
|
|
788
|
+
// (first_name).
|
|
789
|
+
const fullCarouselWideMap = {
|
|
790
|
+
1: '{{user_id_b64}}',
|
|
791
|
+
2: '{{voucher(1190738)}}',
|
|
792
|
+
3: '{{user_id_b64}}',
|
|
793
|
+
4: '{{first_name}}',
|
|
794
|
+
};
|
|
795
|
+
// Card 1's own title/description, called with cardSlotOffset=2 (card 0 contributed 2 slots) —
|
|
796
|
+
// the same way mapRcsCardContentForConsumerWithResolvedTags now calls it per card.
|
|
797
|
+
const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
|
|
798
|
+
'This is a park {{3}}',
|
|
799
|
+
'This park has a cat and a bird {{4}}',
|
|
800
|
+
fullCarouselWideMap,
|
|
801
|
+
true,
|
|
802
|
+
false,
|
|
803
|
+
false,
|
|
804
|
+
2,
|
|
805
|
+
);
|
|
806
|
+
expect(rcsTitle).toBe('This is a park {{user_id_b64}}');
|
|
807
|
+
expect(rcsDesc).toBe('This park has a cat and a bird {{first_name}}');
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('omitting cardSlotOffset defaults to 0 (single-card / standalone callers unaffected)', () => {
|
|
811
|
+
const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
|
|
812
|
+
'Hi {{1}}',
|
|
813
|
+
'Pts {{2}}',
|
|
814
|
+
{ 1: '{{first_name}}', 2: '{{loyalty_points}}' },
|
|
815
|
+
true,
|
|
816
|
+
);
|
|
817
|
+
expect(rcsTitle).toBe('Hi {{first_name}}');
|
|
818
|
+
expect(rcsDesc).toBe('Pts {{loyalty_points}}');
|
|
819
|
+
});
|
|
784
820
|
});
|
|
785
821
|
|
|
786
822
|
describe('sanitizeCardVarMappedValue', () => {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
7
7
|
REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
|
|
8
8
|
RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
|
|
9
|
+
RCS_MUSTACHE_WRAPPED_REGEX,
|
|
9
10
|
} from './constants';
|
|
10
11
|
import './index.scss';
|
|
11
12
|
// import { formatMessage } from '../../../utils/intl';
|
|
@@ -341,6 +342,8 @@ export function resolveRcsCardPreviewStrings(
|
|
|
341
342
|
cardVarMapped,
|
|
342
343
|
isLibraryMode = false,
|
|
343
344
|
textOnlyCard = false,
|
|
345
|
+
preserveLiteralValueTokens = false,
|
|
346
|
+
cardSlotOffset = 0,
|
|
344
347
|
) {
|
|
345
348
|
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
346
349
|
const getVarNameFromToken = (token = '') =>
|
|
@@ -389,8 +392,8 @@ export function resolveRcsCardPreviewStrings(
|
|
|
389
392
|
? 0
|
|
390
393
|
: (effectiveTitle.match(rcsVarRegex) || []).length;
|
|
391
394
|
return {
|
|
392
|
-
rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle,
|
|
393
|
-
rcsDesc: resolveTemplateWithMap(String(description || ''), titleVarCount),
|
|
395
|
+
rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle, cardSlotOffset),
|
|
396
|
+
rcsDesc: resolveTemplateWithMap(String(description || ''), cardSlotOffset + titleVarCount),
|
|
394
397
|
};
|
|
395
398
|
}
|
|
396
399
|
|
|
@@ -411,6 +414,7 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
|
411
414
|
: {};
|
|
412
415
|
const list = Array.isArray(cardContentArray) ? cardContentArray : [];
|
|
413
416
|
const isLibraryMode = isFullMode !== true;
|
|
417
|
+
let carouselSlotOffset = 0;
|
|
414
418
|
return list.map((card) => {
|
|
415
419
|
if (!card || typeof card !== 'object') return card;
|
|
416
420
|
const nested =
|
|
@@ -421,13 +425,21 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
|
421
425
|
const nestedClean = pickRcsCardVarMappedEntries(nested);
|
|
422
426
|
const merged = { ...rootClean, ...nestedClean };
|
|
423
427
|
const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
|
|
428
|
+
const cardTitle = card.title ?? '';
|
|
429
|
+
const cardDescription = card.description ?? '';
|
|
424
430
|
const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
|
|
425
|
-
|
|
426
|
-
|
|
431
|
+
cardTitle,
|
|
432
|
+
cardDescription,
|
|
427
433
|
merged,
|
|
428
434
|
isLibraryMode,
|
|
429
435
|
textOnly,
|
|
436
|
+
true,
|
|
437
|
+
carouselSlotOffset,
|
|
430
438
|
);
|
|
439
|
+
const effectiveTitleForCount = textOnly ? '' : String(cardTitle);
|
|
440
|
+
const titleVarCount = (effectiveTitleForCount.match(rcsVarRegex) || []).length;
|
|
441
|
+
const descVarCount = (String(cardDescription).match(rcsVarRegex) || []).length;
|
|
442
|
+
carouselSlotOffset += titleVarCount + descVarCount;
|
|
431
443
|
const { cardVarMapped: _drop, ...cardRest } = card;
|
|
432
444
|
|
|
433
445
|
return {
|
|
@@ -169,12 +169,11 @@ export const SmsTraiEdit = (props) => {
|
|
|
169
169
|
return traiDltEnabled || hasTraiDltFeature() || isRcsEditFlow;
|
|
170
170
|
}, [isRcsSmsFallback, traiDltEnabled, isRcsEditFlow]);
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
*/
|
|
172
|
+
|
|
173
|
+
const [rcsFallbackVarSegmentModeActive, setRcsFallbackVarSegmentModeActive] = useState(false);
|
|
174
|
+
const useRcsFallbackVarSegmentForCurrentText = isRcsSmsFallback && rcsFallbackVarSegmentModeActive;
|
|
175
|
+
|
|
176
|
+
|
|
178
177
|
const renderDescriptionCharacterCount = (className = "rcs-character-count") => (
|
|
179
178
|
<CapLabel type="label1" className={className}>
|
|
180
179
|
{formatMessage(messages.charactersCountLabel, {
|
|
@@ -196,18 +195,24 @@ export const SmsTraiEdit = (props) => {
|
|
|
196
195
|
};
|
|
197
196
|
|
|
198
197
|
const renderRcsFallbackMessage = (str = '') => {
|
|
199
|
-
if (!
|
|
198
|
+
if (!useRcsFallbackVarSegmentForCurrentText) {
|
|
200
199
|
return (
|
|
201
200
|
<CapRow className="rcs-create-template-message-input">
|
|
202
201
|
<div className="rcs_text_area_wrapper">
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
202
|
+
<div className="var-segment-message-editor__var-slot">
|
|
203
|
+
<TextArea
|
|
204
|
+
id="rcs_fallback_message_textarea"
|
|
205
|
+
autosize={{ minRows: 1, maxRows: 3 }}
|
|
206
|
+
value={fallbackText}
|
|
207
|
+
onChange={(e) => setFallbackText(e.target.value)}
|
|
208
|
+
onBlur={() => {
|
|
209
|
+
const hasVarToken = (fallbackText.match(COMBINED_SMS_TEMPLATE_VAR_REGEX) || []).length > 0;
|
|
210
|
+
if (hasVarToken) setRcsFallbackVarSegmentModeActive(true);
|
|
211
|
+
}}
|
|
212
|
+
placeholder={formatMessage(rcsMessages.fallbackMsgPlaceholder)}
|
|
213
|
+
data-testid="rcs_fallback_plain_text_area"
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
211
216
|
{renderDescriptionCharacterCount()}
|
|
212
217
|
</div>
|
|
213
218
|
</CapRow>
|
|
@@ -362,7 +367,9 @@ export const SmsTraiEdit = (props) => {
|
|
|
362
367
|
const templateBase = get(activeTemplateSourceForInit, 'versions.base', {});
|
|
363
368
|
const unicodeValidity = get(templateBase, 'unicode-validity', true);
|
|
364
369
|
const templateMsg = get(activeTemplateSourceForInit, 'versions.base.sms-editor', '') || '';
|
|
365
|
-
|
|
370
|
+
const templateMsgHasVarToken = (templateMsg.match(COMBINED_SMS_TEMPLATE_VAR_REGEX) || []).length > 0;
|
|
371
|
+
setRcsFallbackVarSegmentModeActive(templateMsgHasVarToken);
|
|
372
|
+
if (!templateMsgHasVarToken) {
|
|
366
373
|
setFallbackText(templateMsg);
|
|
367
374
|
setFallbackVarMappedData({});
|
|
368
375
|
setUpdatedSmsEditor(String(templateMsg).split(''));
|
|
@@ -429,7 +436,7 @@ export const SmsTraiEdit = (props) => {
|
|
|
429
436
|
|
|
430
437
|
useEffect(() => {
|
|
431
438
|
if (!isRcsSmsFallback) return;
|
|
432
|
-
if (!
|
|
439
|
+
if (!useRcsFallbackVarSegmentForCurrentText) {
|
|
433
440
|
const plainFallbackSmsText = fallbackText || '';
|
|
434
441
|
setUpdatedSmsEditor(plainFallbackSmsText.split(''));
|
|
435
442
|
setTotalMessageLength(plainFallbackSmsText.length);
|
|
@@ -441,15 +448,17 @@ export const SmsTraiEdit = (props) => {
|
|
|
441
448
|
);
|
|
442
449
|
setUpdatedSmsEditor(resolvedFallbackDisplay.split(''));
|
|
443
450
|
setTotalMessageLength(resolvedFallbackDisplay.length);
|
|
444
|
-
}, [isRcsSmsFallback,
|
|
451
|
+
}, [isRcsSmsFallback, useRcsFallbackVarSegmentForCurrentText, fallbackText, fallbackVarMappedData]);
|
|
445
452
|
|
|
446
453
|
useEffect(() => {
|
|
447
454
|
if (!isRcsSmsFallback) return;
|
|
455
|
+
// Skip until hydration populates fallbackVarMappedData, or this wipes the parent's rcsSmsFallbackVarMapped before init restores it.
|
|
456
|
+
if (loading) return;
|
|
448
457
|
if (typeof onRcsFallbackEditorStateChange !== 'function') return;
|
|
449
458
|
onRcsFallbackEditorStateChange({
|
|
450
459
|
rcsSmsFallbackVarMapped: fallbackVarMappedData || {},
|
|
451
460
|
});
|
|
452
|
-
}, [isRcsSmsFallback, fallbackVarMappedData, onRcsFallbackEditorStateChange]);
|
|
461
|
+
}, [isRcsSmsFallback, loading, fallbackVarMappedData, onRcsFallbackEditorStateChange]);
|
|
453
462
|
|
|
454
463
|
useEffect(() => {
|
|
455
464
|
if (!isRcsSmsFallback) return;
|
|
@@ -532,7 +541,7 @@ export const SmsTraiEdit = (props) => {
|
|
|
532
541
|
// TRAI/DLT VarSegment: `validateIfTagClosed` only understands paired `{{…}}` and breaks on
|
|
533
542
|
// legitimate `{#…#}` / mixed TRAI shapes (extra `{`/`}` counts). Do not tie Done to it;
|
|
534
543
|
// slot completeness is enforced by `areAllRcsSmsFallbackVarSlotsFilled` on the RCS screen.
|
|
535
|
-
if (
|
|
544
|
+
if (useRcsFallbackVarSegmentForCurrentText) {
|
|
536
545
|
tagValidationResponseRef.current = {};
|
|
537
546
|
updateIsTagValidationError(false);
|
|
538
547
|
return;
|
|
@@ -573,7 +582,7 @@ export const SmsTraiEdit = (props) => {
|
|
|
573
582
|
tags,
|
|
574
583
|
isRcsSmsFallback,
|
|
575
584
|
isFullMode,
|
|
576
|
-
|
|
585
|
+
useRcsFallbackVarSegmentForCurrentText,
|
|
577
586
|
fallbackText,
|
|
578
587
|
fallbackVarMappedData,
|
|
579
588
|
getDefaultTags,
|
|
@@ -712,7 +721,8 @@ export const SmsTraiEdit = (props) => {
|
|
|
712
721
|
...baseWithoutDerivedFields,
|
|
713
722
|
'sms-editor': fallbackText || '',
|
|
714
723
|
'unicode-validity': isUnicodeAllowed,
|
|
715
|
-
|
|
724
|
+
// Gate on useRcsFallbackVarSegmentForCurrentText (not the static DLT flag), or non-DLT var-token flows lose slot values on save.
|
|
725
|
+
...(useRcsFallbackVarSegmentForCurrentText && {
|
|
716
726
|
'rcs-sms-fallback-var-mapped': fallbackVarMappedData || {},
|
|
717
727
|
}),
|
|
718
728
|
};
|
|
@@ -778,8 +788,9 @@ export const SmsTraiEdit = (props) => {
|
|
|
778
788
|
|
|
779
789
|
const onTagSelect = (data) => {
|
|
780
790
|
if (isRcsSmsFallback) {
|
|
781
|
-
if (!
|
|
791
|
+
if (!useRcsFallbackVarSegmentForCurrentText) {
|
|
782
792
|
setFallbackText((prev) => `${prev || ''}{{${data}}}`);
|
|
793
|
+
setRcsFallbackVarSegmentModeActive(true);
|
|
783
794
|
return;
|
|
784
795
|
}
|
|
785
796
|
if (!fallbackFocusedId) return;
|
|
@@ -949,7 +960,7 @@ export const SmsTraiEdit = (props) => {
|
|
|
949
960
|
|
|
950
961
|
const calculateTotalMessageLength = () => {
|
|
951
962
|
if (isRcsSmsFallback) {
|
|
952
|
-
if (
|
|
963
|
+
if (useRcsFallbackVarSegmentForCurrentText) {
|
|
953
964
|
const resolved = getFallbackResolvedContent(fallbackText || '', fallbackVarMappedData || {});
|
|
954
965
|
setTotalMessageLength(resolved.length);
|
|
955
966
|
} else {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@import '~@capillarytech/cap-ui-library/styles/_variables.scss';
|
|
2
2
|
|
|
3
3
|
.ant-tabs-content{
|
|
4
|
+
margin-top: $CAP_SPACE_08;
|
|
4
5
|
// .creatives-templates-list.full-mode{
|
|
5
6
|
.v2-pagination-container, .v2-pagination-container-half {
|
|
6
7
|
.ant-tabs-tabpane-active{
|
|
@@ -1427,7 +1428,6 @@
|
|
|
1427
1428
|
justify-content: space-between;
|
|
1428
1429
|
align-items: center;
|
|
1429
1430
|
gap: $CAP_SPACE_08;
|
|
1430
|
-
margin-right: $CAP_SPACE_32;
|
|
1431
1431
|
}
|
|
1432
1432
|
|
|
1433
1433
|
.template-listing-more-btn {
|
|
@@ -4603,6 +4603,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
4603
4603
|
<CapButton
|
|
4604
4604
|
className={`create-new-${channelLowerCase} margin-l-8 margin-b-12`}
|
|
4605
4605
|
type={"primary"}
|
|
4606
|
+
isAddBtn
|
|
4606
4607
|
disabled={this.isCreateDisabled()}
|
|
4607
4608
|
onClick={this.createTemplate}
|
|
4608
4609
|
>
|
|
@@ -239,6 +239,7 @@ exports[`Test Templates container Should render sms illustration when no templat
|
|
|
239
239
|
<CapButton
|
|
240
240
|
className="create-new-sms margin-l-8 margin-b-12"
|
|
241
241
|
disabled={false}
|
|
242
|
+
isAddBtn={true}
|
|
242
243
|
onClick={[Function]}
|
|
243
244
|
type="primary"
|
|
244
245
|
>
|
|
@@ -813,6 +814,7 @@ exports[`Test Templates container Should render temlates when whatsapp templates
|
|
|
813
814
|
<CapButton
|
|
814
815
|
className="create-new-whatsapp margin-l-8 margin-b-12"
|
|
815
816
|
disabled={false}
|
|
817
|
+
isAddBtn={true}
|
|
816
818
|
onClick={[Function]}
|
|
817
819
|
type="primary"
|
|
818
820
|
>
|
|
@@ -1223,6 +1225,7 @@ exports[`Test Templates container Test max templates exceeded 1`] = `
|
|
|
1223
1225
|
<CapButton
|
|
1224
1226
|
className="create-new-whatsapp margin-l-8 margin-b-12"
|
|
1225
1227
|
disabled={false}
|
|
1228
|
+
isAddBtn={true}
|
|
1226
1229
|
onClick={[Function]}
|
|
1227
1230
|
type="primary"
|
|
1228
1231
|
>
|
|
@@ -1696,6 +1699,7 @@ exports[`Test Templates container Test max templates not exceeded 1`] = `
|
|
|
1696
1699
|
<CapButton
|
|
1697
1700
|
className="create-new-whatsapp margin-l-8 margin-b-12"
|
|
1698
1701
|
disabled={false}
|
|
1702
|
+
isAddBtn={true}
|
|
1699
1703
|
onClick={[Function]}
|
|
1700
1704
|
type="primary"
|
|
1701
1705
|
>
|
|
@@ -2169,6 +2173,7 @@ exports[`Test Templates container Test max templates warning 1`] = `
|
|
|
2169
2173
|
<CapButton
|
|
2170
2174
|
className="create-new-whatsapp margin-l-8 margin-b-12"
|
|
2171
2175
|
disabled={false}
|
|
2176
|
+
isAddBtn={true}
|
|
2172
2177
|
onClick={[Function]}
|
|
2173
2178
|
type="primary"
|
|
2174
2179
|
>
|
|
@@ -10,10 +10,9 @@ export default css`
|
|
|
10
10
|
|
|
11
11
|
.component-wrapper {
|
|
12
12
|
${(props) => props.isFullMode ? `
|
|
13
|
-
max-width: 71.25rem;
|
|
14
13
|
margin: 0 auto;
|
|
15
14
|
width: 100%;
|
|
16
|
-
padding: 0
|
|
15
|
+
padding: 0;
|
|
17
16
|
/* Only main channel tabs content, not HTML Editor validation panel tabs */
|
|
18
17
|
> .cap-tab-v2 > .ant-tabs-content-holder > .ant-tabs-content,
|
|
19
18
|
> .cap-tab-v2 > .ant-tabs-content {
|
|
@@ -97,6 +96,12 @@ export default css`
|
|
|
97
96
|
`;
|
|
98
97
|
|
|
99
98
|
export const CapTabStyle = css`
|
|
100
|
-
${(props) => props.isFullMode ? `margin-top: ${CAP_SPACE_24}
|
|
99
|
+
${(props) => props.isFullMode ? `margin-top: ${CAP_SPACE_24};` : ``
|
|
100
|
+
}
|
|
101
|
+
.ant-tabs-nav-list {
|
|
102
|
+
gap: 2rem;
|
|
103
|
+
}
|
|
104
|
+
.ant-tabs-tab + .ant-tabs-tab {
|
|
105
|
+
margin: 0 !important;
|
|
101
106
|
}
|
|
102
107
|
`;
|