@capillarytech/creatives-library 8.0.319 → 8.0.321

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.
Files changed (139) hide show
  1. package/constants/unified.js +14 -0
  2. package/package.json +1 -1
  3. package/utils/templateVarUtils.js +172 -0
  4. package/utils/tests/tagValidations.test.js +34 -0
  5. package/utils/tests/templateVarUtils.test.js +160 -0
  6. package/v2Components/CapTagList/index.js +25 -22
  7. package/v2Components/CapTagList/style.scss +48 -0
  8. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
  9. package/v2Components/CapTagListWithInput/index.js +4 -0
  10. package/v2Components/CapWhatsappCTA/index.js +2 -0
  11. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  18. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  19. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  20. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  21. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  22. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  23. package/v2Components/CommonTestAndPreview/index.js +693 -155
  24. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  25. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  26. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  27. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  29. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  30. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  31. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  32. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  33. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  34. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  35. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  36. package/v2Components/FormBuilder/index.js +14 -1
  37. package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
  38. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  39. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
  40. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
  41. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  42. package/v2Components/SmsFallback/constants.js +73 -0
  43. package/v2Components/SmsFallback/index.js +956 -0
  44. package/v2Components/SmsFallback/index.scss +265 -0
  45. package/v2Components/SmsFallback/messages.js +78 -0
  46. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  47. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  48. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  49. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  50. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  51. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  52. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  53. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  54. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  55. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  56. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  57. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  58. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  59. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  60. package/v2Containers/BeeEditor/index.js +3 -0
  61. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  62. package/v2Containers/CreativesContainer/SlideBoxContent.js +64 -5
  63. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  64. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  65. package/v2Containers/CreativesContainer/constants.js +9 -0
  66. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  67. package/v2Containers/CreativesContainer/index.js +292 -99
  68. package/v2Containers/CreativesContainer/index.scss +51 -1
  69. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  70. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  71. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  72. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  73. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  74. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  75. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  76. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  77. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  78. package/v2Containers/Email/index.js +1 -0
  79. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +7 -1
  80. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  81. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
  82. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
  83. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -0
  84. package/v2Containers/EmailWrapper/index.js +4 -0
  85. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  86. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
  87. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +19 -0
  88. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +3 -0
  89. package/v2Containers/InAppWrapper/index.js +3 -0
  90. package/v2Containers/MobilePush/Create/index.js +2 -0
  91. package/v2Containers/MobilePush/Edit/index.js +2 -0
  92. package/v2Containers/MobilepushWrapper/index.js +3 -1
  93. package/v2Containers/Rcs/constants.js +32 -1
  94. package/v2Containers/Rcs/index.js +951 -873
  95. package/v2Containers/Rcs/index.scss +85 -6
  96. package/v2Containers/Rcs/messages.js +10 -1
  97. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  98. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  99. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  100. package/v2Containers/Rcs/tests/index.test.js +41 -38
  101. package/v2Containers/Rcs/tests/mockData.js +38 -0
  102. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  103. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  104. package/v2Containers/Rcs/utils.js +358 -10
  105. package/v2Containers/Sms/Create/index.js +83 -36
  106. package/v2Containers/Sms/Edit/index.js +2 -0
  107. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  108. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  109. package/v2Containers/SmsTrai/Create/index.js +9 -4
  110. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  111. package/v2Containers/SmsTrai/Edit/index.js +611 -128
  112. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  113. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  114. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  115. package/v2Containers/SmsWrapper/index.js +39 -8
  116. package/v2Containers/TagList/index.js +47 -2
  117. package/v2Containers/TagList/messages.js +4 -0
  118. package/v2Containers/TagList/tests/TagList.test.js +122 -20
  119. package/v2Containers/TagList/tests/mockdata.js +17 -0
  120. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  121. package/v2Containers/Templates/_templates.scss +61 -2
  122. package/v2Containers/Templates/actions.js +11 -0
  123. package/v2Containers/Templates/constants.js +2 -0
  124. package/v2Containers/Templates/index.js +90 -40
  125. package/v2Containers/Templates/sagas.js +57 -12
  126. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  127. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  128. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  129. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  130. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  131. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  132. package/v2Containers/TemplatesV2/index.js +86 -23
  133. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  134. package/v2Containers/Viber/index.js +5 -0
  135. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
  136. package/v2Containers/WebPush/Create/index.js +9 -1
  137. package/v2Containers/Whatsapp/index.js +8 -20
  138. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +598 -34
  139. package/v2Containers/Zalo/index.js +2 -0
