@capillarytech/creatives-library 8.0.307-alpha.0 → 8.0.307-alpha.5

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 (70) hide show
  1. package/constants/unified.js +1 -3
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +95 -36
  6. package/utils/tagValidations.js +231 -83
  7. package/utils/tests/commonUtil.test.js +124 -147
  8. package/utils/tests/tagValidations.test.js +388 -437
  9. package/v2Components/CapTagListWithInput/index.js +4 -0
  10. package/v2Components/CapWhatsappCTA/index.js +2 -0
  11. package/v2Components/ErrorInfoNote/index.js +5 -2
  12. package/v2Components/FormBuilder/index.js +218 -137
  13. package/v2Components/FormBuilder/messages.js +8 -0
  14. package/v2Components/HtmlEditor/HTMLEditor.js +10 -0
  15. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  16. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  17. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +5 -1
  18. package/v2Containers/BeeEditor/index.js +3 -0
  19. package/v2Containers/Cap/mockData.js +14 -0
  20. package/v2Containers/Cap/reducer.js +55 -3
  21. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  22. package/v2Containers/CreativesContainer/SlideBoxContent.js +26 -5
  23. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  24. package/v2Containers/CreativesContainer/constants.js +0 -6
  25. package/v2Containers/CreativesContainer/index.js +10 -47
  26. package/v2Containers/Email/index.js +6 -1
  27. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +79 -23
  28. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  29. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +121 -20
  30. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +1 -0
  31. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -0
  32. package/v2Containers/EmailWrapper/index.js +4 -0
  33. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  34. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +1 -0
  35. package/v2Containers/FTP/index.js +51 -2
  36. package/v2Containers/FTP/messages.js +4 -0
  37. package/v2Containers/InApp/index.js +110 -35
  38. package/v2Containers/InApp/tests/index.test.js +6 -17
  39. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +1 -0
  40. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +3 -0
  41. package/v2Containers/InAppWrapper/index.js +3 -0
  42. package/v2Containers/InappAdvance/index.js +115 -4
  43. package/v2Containers/InappAdvance/tests/index.test.js +0 -2
  44. package/v2Containers/Line/Container/Text/index.js +1 -0
  45. package/v2Containers/MobilePush/Create/index.js +21 -59
  46. package/v2Containers/MobilePush/Edit/index.js +22 -48
  47. package/v2Containers/MobilePushNew/index.js +33 -12
  48. package/v2Containers/MobilepushWrapper/index.js +3 -3
  49. package/v2Containers/Rcs/index.js +39 -12
  50. package/v2Containers/Sms/Create/index.js +5 -39
  51. package/v2Containers/Sms/Create/messages.js +0 -4
  52. package/v2Containers/Sms/Edit/index.js +5 -35
  53. package/v2Containers/Sms/commonMethods.js +6 -3
  54. package/v2Containers/SmsTrai/Edit/index.js +51 -11
  55. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  56. package/v2Containers/SmsWrapper/index.js +2 -2
  57. package/v2Containers/TagList/index.js +41 -2
  58. package/v2Containers/TagList/messages.js +4 -0
  59. package/v2Containers/TagList/tests/TagList.test.js +13 -1
  60. package/v2Containers/TagList/tests/mockdata.js +17 -0
  61. package/v2Containers/TemplatesV2/index.js +13 -28
  62. package/v2Containers/Viber/index.js +6 -0
  63. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +5 -1
  64. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +10 -0
  65. package/v2Containers/WebPush/Create/index.js +11 -3
  66. package/v2Containers/WebPush/Create/utils/validation.js +8 -17
  67. package/v2Containers/WebPush/Create/utils/validation.test.js +26 -44
  68. package/v2Containers/Whatsapp/index.js +23 -9
  69. package/v2Containers/Zalo/index.js +14 -3
  70. package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
@@ -43,6 +43,7 @@ export const HOSPITALITY_BASED_SCOPE = 'HOSPITALITY_BASED_SCOPE';
43
43
  export const REGISTRATION_CUSTOM_FIELD = 'Registration custom fields';
44
44
  export const GIFT_CARDS = 'GIFT_CARDS';
45
45
  export const PROMO_ENGINE = 'PROMO_ENGINE';
46
+ export const LIQUID_SUPPORT = 'ENABLE_LIQUID_SUPPORT';
46
47
  export const ENABLE_NEW_MPUSH = 'ENABLE_NEW_MPUSH';
