@capillarytech/creatives-library 9.0.73-alpha.sprint-testpreview.22 → 9.0.73-alpha.sprint-testpreview.23

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.73-alpha.sprint-testpreview.22",
4
+ "version": "9.0.73-alpha.sprint-testpreview.23",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -40,7 +40,9 @@ import {
40
40
  getRcsPrimaryTagExtractionText,
41
41
  mergeSyntheticMustacheTags,
42
42
  getWhatsappTagExtractionText,
43
+ getWhatsappCarouselCardBodyText,
43
44
  mergeZaloUnfilledParamTags,
45
+ fillSlotMapFromCustomValues,
44
46
  } from './utils';
45
47
  import AddTestCustomerButton from './AddTestCustomer';
46
48
  import ExistingCustomerModal from './ExistingCustomerModal';
@@ -1090,9 +1092,10 @@ const CommonTestAndPreview = (props) => {
1090
1092
  };
1091
1093
 
1092
1094
  case CHANNELS.WHATSAPP:
1095
+ // contentStr is templateMsg alone; carousel card bodies live in carouselData[].
1093
1096
  return {
1094
1097
  templateSubject: formDataObj?.title || '',
1095
- templateContent: contentStr,
1098
+ templateContent: getWhatsappTagExtractionText(formDataObj) || contentStr,
1096
1099
  };
1097
1100
 