@@ -159,6 +159,20 @@ export const TAG_CONTENT_REGEX = /{{([^}]+)}}/g;
159
159
  export const ENTRY_TRIGGER_TAG_REGEX = /\bentryTrigger\.\w+(?:\.\w+)?(?:\(\w+\))?/g;
160
160
  export const SKIP_TAGS_REGEX_GROUPS = ["dynamic_expiry_date_after_\\d+_days.FORMAT_\\d", "unsubscribe\\(#[a-zA-Z\\d]{6}\\)", "Link_to_[a-zA-Z]", "SURVEY.*.TOKEN", "^[A-Za-z].*\\([a-zA-Z\\d]*\\)", "referral_unique_(code|url).*userid"];
161
161
 
162
+ // --- Template variable tokens (`{{var}}`, DLT `{#var#}`) ---
163
+ /** Global: all `{{…}}` placeholders in a string. */
164
+ export const DEFAULT_MUSTACHE_VAR_REGEX = /\{\{[^}]+\}\}/g;
165
+ /** Global: `{{…}}` or DLT `{#…#}` tokens (SMS combined mode). */
166
+ export const COMBINED_SMS_TEMPLATE_VAR_REGEX = /\{\{[^}]+\}\}|\{\#[^#]*\#\}/g;
167
+ /** Full-string check: one mustache token. */
168
+ export const MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX = /^\{\{[^}]+\}\}$/;
169
+ /** Full-string check: one DLT hash token. */
170
+ export const DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX = /^\{\#[^#]*\#\}$/;
171
+ /** Global with capture group: inner name inside `{{name}}`. */
172
+ export const MUSTACHE_VAR_NAME_CAPTURE_REGEX = /\{\{([^}]+)\}\}/g;
173
+ /** Global with capture group: inner body inside `{#body#}`. */
174
+ export const DLT_VAR_BODY_CAPTURE_REGEX = /\{\#([^#]*)\#\}/g;
175
+
162
176
  export const GET_TRANSLATION_MAPPED = {
163
177
  'en': 'en-US',
164
178
  'zh-cn': 'zh',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.319",
4
+ "version": "8.0.321",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Shared utilities for templates containing {{var}} and DLT `{#var#}` tokens.
3
+ * Same split process used by WhatsApp/RCS: match vars with regex, then split content at each var.
4
+ */
5
+
6
+ import {
7
+ COMBINED_SMS_TEMPLATE_VAR_REGEX,
8
+ DEFAULT_MUSTACHE_VAR_REGEX,
9
+ DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX,
10
+ DLT_VAR_BODY_CAPTURE_REGEX,
11
+ MUSTACHE_VAR_NAME_CAPTURE_REGEX,
12
+ MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX,
13
+ } from '../constants/unified';
14
+
15
+ export { COMBINED_SMS_TEMPLATE_VAR_REGEX, DEFAULT_MUSTACHE_VAR_REGEX } from '../constants/unified';
16
+
17
+ const isMustacheVarToken = (s) =>
18
+ typeof s === 'string' && MUSTACHE_VAR_TOKEN_FULL_STRING_REGEX.test(s);
19
+
20
+ export const isDltHashVarToken = (s) =>
21
+ typeof s === 'string' && DLT_HASH_VAR_TOKEN_FULL_STRING_REGEX.test(s);
22
+
23
+ export const isAnyTemplateVarToken = (s) => isMustacheVarToken(s) || isDltHashVarToken(s);
24
+
25
+ /**
26
+ * `RegExp.prototype.exec` only advances `lastIndex` when the `g` and/or `y` flag is set.
27
+ * A non-global regex in a `while ((m = re.exec(s)) !== null)` loop never advances and can run forever.
28
+ *
29
+ * @param {RegExp} regex
30
+ * @returns {RegExp} Same instance if already global; otherwise a new RegExp with `g` appended to flags.
31
+ */
32
+ function ensureGlobalRegexForExecLoop(regex) {
33
+ if (!regex || !(regex instanceof RegExp) || regex.global) {
34
+ return regex;
35
+ }
36
+ return new RegExp(regex.source, `${regex.flags}g`);
37
+ }
38
+
39
+ /**
40
+ * Splits `content` into alternating plain-text segments and variable tokens, using the order of
41
+ * matches in `matchedVariableTokens` (e.g. from `String.prototype.match` with a global regex).
42
+ *
43
+ * @param {string[]} matchedVariableTokens - Matched tokens in left-to-right order
44
+ * @param {string} content - Full template string
45
+ * @returns {string[]}
46
+ */
47
+ export const splitContentByOrderedVarTokens = (matchedVariableTokens, content) => {
48
+ const segmentList = [];
49
+ const tokenQueue = [...(matchedVariableTokens ?? [])];
50
+ let remainder = content ?? '';
51
+ while ((remainder?.length ?? 0) > 0) {
52
+ const nextVarToken = tokenQueue?.[0];
53
+ if (nextVarToken == null || nextVarToken === '') {
54
+ segmentList.push(remainder);
55
+ break;
56
+ }
57
+ const varStartIndex = remainder.indexOf(nextVarToken);
58
+ if (varStartIndex !== -1) {
59
+ segmentList.push(remainder.substring(0, varStartIndex));
60
+ segmentList.push(nextVarToken);
61
+ const afterVar = varStartIndex + (nextVarToken?.length ?? 0);
62
+ remainder = remainder.substring(afterVar, remainder?.length ?? 0);
63
+ tokenQueue.shift();
64
+ } else {
65
+ segmentList.push(remainder);
66
+ break;
67
+ }
68
+ }
69
+ return segmentList;
70
+ };
71
+
72
+ /**
73
+ * Splits a template string into an array of text + {{var}} segments using the given regex.
74
+ *
75
+ * @param {string} str - Template string
76
+ * @param {RegExp} [varRegex] - Regex that matches var tokens; defaults to DEFAULT_MUSTACHE_VAR_REGEX
77
+ * @returns {string[]}
78
+ */
79
+ export const splitTemplateVarString = (str = '', varRegex = DEFAULT_MUSTACHE_VAR_REGEX) => {
80
+ if (!str) return [];
81
+ const matchedVariableTokens = str.match(varRegex) || [];
82
+ return splitContentByOrderedVarTokens(matchedVariableTokens, str).filter(
83
+ (segment) => segment === 0 || segment
84
+ );
85
+ };
86
+
87
+ /**
88
+ * Extracts unique variable names from `{{var}}` (and, when using default capture, `{#var#}`) strings.
89
+ *
90
+ * @param {string} templateStr
91
+ * @param {RegExp} [captureRegex] - regex with a single capture group for the var name; if omitted, also scans DLT `{#…#}`.
92
+ * @returns {string[]}
93
+ */
94
+ export const extractTemplateVariables = (templateStr = '', captureRegex) => {
95
+ if (!templateStr) return [];
96
+ const variables = [];
97
+ const add = (name) => {
98
+ const n = (name || '').trim();
99
+ if (n && !variables.includes(n)) variables.push(n);
100
+ };
101
+ const mustacheRe = ensureGlobalRegexForExecLoop(
102
+ captureRegex || MUSTACHE_VAR_NAME_CAPTURE_REGEX,
103
+ );
104
+ let match;
105
+ while ((match = mustacheRe.exec(templateStr)) !== null) {
106
+ add(match?.[1]);
107
+ }
108
+ if (!captureRegex) {
109
+ const dltRe = new RegExp(DLT_VAR_BODY_CAPTURE_REGEX.source, DLT_VAR_BODY_CAPTURE_REGEX.flags);
110
+ let dltMatch;
111
+ while ((dltMatch = dltRe.exec(templateStr)) !== null) {
112
+ add(dltMatch?.[1] || 'var');
113
+ }
114
+ }
115
+ return variables;
116
+ };
117
+
118
+ /**
119
+ * SMS / DLT template preview: replace `{{…}}` / `{#…#}` tokens using `varMapData` keys `${token}_${index}`.
120
+ * Used by SmsTraiEdit (RCS SMS fallback) and UnifiedPreview fallback SMS bubble.
121
+ * DLT `{#…#}`: empty / unset slot values show the raw token (matches DLT preview UX); mustache `{{…}}` still
122
+ * resolves to empty when the slot is cleared.
123
+ */
124
+ export const getFallbackResolvedContent = (templateStr = '', varMapData = {}) => {
125
+ const fallbackVarSlotMap = varMapData ?? {};
126
+ const templateSegments = splitTemplateVarString(templateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX);
127
+ return templateSegments
128
+ .map((segment, segmentIndex) => {
129
+ const isVariableToken = typeof segment === 'string' && isAnyTemplateVarToken(segment);
130
+ if (!isVariableToken) return segment;
131
+ const slotKey = `${segment}_${segmentIndex}`;
132
+ if (Object.prototype.hasOwnProperty.call(fallbackVarSlotMap, slotKey)) {
133
+ const slotValue = fallbackVarSlotMap[slotKey];
134
+ if (isDltHashVarToken(segment)) {
135
+ if (slotValue == null) return segment;
136
+ const str = String(slotValue);
137
+ if (str.trim() === '') return segment;
138
+ return str;
139
+ }
140
+ return slotValue == null ? '' : String(slotValue);
141
+ }
142
+ if (isDltHashVarToken(segment)) return segment;
143
+ return '';
144
+ })
145
+ .join('');
146
+ };
147
+
148
+ /**
149
+ * SMS fallback **card** (library list): show filled slot text; if a slot is empty, unset, or
150
+ * whitespace-only, show the raw `{{…}}` / `{#…#}` token so “save without labels” still shows
151
+ * `{#var#}` in the card. {@link getFallbackResolvedContent} keeps DLT placeholders in preview when
152
+ * slots are empty; mustache cleared slots can still render empty.
153
+ */
154
+ export const getFallbackResolvedContentForCardDisplay = (templateStr = '', varMapData = {}) => {
155
+ const fallbackVarSlotMap = varMapData ?? {};
156
+ const templateSegments = splitTemplateVarString(templateStr, COMBINED_SMS_TEMPLATE_VAR_REGEX);
157
+ return templateSegments
158
+ .map((segment, segmentIndex) => {
159
+ const isVariableToken = typeof segment === 'string' && isAnyTemplateVarToken(segment);
160
+ if (!isVariableToken) return segment;
161
+ const slotKey = `${segment}_${segmentIndex}`;
162
+ if (Object.prototype.hasOwnProperty.call(fallbackVarSlotMap, slotKey)) {
163
+ const slotValue = fallbackVarSlotMap[slotKey];
164
+ if (slotValue == null) return segment;
165
+ const str = String(slotValue);
166
+ if (str.trim() === '') return segment;
167
+ return str;
168
+ }
169
+ return segment;
170
+ })
171
+ .join('');
172
+ };
@@ -242,6 +242,7 @@ describe("validateTags", () => {
242
242
  tagsParam,
243
243
  location,
244
244
  tagModule,
245
+ waitEventContextTags: {},
245
246
  });
246
247
 
247
248
  expect(result.valid).toEqual(true);
@@ -273,6 +274,7 @@ describe("validateTags", () => {
273
274
  tagsParam: tagsParamLocal,
274
275
  location,
275
276
  tagModule,
277
+ waitEventContextTags: {},
276
278
  });
277
279
 
278
280
  expect(result.valid).toEqual(true);
@@ -310,6 +312,7 @@ describe("validateTags", () => {
310
312
  tagsParam: tagsParamLocal,
311
313
  location,
312
314
  tagModule,
315
+ waitEventContextTags: {},
313
316
  });
314
317
 
315
318
  expect(result.valid).toEqual(false);
@@ -335,6 +338,7 @@ describe("validateTags", () => {
335
338
  tagsParam: tagsParamUnsubscribe,
336
339
  location,
337
340
  tagModule,
341
+ waitEventContextTags: {},
338
342
  });
339
343
  expect(resultMissing.missingTags).toContain("unsubscribe");
340
344
  expect(resultMissing.valid).toBe(false);
@@ -345,6 +349,7 @@ describe("validateTags", () => {
345
349
  tagsParam: tagsParamUnsubscribe,
346
350
  location,
347
351
  tagModule,
352
+ waitEventContextTags: {},
348
353
  });
349
354
  expect(resultSkipped.missingTags).not.toContain("unsubscribe");
350
355
  expect(resultSkipped.valid).toBe(true);
@@ -360,6 +365,35 @@ describe("validateTags", () => {
360
365
  expect(resultWhitespace.valid).toBe(true);
361
366
  expect(resultWhitespace.unsupportedTags ?? []).toEqual([]);
362
367
  });
368
+
369
+ it('should treat tags from waitEventContextTags as supported', () => {
370
+ const content = 'Hello {{waitEvent.orderId}}';
371
+ const tagsParam = [];
372
+ const injectedTagsParams = [];
373
+ const location = { query: { module: 'DEFAULT' } };
374
+ const tagModule = null;
375
+ const waitEventContextTags = {
376
+ block1: {
377
+ eventName: 'Order Placed',
378
+ blockName: 'Wait Block',
379
+ tags: [{ tagName: 'waitEvent.orderId', label: 'Order ID' }],
380
+ },
381
+ };
382
+
383
+ const result = validateTags({
384
+ content,
385
+ tagsParam,
386
+ injectedTagsParams,
387
+ location,
388
+ tagModule,
389
+ eventContextTags: [],
390
+ waitEventContextTags,
391
+ });
392
+
393
+ expect(result.valid).toEqual(true);
394
+ expect(result.missingTags).toEqual([]);
395
+ expect(result.isBraceError).toEqual(false);
396
+ });
363
397
  });