47
48
  export const ENABLE_NEW_EDITOR_FLOW_INAPP = 'ENABLE_NEW_EDITOR_FLOW_INAPP';
48
49
  export const SUPPORT_CK_EDITOR = 'SUPPORT_CK_EDITOR';
@@ -150,9 +151,6 @@ export const BADGES_ENROLL = 'BADGES_ENROLL';
150
151
  export const BADGES_ISSUE = 'BADGES_ISSUE';
151
152
  export const CUSTOMER_BARCODE_TAG = 'customer_barcode';
152
153
  export const COPY_OF = 'Copy of';
153
- export const UNSUBSCRIBE_TAG = 'unsubscribe';
154
- /** Whitespace-tolerant check for {{ unsubscribe }}-style tag in content. */
155
- export const UNSUBSCRIBE_TAG_REGEX = new RegExp(`\\{\\{\\s*${UNSUBSCRIBE_TAG}\\s*\\}\\}`);
156
154
  export const ENTRY_TRIGGER_TAG_REGEX = /\bentryTrigger\.\w+(?:\.\w+)?(?:\(\w+\))?/g;
157
155
  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"];
158
156
 
package/initialState.js CHANGED
@@ -14,7 +14,9 @@ export default {
14
14
  metaEntities: {
15
15
  tags: [],
16
16
  layouts: [],
17
+ tagLookupMap: {},
17
18
  },
19
+ liquidTags: [],
18
20
  fetchingLiquidTags: false,
19
21
  fetchingSchema: true,
20
22
  fetchingSchemaError: '',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.307-alpha.0",
4
+ "version": "8.0.307-alpha.5",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  BADGES_ISSUE,
23
23
  ENABLE_WECHAT,
24
24
  ENABLE_WEBPUSH,
25
+ LIQUID_SUPPORT,
25
26
  SUPPORT_CK_EDITOR,
26
27
  ENABLE_NEW_MPUSH,
27
28
  ENABLE_NEW_EDITOR_FLOW_INAPP
@@ -91,6 +92,12 @@ export const hasPromoFeature = Auth.hasFeatureAccess.bind(
91
92
  null,
92
93
  PROMO_ENGINE,
93
94
  );
95
+
96
+ export const hasLiquidSupportFeature = Auth.hasFeatureAccess.bind(
97
+ null,
98
+ LIQUID_SUPPORT,
99
+ );
100
+
94
101
  export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
95
102
  null,
96
103
  SUPPORT_CK_EDITOR,
@@ -126,11 +133,7 @@ export const hasCustomerBarcodeFeatureEnabled = Auth.hasFeatureAccess.bind(
126
133
  ENABLE_CUSTOMER_BARCODE_TAG,
127
134
  );
128
135
 
129
- // Note: The "EMAIL_UNSUBSCRIBE_TAG_MANDATORY" feature flag determines if the Unsubscribe tag in email is optional.
130
- // When this flag is enabled for an org, the Unsubscribe tag is NOT mandatory in the email flow.
131
- // This is as per the requirement in the tech doc:
132
- // https://capillarytech.atlassian.net/wiki/spaces/CAM/pages/3941662838/Remove+mandate+for+Unsubscribe+tag+in+email+flow
133
- export const isEmailUnsubscribeTagOptional = Auth.hasFeatureAccess.bind(
136
+ export const isEmailUnsubscribeTagMandatory = Auth.hasFeatureAccess.bind(
134
137
  null,
135
138
  EMAIL_UNSUBSCRIBE_TAG_MANDATORY,
136
139
  );
@@ -16,6 +16,7 @@ import {
16
16
  IOS,
17
17
  } from "../v2Containers/CreativesContainer/constants";
18
18
  import { GLOBAL_CONVERT_OPTIONS } from "../v2Components/FormBuilder/constants";
19
+ import { checkSupport, extractNames, skipTags as defaultSkipTags, isInsideLiquidBlock } from "./tagValidations";
19
20
  import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
20
21
  import { EMAIL_REGEX, PHONE_REGEX } from '../v2Components/CommonTestAndPreview/constants';
21
22
 
@@ -142,7 +143,13 @@ export const validateLiquidTemplateContent = async (
142
143
  messages,
143
144
  onError = () => {},
144
145
  onSuccess = () => {},
146
+ tagLookupMap,
147
+ eventContextTags,
148
+ waitEventContextTags = {},
149
+ isLiquidFlow,
150
+ forwardedTags = {},
145
151
  tabType,
152
+ skipTags = defaultSkipTags,
146
153
  } = options;
147
154
  const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
148
155
  const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
@@ -200,6 +207,82 @@ export const validateLiquidTemplateContent = async (
200
207
  });
201
208
  return false;
202
209
  }