1098
1101
  case CHANNELS.RCS: {
@@ -1189,9 +1192,10 @@ const CommonTestAndPreview = (props) => {
1189
1192
  * Updated to match API structure with ouId, sourceEntityId, module, deliverySettings, etc.
1190
1193
  */
1191
1194
 
1192
- const getCarouselMappedData = (carouselData = []) => carouselData.map((carousel) => {
1195
+ const getCarouselMappedData = (carouselData = [], customValuesObj = {}) => carouselData.map((carousel) => {
1193
1196
  const {
1194
- bodyText, imageUrl, videoUrl, videoPreviewImg, buttons, mediaType, cardVarMapped, bodyTemplate,
1197
+ imageUrl, videoUrl, videoPreviewImg, buttons, mediaType, cardVarMapped, bodyTemplate,
1198
+ varMap, tempBodyData,
1195
1199
  } = carousel || {};
1196
1200
  const buttonData = buttons.map((button, index) => {
1197
1201
  const {
@@ -1214,10 +1218,14 @@ const CommonTestAndPreview = (props) => {
1214
1218
  }
1215
1219
  return buttonObj;
1216
1220
  });
1221
+ // Overview state keys a card's slot map `varMap` / `tempBodyData`; the saved-template shape
1222
+ // uses cardVarMapped / bodyTemplate. Read both, as whatsappMedia does for the header.
1223
+ // varMap wins: the editor seeds it from carousel.varMap and never from cardVarMapped, so on a
1224
+ // library-saved template the saved map is stale relative to what the user sees on Overview.
1217
1225
  return {
1218
- body: bodyText,
1219
- cardVarMapped,
1220
- bodyTemplate,
1226
+ body: resolveTagsInText(getWhatsappCarouselCardBodyText(carousel), customValuesObj),
1227
+ cardVarMapped: fillSlotMapFromCustomValues(varMap ?? cardVarMapped, customValuesObj),
1228
+ bodyTemplate: Array.isArray(tempBodyData) ? tempBodyData.join('') : bodyTemplate,
1221
1229
  media: {
1222
1230
  ...(mediaType?.toLowerCase() === IMAGE.toLowerCase() && {
1223
1231
  url: imageUrl,
@@ -1289,7 +1297,9 @@ const CommonTestAndPreview = (props) => {
1289
1297
  ...(suggestionsFormattedForTestMeta.length > 0 && {
1290
1298
  suggestions: suggestionsFormattedForTestMeta,
1291
1299
  }),
1292
- ...(singleRcsCardPayload?.cardVarMapped && { cardVarMapped: singleRcsCardPayload.cardVarMapped }),
1300
+ ...(singleRcsCardPayload?.cardVarMapped && {
1301
+ cardVarMapped: fillSlotMapFromCustomValues(singleRcsCardPayload.cardVarMapped, customValuesObj),
1302
+ }),
1293
1303
  };
1294
1304
  });
1295
1305
  // Use the component-level smsFallbackContent prop (has rcsSmsFallbackVarMapped) so DLT
@@ -1524,7 +1534,7 @@ const CommonTestAndPreview = (props) => {
1524
1534
  const language = formDataObj?.language || formDataObj?.templateLanguage || 'en';
1525
1535
  const mediaType = formDataObj?.mediaType || formDataObj?.templateMediaType || MEDIA_TYPE_TEXT;
1526
1536
  const buttonType = formDataObj?.buttonType || BUTTON_TYPE_NONE;
1527
- const varMapped = formDataObj?.varMapped || {};
1537
+ const varMapped = fillSlotMapFromCustomValues(formDataObj?.varMapped || {}, customValuesObj);
1528
1538
  const isPreviewUrl = formDataObj?.isPreviewUrl || false;
1529
1539
 
1530
1540
  // Extract buttons from ctaData or quickReplyData
@@ -1559,7 +1569,10 @@ const CommonTestAndPreview = (props) => {
1559
1569
  const whatsappMedia = {
1560
1570
  header: resolveTagsInText(formDataObj?.templateHeader || formDataObj?.header || '', customValuesObj),
1561
1571
  footer: formDataObj?.templateFooter || formDataObj?.footer || '',
1562
- headerVarMapped: formDataObj?.headerVarMapped || formDataObj?.headerVarMappedData || {},
1572
+ headerVarMapped: fillSlotMapFromCustomValues(
1573
+ formDataObj?.headerVarMapped || formDataObj?.headerVarMappedData || {},
1574
+ customValuesObj,
1575
+ ),
1563
1576
  headerTemplate: formDataObj?.headerTemplate || formDataObj?.templateHeader || '',
1564
1577
  buttons: modifiedButtons,
1565
1578
  language,
@@ -1632,7 +1645,7 @@ const CommonTestAndPreview = (props) => {
1632
1645
 
1633
1646
  // Add carousel data if mediaType is CAROUSEL
1634
1647
  if (mediaType === MEDIA_TYPE_CAROUSEL && formDataObj?.carouselData) {
1635
- templateConfigs.cards = getCarouselMappedData(formDataObj.carouselData);
1648
+ templateConfigs.cards = getCarouselMappedData(formDataObj.carouselData, customValuesObj);
1636
1649
  templateConfigs.mediaType = formDataObj?.carouselMediaType?.toUpperCase() || MEDIA_TYPE_IMAGE;
1637
1650
  }
1638
1651
 
@@ -2674,6 +2687,16 @@ const CommonTestAndPreview = (props) => {
2674
2687
  ...contentObj,
2675
2688
  templateHeader: resolvePreviewText(contentObj.templateHeader),
2676
2689
  templateMsg: resolvePreviewText(contentObj.templateMsg),
2690
+ // Carousel cards render bodyText/updatedBodyText directly, so resolve both shapes.
2691
+ ...(Array.isArray(contentObj.carouselData) && {
2692
+ carouselData: contentObj.carouselData.map((card) => ({
2693
+ ...card,
2694
+ bodyText: resolvePreviewText(card?.bodyText),
2695
+ ...(Array.isArray(card?.updatedBodyText) && {
2696
+ updatedBodyText: card.updatedBodyText.map((segment) => resolvePreviewText(segment)),
2697
+ }),
2698
+ })),
2699
+ }),
2677
2700
  };
2678
2701
  }
2679
2702
  } else if (channel === CHANNELS.RCS) {
@@ -3533,6 +3556,10 @@ const CommonTestAndPreview = (props) => {
3533
3556
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
3534
3557
  contentToExtract = templateContent || contentToExtract;
3535
3558
  }
3559
+ // Include carousel card bodies, else a carousel-only template scans as tag-free.
3560
+ if (channel === CHANNELS.WHATSAPP) {
3561
+ contentToExtract = getWhatsappTagExtractionText(formData || {}) || contentToExtract;
3562
+ }
3536
3563
  return contentToExtract;
3537
3564
  };
3538
3565
 
@@ -667,6 +667,41 @@ describe('CommonTestAndPreview', () => {
667
667
  expect(payload.whatsappMessageContent.templateConfigs.mediaType).toBe('VIDEO');
668
668
  });
669
669
 
670
+ it('should resolve entered values into card body and fill the card slot map', async () => {
671
+ const formData = {
672
+ templateMsg: 'Deals inside',
673
+ mediaType: MEDIA_TYPE_CAROUSEL,
674
+ carouselMediaType: 'IMAGE',
675
+ carouselData: [
676
+ {
677
+ bodyText: 'Save {{1}} today',
678
+ tempBodyData: ['Save ', '{{1}}', ' today'],
679
+ varMap: { '{{1}}_1': '' },
680
+ mediaType: 'image',
681
+ buttons: [],
682
+ },
683
+ ],
684
+ };
685
+ render(
686
+ <TestWrapper>
687
+ <CommonTestAndPreview {...defaultProps} channel={CHANNELS.WHATSAPP} formData={formData} />
688
+ </TestWrapper>
689
+ );
690
+ await waitFor(() => expect(lastCustomValuesEditorProps).not.toBeNull());
691
+ act(() => {
692
+ lastCustomValuesEditorProps.handleCustomValueChange('1', '20%');
693
+ });
694
+ await waitFor(() => {
695
+ expect(lastSendTestMessageProps).toBeDefined();
696
+ });
697
+ lastSendTestMessageProps.handleSendTestMessage();
698
+ const [payload] = mockActions.createMessageMetaRequested.mock.calls[0];
699
+ const { cards } = payload.whatsappMessageContent.templateConfigs;
700
+ expect(cards[0].body).toBe('Save 20% today');
701
+ expect(cards[0].bodyTemplate).toBe('Save {{1}} today');
702
+ expect(cards[0].cardVarMapped).toEqual({ '{{1}}_1': '20%' });
703
+ });
704
+
670
705
  it('should map button type PHONE_NUMBER to buttonObj with phoneNumber', async () => {
671
706
  const props = {
672
707
  ...defaultProps,
@@ -3843,6 +3878,31 @@ describe('CommonTestAndPreview', () => {
3843
3878
  expect(getPreviewContent().templateMsg).toBe('Hi Shash, delivered by Name');
3844
3879
  });
3845
3880
 
3881
+ it('fills entered values into WhatsApp carousel card bodies', async () => {
3882
+ const carouselContent = {
3883
+ templateMsg: 'Deals inside',
3884
+ carouselData: [
3885
+ { bodyText: 'Card A {{1}}' },
3886
+ { bodyText: 'Card B {{2}}', updatedBodyText: ['Card B ', '{{2}}'] },
3887
+ ],
3888
+ };
3889
+ render(
3890
+ <TestWrapper>
3891
+ <CommonTestAndPreview
3892
+ {...defaultProps}
3893
+ channel={CHANNELS.WHATSAPP}
3894
+ content={carouselContent}
3895
+ formData={carouselContent}
3896
+ />
3897
+ </TestWrapper>
3898
+ );
3899
+
3900
+ await enterValuesAndUpdatePreview({ 1: 'Sh', 2: 'Monday' });
3901
+
3902
+ expect(getPreviewContent().carouselData[0].bodyText).toBe('Card A Sh');
3903
+ expect(getPreviewContent().carouselData[1].updatedBodyText.join('')).toBe('Card B Monday');
3904
+ });
3905
+
3846
3906
  it('restores WhatsApp placeholders after Discard custom values', async () => {
3847
3907
  render(
3848
3908
  <TestWrapper>
@@ -12,7 +12,10 @@ import {
12
12
  getCarouselButtonSlotCount,
13
13
  mergeSyntheticMustacheTags,
14
14
  getWhatsappTagExtractionText,
15
+ getWhatsappCarouselCardBodyText,
16
+ getWhatsappCarouselTagExtractionText,
15
17
  mergeZaloUnfilledParamTags,
18
+ fillSlotMapFromCustomValues,
16
19
  } from '../utils';
17
20
 
18
21
  describe('CommonTestAndPreview/utils', () => {
@@ -220,6 +223,48 @@ describe('CommonTestAndPreview/utils', () => {
220
223
  });
221
224
  expect(mergeSyntheticMustacheTags(apiTags, text).map((tag) => tag.name)).toEqual(['adv', '1', '2']);
222
225
  });
226
+
227
+ it('includes carousel card bodies', () => {
228
+ expect(getWhatsappTagExtractionText({
229
+ templateMsg: 'Hi {{1}}',
230
+ carouselData: [{ bodyText: 'Card A {{1}}' }, { bodyText: 'Card B {{2}}' }],
231
+ })).toBe('Hi {{1}} Card A {{1}} Card B {{2}}');
232
+ });
233
+
234
+ it('surfaces a carousel-only template as having tags', () => {
235
+ const text = getWhatsappTagExtractionText({
236
+ templateMsg: 'Hi there',
237
+ carouselData: [{ bodyText: 'Save {{1}} today' }],
238
+ });
239
+ expect(mergeSyntheticMustacheTags([], text).map((tag) => tag.name)).toEqual(['1']);
240
+ });
241
+ });
242
+
243
+ describe('getWhatsappCarouselCardBodyText', () => {
244
+ it('prefers the slot-resolved updatedBodyText', () => {
245
+ expect(getWhatsappCarouselCardBodyText({
246
+ bodyText: 'Hi {{1}}',
247
+ updatedBodyText: ['Hi ', '{{firstName}}'],
248
+ })).toBe('Hi {{firstName}}');
249
+ });
250
+
251
+ it('falls back to bodyText when no resolved text exists', () => {
252
+ expect(getWhatsappCarouselCardBodyText({ bodyText: 'Hi {{1}}', updatedBodyText: [] })).toBe('Hi {{1}}');
253
+ expect(getWhatsappCarouselCardBodyText({ bodyText: 'Hi {{1}}' })).toBe('Hi {{1}}');
254
+ expect(getWhatsappCarouselCardBodyText(undefined)).toBe('');
255
+ });
256
+ });
257
+
258
+ describe('getWhatsappCarouselTagExtractionText', () => {
259
+ it('skips blank card bodies', () => {
260
+ expect(getWhatsappCarouselTagExtractionText({
261
+ carouselData: [{ bodyText: ' ' }, { bodyText: 'Card B {{2}}' }, {}],
262
+ })).toBe('Card B {{2}}');
263
+ });
264
+
265
+ it('returns empty string for a non-carousel template', () => {
266
+ expect(getWhatsappCarouselTagExtractionText({ templateMsg: 'Hi' })).toBe('');
267
+ });
223
268
  });
224
269
 
225
270
  describe('mergeZaloUnfilledParamTags', () => {
@@ -257,6 +302,40 @@ describe('CommonTestAndPreview/utils', () => {
257
302
  });
258
303
  });
259
304
 
305
+ describe('fillSlotMapFromCustomValues', () => {
306
+ it('fills blank slots from custom values, keyed by tag name', () => {
307
+ expect(fillSlotMapFromCustomValues(
308
+ { '{{1}}_1': '', '{{2}}_3': ' ' },
309
+ { 1: 'Shash', 2: 'Name' },
310
+ )).toEqual({ '{{1}}_1': 'Shash', '{{2}}_3': 'Name' });
311
+ });
312
+
313
+ it('keeps slots already mapped on Overview', () => {
314
+ expect(fillSlotMapFromCustomValues(
315
+ { '{{1}}_1': '{{first_name}}', '{{adv}}_3': '' },
316
+ { first_name: 'ignored', adv: 'Promo' },
317
+ )).toEqual({ '{{1}}_1': '{{first_name}}', '{{adv}}_3': 'Promo' });
318
+ });
319
+
320
+ it('supports bare numeric and semantic slot keys (RCS cardVarMapped)', () => {
321
+ expect(fillSlotMapFromCustomValues({ 1: '', user_name: '' }, { 1: 'A', user_name: 'B' }))
322
+ .toEqual({ 1: 'A', user_name: 'B' });
323
+ });
324
+
325
+ it('matches custom values given as nested tag paths', () => {
326
+ expect(fillSlotMapFromCustomValues({ '{{first_name}}_0': '' }, { 'customer.first_name': 'Sh' }))
327
+ .toEqual({ '{{first_name}}_0': 'Sh' });
328
+ });
329
+
330
+ it('returns the map unchanged when there is nothing to fill', () => {
331
+ const slotMap = { '{{1}}_1': '' };
332
+ expect(fillSlotMapFromCustomValues(slotMap, {})).toBe(slotMap);
333
+ expect(fillSlotMapFromCustomValues(slotMap, { 2: 'x' })).toEqual(slotMap);
334
+ expect(fillSlotMapFromCustomValues(null, { 1: 'x' })).toBeNull();
335
+ expect(fillSlotMapFromCustomValues(undefined)).toBeUndefined();
336
+ });
337
+ });
338
+
260
339
  describe('mergeSyntheticMustacheTags', () => {
261
340
  it('handles null API tags', () => {
262
341
  expect(mergeSyntheticMustacheTags(null, 'Hi {{1}}').map((tag) => tag.name)).toEqual(['1']);
@@ -61,9 +61,25 @@ export const mergeSyntheticMustacheTags = (apiTags = [], rawText = '') => {
61
61
  return [...(apiTags ?? []), ...synthetic];
62
62
  };
63
63
 
64
- /** WhatsApp header + body text, scanned for unmapped `{{N}}` slots. */
64
+ /**
65
+ * A WhatsApp carousel card's body as the user sees it: the slot-resolved text when Overview
66
+ * mapping produced one, else the raw body. Mirrors the templateHeader/headerTemplate pair.
67
+ */
68
+ export const getWhatsappCarouselCardBodyText = (card) => {
69
+ const resolvedBody = Array.isArray(card?.updatedBodyText) ? card.updatedBodyText.join('') : '';
70
+ return resolvedBody || (card?.bodyText ?? '');
71
+ };
72
+
73
+ /** Carousel card bodies joined — they live outside templateMsg, so a tag scan misses them. */
74
+ export const getWhatsappCarouselTagExtractionText = (formDataObj) => (
75
+ Array.isArray(formDataObj?.carouselData) ? formDataObj.carouselData : [])
76
+ .map(getWhatsappCarouselCardBodyText)
77
+ .filter((bodyText) => hasTrimmedText(bodyText))
78
+ .join(' ');
79
+
80
+ /** WhatsApp header + body + carousel card bodies, scanned for unmapped `{{N}}` slots. */
65
81
  export const getWhatsappTagExtractionText = (formDataObj) => (
66
- `${formDataObj?.templateHeader || ''} ${formDataObj?.templateMsg || ''}`.trim()
82
+ `${formDataObj?.templateHeader || ''} ${formDataObj?.templateMsg || ''} ${getWhatsappCarouselTagExtractionText(formDataObj)}`.trim()
67
83
  );
68
84
 
69
85
  /** Zalo params left empty on Overview, surfaced as fillable test values (keyed by param name). */
@@ -75,6 +91,34 @@ export const mergeZaloUnfilledParamTags = (apiTags = [], formDataObj = {}) => {
75
91
  return [...(apiTags ?? []), ...unfilledParamTags];
76
92
  };
77
93
 
94
+ /**
95
+ * Slot key -> the variable name it holds. WhatsApp keys a slot by token and occurrence index
96
+ * (`<token>_<index>`); RCS keys it by the variable's own name, per card.
97
+ */
98
+ const getSlotKeyVarName = (slotKey) => String(slotKey).replace(/^\{\{(.+?)\}\}_\d+$/, '$1');
99
+
100
+ /**
101
+ * A slot map holds what the user mapped on Overview, per variable of the template.
102
+ * Slots left blank there take the value entered in Test & Preview for the same variable, so
103
+ * createMessageMeta gets a complete map instead of empty slots. Mapped slots are left untouched.
104
+ */
105
+ export const fillSlotMapFromCustomValues = (slotMap, customValues = {}) => {
106
+ if (!slotMap || typeof slotMap !== 'object') return slotMap;
107
+ const customValueByVarName = {};
108
+ Object.keys(customValues || {}).forEach((tagPath) => {
109
+ customValueByVarName[tagPath.split('.').pop()] = customValues[tagPath];
110
+ });
111
+ if (Object.keys(customValueByVarName).length === 0) return slotMap;
112
+ const hasValue = (value) => value != null && String(value).trim() !== '';
113
+ const filledSlotMap = { ...slotMap };
114
+ Object.keys(filledSlotMap).forEach((slotKey) => {
115
+ if (hasValue(filledSlotMap[slotKey])) return;
116
+ const customValue = customValueByVarName[getSlotKeyVarName(slotKey)];
117
+ if (hasValue(customValue)) filledSlotMap[slotKey] = String(customValue);
118
+ });
119
+ return filledSlotMap;
120
+ };
121
+
78
122
  /** RCS createMessageMeta: normalize media shape (mediaUrl, thumbnailUrl, height string). */
79
123
  export const normalizeRcsTestCardMedia = (media) => {
80
124
  if (!media || typeof media !== 'object') return undefined;
@@ -3176,13 +3176,11 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
3176
3176
  templateMsgText += `\n${formatMessage(messages.templateMessageUnsubscribeText)}`;
3177
3177
  }
3178
3178
 
3179
- // Get templateEditor from editData or construct from templateMessage
3180
- const templateEditorValue = isEditFlow && editData?.templateEditor
3181
- ? editData.templateEditor
3182
- : templateMsgText;
3179
+ // Raw body, slots intact — pairs with varMapped below; templateMsg carries the resolved text.
3180
+ const templateEditorValue = tempMsgArray.length > 0 ? tempMsgArray.join("") : templateMsgText;
3183
3181
 
3184
- // Get varMapped from editData or empty object
3185
- const varMappedValue = editData?.varMapped || {};
3182
+ // Live slot map (`{{1}}_1` -> mapped tag/value); `editData` has no varMapped, so it was always {}.
3183
+ const varMappedValue = { ...varMap };
3186
3184
 
3187
3185
  return {
3188
3186
  // Preview structure (for WhatsAppPreviewContent component)
@@ -3220,7 +3218,8 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
3220
3218
  mediaType: templateMediaType,
3221
3219
  isPreviewUrl: false,
3222
3220
  headerVarMapped: headerVarMappedData,
3223
- headerTemplate: isEditFlow ? updatedHeaderData.join("") : templateHeader,
3221
+ // Raw header, slots intact — pairs with headerVarMapped; templateHeader carries the resolved text.
3222
+ headerTemplate: tempHeaderData.length > 0 ? tempHeaderData.join("") : templateHeader,
3224
3223
  whatsappVideoSrcAndPreview,
3225
3224
  whatsappDocSource,
3226
3225
  whatsappDocParams,
@@ -3239,6 +3238,8 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
3239
3238
  docPreview,
3240
3239
  isEditFlow,
3241
3240
  updatedSmsEditor,
3241
+ tempMsgArray,
3242
+ tempHeaderData,
3242
3243
  templateMessage,
3243
3244
  unsubscribeRequired,
3244
3245
  isAuthenticationTemplate,
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap
3
- size: 57271855 bytes
4
- sha256: d1e9c0cd51e8b68c9acd1ddeee01866302a054ed135d594d34509d92a3e39124
3
+ size: 57295127 bytes
4
+ sha256: 3ca4752bd0410b9d7f0e57070855228ef817cc04531f23794bf27e2647b4f7a2