364
398
 
365
399
  describe('validateTags wrapper (v2 consumers)', () => {
@@ -0,0 +1,160 @@
1
+ import {
2
+ extractTemplateVariables,
3
+ splitTemplateVarString,
4
+ splitContentByOrderedVarTokens,
5
+ getFallbackResolvedContent,
6
+ getFallbackResolvedContentForCardDisplay,
7
+ isDltHashVarToken,
8
+ isAnyTemplateVarToken,
9
+ } from '../templateVarUtils';
10
+
11
+ describe('templateVarUtils', () => {
12
+ describe('splitContentByOrderedVarTokens', () => {
13
+ it('pushes remainder when next token is not found in string', () => {
14
+ expect(splitContentByOrderedVarTokens(['{{b}}'], 'hello')).toEqual(['hello']);
15
+ });
16
+
17
+ it('handles empty token queue by pushing remainder', () => {
18
+ expect(splitContentByOrderedVarTokens([], 'rest')).toEqual(['rest']);
19
+ });
20
+
21
+ it('handles null matchedVariableTokens as empty queue', () => {
22
+ expect(splitContentByOrderedVarTokens(null, 'abc')).toEqual(['abc']);
23
+ });
24
+
25
+ it('stops and pushes remainder when next token in queue is empty string', () => {
26
+ expect(splitContentByOrderedVarTokens(['{{a}}', ''], 'x{{a}}y')).toEqual(['x', '{{a}}', 'y']);
27
+ });
28
+
29
+ it('splits multiple ordered tokens across content', () => {
30
+ expect(splitContentByOrderedVarTokens(['{{a}}', '{{b}}'], 'p{{a}}q{{b}}r')).toEqual([
31
+ 'p',
32
+ '{{a}}',
33
+ 'q',
34
+ '{{b}}',
35
+ 'r',
36
+ ]);
37
+ });
38
+
39
+ it('returns empty segment list when content is null and queue is empty', () => {
40
+ expect(splitContentByOrderedVarTokens([], null)).toEqual([]);
41
+ });
42
+ });
43
+
44
+ describe('splitTemplateVarString', () => {
45
+ it('returns empty array for falsy string', () => {
46
+ expect(splitTemplateVarString('')).toEqual([]);
47
+ expect(splitTemplateVarString(null)).toEqual([]);
48
+ });
49
+
50
+ it('splits mustache segments', () => {
51
+ const parts = splitTemplateVarString('Hi {{name}}!');
52
+ expect(parts.some((p) => p.includes('name'))).toBe(true);
53
+ });
54
+
55
+ it('omits empty segments after filtering', () => {
56
+ expect(splitTemplateVarString('a{{x}}b')).toEqual(['a', '{{x}}', 'b']);
57
+ });
58
+ });
59
+
60
+ describe('getFallbackResolvedContent', () => {
61
+ it('replaces var segments from varMapData', () => {
62
+ const out = getFallbackResolvedContent('{{a}}', { '{{a}}_0': 'X' });
63
+ expect(out).toBe('X');
64
+ });
65
+
66
+ it('keeps DLT token string when slot empty', () => {
67
+ const out = getFallbackResolvedContent('{#v#}', {});
68
+ expect(out).toContain('#');
69
+ });
70
+
71
+ it('returns empty string for mustache slot when map has no value', () => {
72
+ const out = getFallbackResolvedContent('Hi {{missing}}', {});
73
+ expect(out).toBe('Hi ');
74
+ });
75
+
76
+ it('treats null as empty; explicit empty string is kept (cleared input)', () => {
77
+ expect(getFallbackResolvedContent('{{a}}', { '{{a}}_0': null })).toBe('');
78
+ expect(getFallbackResolvedContent('{{a}}', { '{{a}}_0': '' })).toBe('');
79
+ });
80
+
81
+ it('keeps DLT token in preview when slot map has empty string (no tag yet)', () => {
82
+ expect(getFallbackResolvedContent('{#v#}', { '{#v#}_0': '' })).toBe('{#v#}');
83
+ });
84
+
85
+ it('keeps DLT token in preview when slot map has null', () => {
86
+ expect(getFallbackResolvedContent('{#v#}', { '{#v#}_0': null })).toBe('{#v#}');
87
+ });
88
+
89
+ it('uses slot replacement when value is non-empty string', () => {
90
+ expect(getFallbackResolvedContent('A{{b}}C', { '{{b}}_1': 'ok' })).toBe('AokC');
91
+ });
92
+ });
93
+
94
+ describe('getFallbackResolvedContentForCardDisplay', () => {
95
+ it('shows raw tokens when map is empty or slots unset', () => {
96
+ expect(getFallbackResolvedContentForCardDisplay('Hi {{name}}', {})).toBe('Hi {{name}}');
97
+ expect(getFallbackResolvedContentForCardDisplay('{#var#}', {})).toBe('{#var#}');
98
+ });
99
+
100
+ it('shows raw token when slot key exists but value is empty (no labels)', () => {
101
+ expect(getFallbackResolvedContentForCardDisplay('{#var#}', { '{#var#}_0': '' })).toBe('{#var#}');
102
+ expect(getFallbackResolvedContentForCardDisplay('{{a}}', { '{{a}}_0': '' })).toBe('{{a}}');
103
+ });
104
+
105
+ it('replaces when slot has non-empty value', () => {
106
+ expect(getFallbackResolvedContentForCardDisplay('Hi {{name}}', { '{{name}}_1': 'Pat' })).toBe('Hi Pat');
107
+ });
108
+ });
109
+
110
+ describe('isDltHashVarToken / isAnyTemplateVarToken', () => {
111
+ it('detects DLT hash token', () => {
112
+ expect(isDltHashVarToken('{#x#}')).toBe(true);
113
+ expect(isDltHashVarToken('plain')).toBe(false);
114
+ });
115
+
116
+ it('isAnyTemplateVarToken combines mustache and DLT', () => {
117
+ expect(isAnyTemplateVarToken('{{a}}')).toBe(true);
118
+ expect(isAnyTemplateVarToken('{#x#}')).toBe(true);
119
+ });
120
+ });
121
+
122
+ describe('extractTemplateVariables', () => {
123
+ it('returns empty array for empty or missing template', () => {
124
+ expect(extractTemplateVariables('')).toEqual([]);
125
+ expect(extractTemplateVariables()).toEqual([]);
126
+ });
127
+
128
+ it('includes DLT {#var#} names when no custom capture regex', () => {
129
+ const vars = extractTemplateVariables('Hello {#promo#} and {{name}}');
130
+ expect(vars).toEqual(expect.arrayContaining(['promo', 'name']));
131
+ });
132
+
133
+ it('skips DLT scan when captureRegex is provided', () => {
134
+ const re = /\{\{([^}]+)\}\}/g;
135
+ expect(extractTemplateVariables('{#x#} {{y}}', re)).toEqual(['y']);
136
+ });
137
+
138
+ it('dedupes duplicate variable names', () => {
139
+ expect(extractTemplateVariables('{{a}} {{a}}')).toEqual(['a']);
140
+ });
141
+
142
+ it('uses default name var when DLT body capture is empty', () => {
143
+ const vars = extractTemplateVariables('{##}');
144
+ expect(vars).toContain('var');
145
+ });
146
+ });
147
+
148
+ describe('extractTemplateVariables (regex global)', () => {
149
+ it('extracts all vars when caller passes a non-global capture regex (avoids infinite exec loop)', () => {
150
+ const nonGlobal = /\{\{([^}]+)\}\}/;
151
+ expect(nonGlobal.global).toBe(false);
152
+ expect(extractTemplateVariables('{{a}} and {{b}}', nonGlobal)).toEqual(['a', 'b']);
153
+ });
154
+
155
+ it('still works when capture regex already has the global flag', () => {
156
+ const globalRe = /\{\{([^}]+)\}\}/g;
157
+ expect(extractTemplateVariables('{{x}} {{y}}', globalRe)).toEqual(['x', 'y']);
158
+ });
159
+ });
160
+ });
@@ -211,6 +211,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
211
211
  } else if (info && info.selectedNodes && info.selectedNodes.length > 0 && !info.selectedNodes[0].props.isLeaf) {
212
212
  this.handleOnExpand(selectedKeys[0]);
213
213
  }