210
+ // Extract and validate tags
211
+ const extractedLiquidTags = extractNames(result?.data || []);
212
+ // Get supported tags
213
+ const supportedLiquidTags = checkSupport(
214
+ result,
215
+ tagLookupMap,
216
+ eventContextTags,
217
+ isLiquidFlow,
218
+ forwardedTags,
219
+ waitEventContextTags
220
+ );
221
+ // Helper function to check if a tag appears only inside {% %} blocks
222
+ const isTagOnlyInsideLiquidBlocks = (tagName) => {
223
+ // Escape special regex characters in tag name, including dots
224
+ // Dots need to be escaped to match literally (item.name should match item.name, not item or name)
225
+ const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
226
+
227
+ // First, check if tag appears in {% %} syntax itself (like "order.items" in "{% for item in order.items %}")
228
+ // This is a tag used in Liquid logic, not output, so it should always be skipped
229
+ // Match the tag name as a whole (with dots escaped), optionally surrounded by word boundaries or non-word chars
230
+ // For tags with dots, we match them directly; for simple tags, we use word boundaries
231
+ const hasDot = tagName.includes('.');
232
+ const liquidSyntaxPattern = hasDot
233
+ ? `{%[^%]*${escapedTagName}[^%]*%}`
234
+ : `{%[^%]*\\b${escapedTagName}\\b[^%]*%}`;
235
+ const liquidSyntaxRegex = new RegExp(liquidSyntaxPattern, 'g');
236
+ const liquidSyntaxMatches = Array.from(content.matchAll(liquidSyntaxRegex));
237
+
238
+ // Find all occurrences of {{tagName}} in the content (output tags)
239
+ // Match patterns like: {{tagName}}, {{ tagName }}, {{tagName }}, {{ tagName}}
240
+ // Use non-word-boundary approach for tags with dots (item.name should match item.name, not item or name separately)
241
+ const outputTagRegex = new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g');
242
+ const outputTagMatches = Array.from(content.matchAll(outputTagRegex));
243
+ const outputTagPositions = outputTagMatches.map((match) => match.index);
244
+
245
+ // If tag appears in {% %} syntax, skip validation
246
+ if (liquidSyntaxMatches.length > 0) {
247
+ return true;
248
+ }
249
+
250
+ // If no output tag matches found, don't skip validation
251
+ // The tag was extracted by the API, so it exists somewhere and should be validated
252
+ if (outputTagPositions.length === 0) {
253
+ return false;
254
+ }
255
+
256
+ // Check if all output tag occurrences are inside {% %} blocks
257
+ // We check the position of {{ to see if it's inside a block
258
+ // Only skip validation if ALL occurrences are inside blocks
259
+ return outputTagPositions.every((tagIndex) => {
260
+ if (tagIndex === undefined || tagIndex === null) {
261
+ return false;
262
+ }
263
+ return isInsideLiquidBlock(content, tagIndex);
264
+ });
265
+ };
266
+
267
+ // Find unsupported tags, excluding those that are only inside {% %} blocks
268
+ const unsupportedLiquidTags = extractedLiquidTags?.filter(
269
+ (tag) => !supportedLiquidTags?.includes(tag)
270
+ && !skipTags(tag)
271
+ && !isTagOnlyInsideLiquidBlocks(tag)
272
+ );
273
+ // Handle unsupported tags
274
+ if (unsupportedLiquidTags?.length > 0) {
275
+ const errorMsg = formatMessage(messages.unsupportedTagsValidationError, {
276
+ unsupportedTags: unsupportedLiquidTags.join(", "),
277
+ });
278
+ onError({
279
+ standardErrors: [],
280
+ liquidErrors: [errorMsg],
281
+ tabType,
282
+ });
283
+ return false;
284
+ }
285
+ // All validations passed
203
286
  onSuccess(content, tabType);
204
287
  return true;
205
288
  };
@@ -217,8 +300,8 @@ export const _validatePlatformSpecificContent = async (
217
300
  ...commonVltcOptions // Other options like getLiquidTags, formatMessage, messages, currentTab, etc.
218
301
  } = options;
