@capillarytech/creatives-library 8.0.304 → 8.0.305-alpha.0

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