214
+ this.setState({expandedKeys: []})
214
215
  }
215
216
  };
216
217
 
@@ -233,6 +234,13 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
233
234
  }
234
235
  };
235
236
 
237
+ /** Single-line ellipsis within popover width; full label on hover via CapTooltip. */
238
+ wrapTreeTitle = (displayNode, text) => (
239
+ <CapTooltip title={displayNode}>
240
+ <CapLabel.CapLabelInline type="label15" className="cap-tag-list-tree-title-wrap">{text || displayNode}</CapLabel.CapLabelInline>
241
+ </CapTooltip>
242
+ );
243
+
236
244
  renderDynamicTagFlow = () => {
237
245
  this.setState({showModal: true, visible: false});
238
246
  };
@@ -280,7 +288,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
280
288
  if (temp?.length) {
281
289
  const tagValue = (
282
290
  <CapTreeNode
283
- title={disabled ? <CapTooltip title={loyaltyAttrDisableText}>{val?.name}</CapTooltip> : val?.name}
291
+ title={disabled ? this.wrapTreeTitle(loyaltyAttrDisableText, val?.name) : this.wrapTreeTitle(val?.name)}
284
292
  tag={val}
285
293
  key={val?.incentiveSeriesId ? `${key}(${val?.incentiveSeriesId})` : `${key}`}
286
294
  disabled={disabled}
@@ -303,17 +311,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
303
311
  <CapTreeNode
304
312
  title={
305
313
  childDisabled ? (
306
- <CapTooltip
307
- title={
308
- key === CUSTOMER_BARCODE_TAG
309
- ? customerBarcodeDisableText
310
- : loyaltyAttrDisableText
311
- }
312
- >
313
- {val?.desc || val?.name}
314
- </CapTooltip>
314
+ this.wrapTreeTitle(key === CUSTOMER_BARCODE_TAG ? customerBarcodeDisableText : loyaltyAttrDisableText, val?.desc || val?.name)
315
315
  ) : (
316
- val?.desc || val?.name
316
+ this.wrapTreeTitle(val?.desc || val?.name)
317
317
  )
318
318
  }
319
319
  tag={val}
@@ -339,17 +339,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
339
339
  <CapTreeNode
340
340
  title={
341
341
  childDisabled ? (
342
- <CapTooltip
343
- title={
344
- key === CUSTOMER_BARCODE_TAG
345
- ? customerBarcodeDisableText
346
- : loyaltyAttrDisableText
347
- }
348
- >
349
- {val?.desc || val?.name}
350
- </CapTooltip>
342
+ this.wrapTreeTitle(key === CUSTOMER_BARCODE_TAG ? customerBarcodeDisableText : loyaltyAttrDisableText, val?.desc || val?.name)
351
343
  ) : (
352
- val?.desc || val?.name
344
+ this.wrapTreeTitle(val?.desc || val?.name)
353
345
  )
354
346
  }
355
347
  tag={val}
@@ -382,6 +374,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
382
374
  render() {
383
375
  const {
384
376
  hidePopover = false, intl = {}, moduleFilterEnabled, label, modalProps, channel, fetchingSchemaError = false,
377
+ overlayStyle,
378
+ overlayClassName,
379
+ getPopupContainer,
385
380
  } = this.props;
386
381
  const {formatMessage} = intl;
387
382
  const {
@@ -404,7 +399,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
404
399
  },
405
400
  ];
406
401
  const contentSection = (
407
- <CapRow>
402
+ <CapRow className="cap-tag-list-popover-inner">
408
403
  <CapSpin tip={formatMessage(messages.gettingTags)} spinning={shouldShowLoading}>
409
404
  <Search
410
405
  style={{ marginBottom: 8, width: '250px'}}
@@ -472,8 +467,12 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
472
467
  visible={fetchingSchemaError ? false : visible}
473
468
  onVisibleChange={this.togglePopoverVisibility}
474
469
  content={contentSection}
470
+ overlayClassName="cap-tag-list-popover-overlay"
475
471
  trigger="click"
476
472
  placement={this.props.popoverPlacement || (channel === EMAIL.toUpperCase() ? "leftTop" : "rightTop")}
473
+ overlayStyle={overlayStyle}
474
+ overlayClassName={overlayClassName}
475
+ getPopupContainer={getPopupContainer}
477
476
  >
478
477
  <CapTooltip
479
478
  title={
@@ -545,6 +544,10 @@ CapTagList.propTypes = {
545
544
  disableTooltipMsg: PropTypes.string,
546
545
  fetchingSchemaError: PropTypes.bool,
547
546
  popoverPlacement: PropTypes.string,
547
+ overlayStyle: PropTypes.object,
548
+ overlayClassName: PropTypes.string,
549
+ /** e.g. () => document.body — avoids overflow/stacking issues inside slideboxes */
550
+ getPopupContainer: PropTypes.func,
548
551
  };
549
552
 
550
553
  CapTagList.defaultValue = {
@@ -1,5 +1,53 @@
1
1
  @import "~@capillarytech/cap-ui-library/styles/_variables";
2
2
 
3
+ // Tag list popover: keep overlay width aligned with search (250px); tree rows ellipsis + tooltip for full text
4
+ .cap-tag-list-popover-overlay.ant-popover {
5
+ .ant-popover-inner-content {
6
+ max-width: 20rem;
7
+ box-sizing: border-box;
8
+ overflow: hidden;
9
+ }
10
+ }
11
+
12
+ .cap-tag-list-popover-inner {
13
+ max-width: 20rem;
14
+ min-width: 0;
15
+ box-sizing: border-box;
16
+
17
+ .ant-tree.cap-tree-v2.ant-tree-icon-hide {
18
+ width: 100%;
19
+ max-width: 100%;
20
+
21
+ ul {
22
+ max-width: 100%;
23
+ }
24
+
25
+ li {
26
+ overflow: hidden;
27
+ max-width: 100%;
28
+ }
29
+
30
+ li .ant-tree-node-content-wrapper {
31
+ width: calc(100% - 3.5rem); // leave room for switcher (~24px)
32
+ max-width: calc(100% - 3.5rem);
33
+ overflow: hidden;
34
+ vertical-align: top;
35
+ box-sizing: border-box;
36
+ }
37
+
38
+ .cap-tag-list-tree-title-wrap {
39
+ display: inline-block;
40
+ width: 100%;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ white-space: nowrap;
44
+ max-width: 100%;
45
+ vertical-align: top;
46
+ margin-top: 0.5rem;
47
+ }
48
+ }
49
+ }
50
+
3
51
  @media (max-height: 25rem) {
4
52
  .ant-tree.cap-tree-v2.ant-tree-icon-hide {
5
53
  height: 8.5714rem;
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { IntlProvider } from 'react-intl';
5
+ import CapTagListWithInput from '../index';
6
+
7
+ const capturedTagListProps = { current: null };
8
+
9
+ jest.mock('../../../v2Containers/TagList', () => {
10
+ const React = require('react');
11
+ const Mock = (props) => {
12
+ capturedTagListProps.current = props;
13
+ return <div data-testid="mock-tag-list">TagList</div>;
14
+ };
15
+ return Mock;
16
+ });
17
+
18
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => ({ children }) => <div>{children}</div>);
19
+ jest.mock('@capillarytech/cap-ui-library/CapColumn', () => ({ children }) => <div>{children}</div>);
20
+ jest.mock('@capillarytech/cap-ui-library/CapHeading', () => () => null);
21
+ jest.mock('@capillarytech/cap-ui-library/CapInput', () => () => <input data-testid="cap-input" />);
22
+
23
+ const waitMap = {
24
+ b1: { eventName: 'Order Placed', blockName: 'Wait', tags: [] },
25
+ };
26
+
27
+ describe('CapTagListWithInput', () => {
28
+ beforeEach(() => {
29
+ capturedTagListProps.current = null;
30
+ });
31
+
32
+ it('forwards waitEventContextTags to TagList', () => {
33
+ render(
34
+ <IntlProvider locale="en" messages={{}}>
35
+ <CapTagListWithInput
36
+ inputId="test-url"
37
+ inputOnChange={jest.fn()}
38
+ waitEventContextTags={waitMap}
39
+ onTagSelect={jest.fn()}
40
+ onContextChange={jest.fn()}
41
+ />
42
+ </IntlProvider>
43
+ );
44
+
45
+ expect(screen.getByTestId('mock-tag-list')).toBeInTheDocument();
46
+ expect(capturedTagListProps.current.waitEventContextTags).toEqual(waitMap);
47
+ });
48
+
49
+ it('uses default empty object for waitEventContextTags when omitted', () => {
50
+ render(
51
+ <IntlProvider locale="en" messages={{}}>
52
+ <CapTagListWithInput
53
+ inputId="test-url"
54
+ inputOnChange={jest.fn()}
55
+ onTagSelect={jest.fn()}
56
+ onContextChange={jest.fn()}
57
+ />
58
+ </IntlProvider>
59
+ );
60
+
61
+ expect(capturedTagListProps.current.waitEventContextTags).toEqual({});
62
+ });
63
+ });