219
302
 
220
- let isAndroidValid = true;
221
- let isIosValid = true;
303
+ let isAndroidValid = false;
304
+ let isIosValid = false;
222
305
 
223
306
  // This aggregator is passed to validateLiquidTemplateContent.
224
307
  // It accumulates errors in the new structure and calls the parentOnError
@@ -286,8 +369,8 @@ export const _validatePlatformSpecificContent = async (
286
369
  /**
287
370
  * Validate Mobile Push content for both Android and iOS tabs
288
371
  * @param {object} formData - Form data containing Android and iOS content
289
- * @param {object} options - Options for validation (currentTab, onError, onSuccess, getLiquidTags, formatMessage, messages, singleTab).
290
- * @returns {Promise<boolean>} - Promise that resolves to true when validation succeeds, false otherwise
372
+ * @param {object} options - Options for validation
373
+ * @returns {Promise} - Promise that resolves when validation completes
291
374
  */
292
375
  export const validateMobilePushContent = async (formData, options) => {
293
376
  const {
@@ -299,46 +382,22 @@ export const validateMobilePushContent = async (formData, options) => {
299
382
  // Clear previous errors by calling the passed onError
300
383
  onError({ standardErrors: [], liquidErrors: [] });
301
384
 
302
- // For extractTags API: send plain template text (title + message), not the whole form as JSON.
303
- // OLD Mobile Push UI: Android = message-title, message-editor; iOS = message-title2, message-editor2.
304
- // New Mobile Push UI: both tabs use { title, message, buttons/ctas } — use extractContent().
305
- const rawAndroid = formData?.[0];
306
- const rawIos = formData?.[1];
307
- const isObject = (v) => v != null && typeof v === 'object' && !Array.isArray(v);
308
- const androidTab = isObject(rawAndroid) ? rawAndroid : {};
309
- const iosTab = isObject(rawIos) ? rawIos : {};
310
- const isOldUiShape = 'message-title' in androidTab || 'message-editor' in androidTab;
311
- let androidContentForValidation;
312
- let iosContentForValidation;
313
- if (isOldUiShape) {
314
- androidContentForValidation = [androidTab['message-title'], androidTab['message-editor']]
315
- .filter(Boolean)
316
- .join(' ');
317
- iosContentForValidation = [iosTab['message-title2'], iosTab['message-editor2']]
318
- .filter(Boolean)
319
- .join(' ');
320
- } else {
321
- androidContentForValidation = extractContent(androidTab);
322
- iosContentForValidation = extractContent(iosTab);
323
- }
385
+ const androidContent = JSON.stringify(formData?.[0]);
386
+ const iosContent = JSON.stringify(formData?.[1]);
324
387
 
325
388
  // Pass the original 'onError' from this function's arguments,
326
389
  // 'currentTab', and other relevant options to the helper.
327
390
  const overallSuccess = await _validatePlatformSpecificContent(
328
- androidContentForValidation,
329
- iosContentForValidation,
391
+ androidContent,
392
+ iosContent,
330
393
  MOBILE_PUSH,
331
394
  { ...restOptions, currentTab, onError }
332
395
  );
333
-
334
- // Submit payload remains full form data (JSON string) for save.
335
- const androidContentToSubmit = JSON.stringify(formData?.[0]);
336
- const iosContentToSubmit = JSON.stringify(formData?.[1]);
337
396
  const getContentToSubmit = () => {
338
- if (currentTab === 1 && androidContentToSubmit) return [androidContentToSubmit, ANDROID?.toLowerCase()];
339
- if (currentTab === 2 && iosContentToSubmit) return [iosContentToSubmit, IOS?.toLowerCase()];
340
- if (androidContentToSubmit) return [androidContentToSubmit, ANDROID?.toLowerCase()];
341
- if (iosContentToSubmit) return [iosContentToSubmit, IOS?.toLowerCase()];
397
+ if (currentTab === 1 && androidContent) return [androidContent, ANDROID?.toLowerCase()];
398
+ if (currentTab === 2 && iosContent) return [iosContent, IOS?.toLowerCase()];
399
+ if (androidContent) return [androidContent, ANDROID?.toLowerCase()];
400
+ if (iosContent) return [iosContent, IOS?.toLowerCase()];
342
401
  return ["", ""];
343
402
  };
344
403
  if (overallSuccess) {
@@ -8,124 +8,272 @@
8
8
 
9
9
  import lodashForEach from 'lodash/forEach';
10
10
  import lodashCloneDeep from 'lodash/cloneDeep';
11
- import { ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS, UNSUBSCRIBE_TAG, UNSUBSCRIBE_TAG_REGEX } from '../constants/unified';
11
+ import { ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS } from '../constants/unified';
12
12
 
13
13
  const DEFAULT = 'default';
14
14
  const SUBTAGS = 'subtags';
15
15
 
16
- export const hasUnsubscribeTag = (content) =>
17
- typeof content === 'string' && UNSUBSCRIBE_TAG_REGEX.test(content);
18
-
19
16
  /**
20
- * Shared core for tag validation. Used by validateTags (tagValidations) and FormBuilder.validateTags.
21
- * @param {Object} params
22
- * @param {string} params.contentForBraceCheck - String to run validateIfTagClosed on.
23
- * @param {string} params.contentForUnsubscribeScan - String to scan for {{...}} unsubscribe variants.
24
- * @param {Array} [params.tags] - Tag definitions (for definition-based missing tags).
25
- * @param {string} [params.currentModule] - Module context (e.g. 'default', 'outbound').
26
- * @param {boolean} [params.isFullMode] - If true, skip unsubscribe checks.
27
- * @param {Array} [params.initialMissingTags=null] - If set, use instead of computing from definitions.
28
- * @param {function} [params.skipTagsFn=skipTags] - skipTags implementation.
29
- * @param {boolean} [params.includeIsContentEmpty=false] - If true, response includes isContentEmpty: false.
30
- * @returns {{ valid: boolean, missingTags: string[], isBraceError: boolean }}
17
+ * Checks if the response object of Tags supports the Tags added in the Add Labels Section
18
+ * @param {Object} response - The response object to check.
19
+ * @param {Object} tagObject - The tagLookupMap.
31
20
  */
32
- export const validateTagsCore = ({
33
- contentForBraceCheck,
34
- contentForUnsubscribeScan,
35
- tags,
36
- currentModule,
37
- isFullMode,
38
- initialMissingTags = null,
39
- skipTagsFn = skipTags,
40
- includeIsContentEmpty = false,
41
- }) => {
42
- const response = {
43
- valid: true,
44
- missingTags: [],
45
- isBraceError: false,
46
- ...(includeIsContentEmpty && { isContentEmpty: false }),
21
+ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [], isLiquidFlow = false, forwardedTags = {}, waitEventContextTags = {}) => {
22
+ const supportedList = [];
23
+ // Verifies the presence of the tag in the 'Add Labels' section.
24
+ // Incase of journey event context the tags won't be available in the tagObject(tagLookupMap).
25
+ //Here forwardedTags only use in case of loyalty module
26
+ const mappedForwardedTags = handleForwardedTags(forwardedTags);
27
+ const allTags = [...eventContextTags, ...Object.values(waitEventContextTags).flatMap(block => block.tags)];
28
+ const checkNameInTagObjectOrEventContext = (name) => !!tagObject[name] || allTags?.some((tag) => tag?.tagName === name) || mappedForwardedTags.includes(name);
29
+
30
+ // Verify if childTag is a valid sub-tag of parentTag from the 'Add Labels' section or if it's unsupported.
31
+ const checkSubtags = (parentTag, childName) => {
32
+ // For event context tags the parentTag will be the event context tag name and subtags will be the child attributes for leaderboards
33
+ if (checkNameInTagObjectOrEventContext(parentTag) && isLiquidFlow && allTags?.length) {
34
+ const childNameAfterDot = childName?.split(".")?.[1];
35
+ if (allTags?.some((tag) => tag?.subTags?.includes(childNameAfterDot))) {
36
+ supportedList.push(childName);
37
+ }
38
+ }
39
+ if (!tagObject?.[parentTag] && !mappedForwardedTags.includes(parentTag)) return false;
40
+ let updatedChildName = childName;
41
+ let updatedWithoutDotChildName = childName;
42
+ if (childName?.includes(".")) {
43
+ updatedChildName = `.${childName?.split(".")?.[1]}`;
44
+ updatedWithoutDotChildName = childName?.split(".")?.[1];
45
+ }
46
+ if (tagObject?.[parentTag]) {
47
+ const subTags = tagObject?.[parentTag]?.definition?.subtags;
48
+ return subTags?.includes(updatedChildName);
49
+ }
50
+ return mappedForwardedTags.includes(updatedChildName) || mappedForwardedTags.includes(updatedWithoutDotChildName) || mappedForwardedTags.includes(childName);
47
51
  };
48
52
 
49
- if (tags && tags.length && !isFullMode) {
50
- if (initialMissingTags == null) {
51
- // Definition-based: same as original tagValidations (when caller does not pass initialMissingTags)
52
- lodashForEach(tags, ({
53
- definition: {
54
- supportedModules,
55
- value,
56
- },
57
- }) => {
58
- if (value === UNSUBSCRIBE_TAG) {
59
- lodashForEach(supportedModules, (module) => {
60
- if (module.mandatory && (currentModule === module.context)) {
61
- if (!hasUnsubscribeTag(contentForUnsubscribeScan)) {
62
- response.missingTags.push(value);
63
- }
64
- }
65
- });
66
- }
53
+ //Recursively checks if the childTag is actually a Sub-tag of the ParentTag
54
+ const processChildren = (parentTag = "", children = []) => {
55
+ for (const child of children) {
56
+ if (checkSubtags(parentTag, child?.name)) {
57
+ supportedList.push(child?.name);
58
+ }
59
+ if (child?.children?.length) {
60
+ processChildren(child?.name, child?.children);
61
+ }
62
+ }
63
+ };
64
+
65
+ //Checks if the tag is present in the Add Label Section
66
+ for (const item of response?.data || []) {
67
+ if (checkNameInTagObjectOrEventContext(item?.name)) {
68
+ supportedList?.push(item?.name);
69
+ }
70
+ //Repeat the process for subtags
71
+ if (item?.children?.length) {
72
+ processChildren(item?.name, item?.children);
73
+ }
74
+ }
75
+
76
+
77
+ return supportedList;
78
+ };
79
+
80
+ const handleForwardedTags = (forwardedTags) => {
81
+ const result = [];
82
+ Object.keys(forwardedTags).forEach((key) => {
83
+ result.push(key); // Add the main key to the result array
84
+
85
+ // Check if there are subtags for the current key
86
+ if (forwardedTags[key].subtags) {
87
+ // If subtags exist, add all subtag keys to the result array
88
+ Object.keys(forwardedTags[key].subtags).forEach((subkey) => {
89
+ result.push(subkey);
67
90
  });
68
- } else {
69
- response.missingTags = [...initialMissingTags];
70
91
  }
92
+ });
93
+ return result;
94
+ };
71
95
 
72
- const regex = /{{([^}]+)}}/g;
73
- let match = regex.exec(contentForUnsubscribeScan);
74
- while (match !== null) {
75
- const tagValue = match[1].trim();
76
- const ifSkipped = skipTagsFn(tagValue);
77
- if (ifSkipped && tagValue.indexOf(UNSUBSCRIBE_TAG) !== -1) {
78
- const missingTagIndex = response.missingTags.indexOf(UNSUBSCRIBE_TAG);
79
- if (missingTagIndex !== -1) {
80
- response.missingTags.splice(missingTagIndex, 1);
81
- }
82
- }
83
- match = regex.exec(contentForUnsubscribeScan);
96
+ /**
97
+ * Extracts the names from the given data.
98
+ * @param {Array} data - The data to extract names from.
99
+ * @returns {Array} - The extracted names.
100
+ */
101
+ export function extractNames(data) {
102
+ const names = [];
103
+
104
+ function traverse(node) {
105
+ if (node?.name) {
106
+ names.push(node?.name);
84
107
  }
85
108
 
86
- if (response.missingTags.length > 0) {
87
- response.valid = false;
109
+ if (node?.children?.length > 0) {
110
+ node?.children?.forEach((child) => traverse(child));
88
111
  }
89
112
  }
90
113
 
91
- response.isBraceError = !validateIfTagClosed(contentForBraceCheck);
92
- if (response.isBraceError) {
93
- response.valid = false;
94
- } else if (response.missingTags.length > 0) {
95
- response.valid = false;
114
+ data?.forEach((item) => traverse(item));
115
+
116
+ return names;
117
+ }
118
+
119
+ // Helper to check if a tag is inside a {% ... %} block
120
+ // Handles all Liquid block types: {% %}, {%- -%}, {%- %}, {% -%}
121
+ // Content inside {% %} blocks can contain any dynamic tags and should not be validated
122
+ export const isInsideLiquidBlock = (content, tagIndex) => {
123
+ if (!content || tagIndex < 0 || tagIndex >= content.length) {
124
+ return false;
125
+ }
126
+
127
+ // Check if tagIndex is at the start of a {% block
128
+ // Need to check a few characters ahead to catch {% patterns
129
+ const checkWindow = content.substring(Math.max(0, tagIndex - 1), Math.min(content.length, tagIndex + 3));
130
+ if (/{%-?/.test(checkWindow) && content.substring(tagIndex, tagIndex + 2) === '{%') {
131
+ return true;
96
132
  }
97
- return response;
133
+
134
+ // Check content up to tagIndex to see if we're inside any {% %} block
135
+ // We need to properly handle Liquid tags:
136
+ // - Block opening tags: {% for %}, {% if %}, {% unless %}, {% case %}, {% capture %}, etc.
137
+ // These OPEN a block that needs to be closed with an "end" tag
138
+ // - Block closing tags: {% endfor %}, {% endif %}, {% endunless %}, etc.
139
+ // These CLOSE a block opened by a corresponding opening tag
140
+ // - Self-contained tags: {% assign %}, {% comment %}, etc.
141
+ // These are blocks themselves (from {% to %})
142
+ const contentBeforeTag = content.substring(0, tagIndex);
143
+
144
+ // Regex to match Liquid block opening tags (for, if, unless, case, capture, tablerow, raw, comment)
145
+ // These tags OPEN a block that needs to be closed with an "end" tag
146
+ const blockOpeningTags = ['for', 'if', 'unless', 'case', 'capture', 'tablerow', 'raw', 'comment'];
147
+ const blockOpeningPattern = blockOpeningTags.map(tag => `\\b${tag}\\b`).join('|');
148
+ const blockOpeningRegex = new RegExp(`{%-?\\s*(${blockOpeningPattern})[^%]*%}`, 'gi');
149
+
150
+ // Regex to match Liquid block closing tags (endfor, endif, endunless, endcase, endcapture, etc.)
151
+ const blockClosingTags = ['endfor', 'endif', 'endunless', 'endcase', 'endcapture', 'endtablerow', 'endraw', 'endcomment'];
152
+ const blockClosingPattern = blockClosingTags.map(tag => `\\b${tag}\\b`).join('|');
153
+ const blockClosingRegex = new RegExp(`{%-?\\s*(${blockClosingPattern})[^%]*%}`, 'gi');
154
+
155
+ // Find all block opening tags before tagIndex
156
+ const blockOpenings = Array.from(contentBeforeTag.matchAll(blockOpeningRegex), (match) => match.index);
157
+
158
+ // Find all block closing tags before tagIndex
159
+ const blockClosings = Array.from(contentBeforeTag.matchAll(blockClosingRegex), (match) => match.index);
160
+
161
+ // Count unmatched opening blocks (for, if, etc.)
162
+ const openBlockCount = blockOpenings.length - blockClosings.length;
163
+
164
+ // Also check for self-contained tags (assign, etc.) that create blocks from {% to %}
165
+ // These are any {% %} tags that are not block-opening or block-closing tags
166
+ const allBlockStarts = /{%-?/g;
167
+ const allBlockEnds = /-?%}/g;
168
+ const allStarts = Array.from(contentBeforeTag.matchAll(allBlockStarts), (match) => match.index);
169
+ const allEnds = Array.from(contentBeforeTag.matchAll(allBlockEnds), (match) => match.index);
170
+
171
+ // Check if we're inside a self-contained tag (more {% than %} before tagIndex, excluding block tags)
172
+ const selfContainedCount = allStarts.length - allEnds.length;
173
+
174
+ // We're inside a block if:
175
+ // 1. We're inside an unclosed block-opening tag (for, if, etc.), OR
176
+ // 2. We're inside a self-contained tag (assign, etc.)
177
+ return openBlockCount > 0 || selfContainedCount > 0;
98
178
  };
99
179
 
100
180
  /**
101
181
  * Validates the tags based on the provided parameters.
102
182
  * @param {Object} params - The parameters for tag validation.
103
- * @param {string} params.content - Content to validate.
104
- * @param {Array} [params.tagsParam] - Tag definitions.
105
- * @param {Object} [params.location] - Location with query.module.
106
- * @param {string} [params.tagModule] - Override for current module context.
107
- * @param {boolean} [params.isFullMode] - If true, skip unsubscribe checks.
108
- * @returns {{ valid: boolean, missingTags: string[], isBraceError: boolean }}
109
183
  */
110
184
  export const validateTags = ({
111
185
  content,
112
186
  tagsParam,
187
+ injectedTagsParams,
113
188
  location,
114
189
  tagModule,
190
+ eventContextTags,
191
+ waitEventContextTags = {},
115
192
  isFullMode,
116
193
  }) => {
117
194
  const tags = tagsParam;
195
+ const injectedTags = transformInjectedTags(injectedTagsParams);
118
196
  let currentModule = location?.query?.module ? location?.query?.module : DEFAULT;
119
197
  if (tagModule) {
120
198
  currentModule = tagModule;
121
199
  }
122
- return validateTagsCore({
123
- contentForBraceCheck: content,
124
- contentForUnsubscribeScan: content,
125
- tags,
126
- currentModule,
127
- isFullMode,
128
- });
200
+ const response = {
201
+ valid: true,
202
+ missingTags: [],
203
+ unsupportedTags: [],
204
+ isBraceError: false,
205
+ };
206
+ // Mandatory-tags check: only when we have a tags list and are in library mode
207
+ if (tags && tags.length && !isFullMode) {
208
+ lodashForEach(tags, ({
209
+ definition: {
210
+ supportedModules,
211
+ value,
212
+ },
213
+ }) => {
214
+ lodashForEach(supportedModules, (module) => {
215
+ if (module.mandatory && (currentModule === module.context)) {
216
+ if (content.indexOf(`{{${value}}}`) === -1) {
217
+ response.valid = false;
218
+ response.missingTags.push(value);
219
+ }
220
+ }
221
+ });
222
+ });
223
+ }
224
+ // In library mode, always scan content for {{...}} and flag unsupported tags (even when tags list is empty)
225
+ if (!isFullMode && content) {
226
+ const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
227
+ let match = regex.exec(content);
228
+ while (match !== null) {
229
+ const tagValue = match[0].substring(indexOfEnd(match[0], '{{'), match[0].indexOf('}}'));
230
+ const tagIndex = match?.index;
231
+ match = regex.exec(content);
232
+ let ifSupported = false;
233
+ lodashForEach(tags || [], (tag) => {
234
+ if (tag?.definition?.value === tagValue) {
235
+ ifSupported = true;
236
+ }
237
+ });
238
+ const ifSkipped = skipTags(tagValue);
239
+ if (ifSkipped) {
240
+ ifSupported = true;
241
+ const isUnsubscribeSkipped = tagValue.indexOf("unsubscribe") !== -1;
242
+ if (isUnsubscribeSkipped) {
243
+ const missingTagIndex = response.missingTags.indexOf("unsubscribe");
244
+ if (missingTagIndex !== -1) {
245
+ response.missingTags.splice(missingTagIndex, 1);
246
+ }
247
+ }
248
+ }
249
+ // Journey Event Context Tags support
250
+ eventContextTags?.forEach((tag) => {
251
+ if (tagValue === tag?.tagName) {
252
+ ifSupported = true;
253
+ }
254
+ });
255
+ // Wait Event Context Tags support
256
+ Object.values(waitEventContextTags).flatMap(block => block.tags)?.forEach((tag) => {
257
+ if (tagValue === tag?.tagName) {
258
+ ifSupported = true;
259
+ }
260
+ });
261
+ ifSupported = ifSupported || checkIfSupportedTag(tagValue, injectedTags);
262
+ // Only add to unsupportedTags if not inside a {% ... %} block and does not contain a dot
263
+ if (!ifSupported && !isInsideLiquidBlock(content, tagIndex) && tagValue?.indexOf('.') === -1) {
264
+ response.unsupportedTags.push(tagValue);
265
+ response.valid = false;
266
+ }
267
+ if (response.unsupportedTags.length === 0 && response.missingTags.length === 0) {
268
+ response.valid = true;
269
+ }
270
+ }
271
+ }
272
+ response.isBraceError = !validateIfTagClosed(content);
273
+ // response should not be valid if there is unbalanced bracket error, as
274
+ // validations (eg button) are handled on valid property coming from the response.
275
+ response.isBraceError ? response.valid = false : response.valid = true;
276
+ return response;
129
277
  };
130
278
 
131
279
  /**