@capillarytech/creatives-library 8.0.307-alpha.0 → 8.0.308
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +1 -3
- package/initialState.js +2 -0
- package/package.json +1 -1
- package/utils/common.js +8 -5
- package/utils/commonUtils.js +93 -36
- package/utils/tagValidations.js +223 -83
- package/utils/tests/commonUtil.test.js +124 -147
- package/utils/tests/tagValidations.test.js +358 -441
- package/v2Components/ErrorInfoNote/index.js +5 -2
- package/v2Components/FormBuilder/index.js +203 -137
- package/v2Components/FormBuilder/messages.js +8 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +5 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +2 -1
- package/v2Containers/Cap/mockData.js +14 -0
- package/v2Containers/Cap/reducer.js +55 -3
- package/v2Containers/Cap/tests/reducer.test.js +102 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -5
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
- package/v2Containers/CreativesContainer/constants.js +0 -6
- package/v2Containers/CreativesContainer/index.js +7 -47
- package/v2Containers/Email/index.js +5 -1
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +120 -20
- package/v2Containers/FTP/index.js +51 -2
- package/v2Containers/FTP/messages.js +4 -0
- package/v2Containers/InApp/index.js +122 -35
- package/v2Containers/InApp/tests/index.test.js +6 -17
- package/v2Containers/InappAdvance/index.js +112 -4
- package/v2Containers/InappAdvance/tests/index.test.js +0 -2
- package/v2Containers/Line/Container/Text/index.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +19 -59
- package/v2Containers/MobilePush/Edit/index.js +20 -48
- package/v2Containers/MobilePushNew/index.js +32 -12
- package/v2Containers/MobilepushWrapper/index.js +1 -3
- package/v2Containers/Rcs/index.js +37 -12
- package/v2Containers/Sms/Create/index.js +3 -39
- package/v2Containers/Sms/Create/messages.js +0 -4
- package/v2Containers/Sms/Edit/index.js +3 -35
- package/v2Containers/Sms/commonMethods.js +6 -3
- package/v2Containers/SmsTrai/Edit/index.js +47 -11
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/SmsWrapper/index.js +0 -2
- package/v2Containers/TemplatesV2/index.js +13 -28
- package/v2Containers/Viber/index.js +1 -0
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
- package/v2Containers/WebPush/Create/index.js +2 -2
- package/v2Containers/WebPush/Create/utils/validation.js +8 -17
- package/v2Containers/WebPush/Create/utils/validation.test.js +24 -44
- package/v2Containers/Whatsapp/index.js +17 -9
- package/v2Containers/Zalo/index.js +11 -3
- package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
package/constants/unified.js
CHANGED
|
@@ -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
package/package.json
CHANGED
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
|
-
|
|
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
|
);
|
package/utils/commonUtils.js
CHANGED
|
@@ -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,12 @@ export const validateLiquidTemplateContent = async (
|
|
|
142
143
|
messages,
|
|
143
144
|
onError = () => {},
|
|
144
145
|
onSuccess = () => {},
|
|
146
|
+
tagLookupMap,
|
|
147
|
+
eventContextTags,
|
|
148
|
+
isLiquidFlow,
|
|
149
|
+
forwardedTags = {},
|
|
145
150
|
tabType,
|
|
151
|
+
skipTags = defaultSkipTags,
|
|
146
152
|
} = options;
|
|
147
153
|
const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
|
|
148
154
|
const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
|
|
@@ -200,6 +206,81 @@ export const validateLiquidTemplateContent = async (
|
|
|
200
206
|
});
|
|
201
207
|
return false;
|
|
202
208
|
}
|
|
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
|
|
203
284
|
onSuccess(content, tabType);
|
|
204
285
|
return true;
|
|
205
286
|
};
|
|
@@ -217,8 +298,8 @@ export const _validatePlatformSpecificContent = async (
|
|
|
217
298
|
...commonVltcOptions // Other options like getLiquidTags, formatMessage, messages, currentTab, etc.
|
|
218
299
|
} = options;
|
|
219
300
|
|
|
220
|
-
let isAndroidValid =
|
|
221
|
-
let isIosValid =
|
|
301
|
+
let isAndroidValid = false;
|
|
302
|
+
let isIosValid = false;
|
|
222
303
|
|
|
223
304
|
// This aggregator is passed to validateLiquidTemplateContent.
|
|
224
305
|
// It accumulates errors in the new structure and calls the parentOnError
|
|
@@ -286,8 +367,8 @@ export const _validatePlatformSpecificContent = async (
|
|
|
286
367
|
/**
|
|
287
368
|
* Validate Mobile Push content for both Android and iOS tabs
|
|
288
369
|
* @param {object} formData - Form data containing Android and iOS content
|
|
289
|
-
* @param {object} options - Options for validation
|
|
290
|
-
* @returns {Promise
|
|
370
|
+
* @param {object} options - Options for validation
|
|
371
|
+
* @returns {Promise} - Promise that resolves when validation completes
|
|
291
372
|
*/
|
|
292
373
|
export const validateMobilePushContent = async (formData, options) => {
|
|
293
374
|
const {
|
|
@@ -299,46 +380,22 @@ export const validateMobilePushContent = async (formData, options) => {
|
|
|
299
380
|
// Clear previous errors by calling the passed onError
|
|
300
381
|
onError({ standardErrors: [], liquidErrors: [] });
|
|
301
382
|
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
}
|
|
383
|
+
const androidContent = JSON.stringify(formData?.[0]);
|
|
384
|
+
const iosContent = JSON.stringify(formData?.[1]);
|
|
324
385
|
|
|
325
386
|
// Pass the original 'onError' from this function's arguments,
|
|
326
387
|
// 'currentTab', and other relevant options to the helper.
|
|
327
388
|
const overallSuccess = await _validatePlatformSpecificContent(
|
|
328
|
-
|
|
329
|
-
|
|
389
|
+
androidContent,
|
|
390
|
+
iosContent,
|
|
330
391
|
MOBILE_PUSH,
|
|
331
392
|
{ ...restOptions, currentTab, onError }
|
|
332
393
|
);
|
|
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
394
|
const getContentToSubmit = () => {
|
|
338
|
-
if (currentTab === 1 &&
|
|
339
|
-
if (currentTab === 2 &&
|
|
340
|
-
if (
|
|
341
|
-
if (
|
|
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()];
|
|
342
399
|
return ["", ""];
|
|
343
400
|
};
|
|
344
401
|
if (overallSuccess) {
|
package/utils/tagValidations.js
CHANGED
|
@@ -8,124 +8,264 @@
|
|
|
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
|
|
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
|
-
*
|
|
21
|
-
* @param {Object}
|
|
22
|
-
* @param {
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
tags
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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);
|
|
47
50
|
};
|
|
48
51
|
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
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);
|
|
67
89
|
});
|
|
68
|
-
} else {
|
|
69
|
-
response.missingTags = [...initialMissingTags];
|
|
70
90
|
}
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
71
94
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
match = regex.exec(contentForUnsubscribeScan);
|
|
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
|
+
|
|
103
|
+
function traverse(node) {
|
|
104
|
+
if (node?.name) {
|
|
105
|
+
names.push(node?.name);
|
|
84
106
|
}
|
|
85
107
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
108
|
+
if (node?.children?.length > 0) {
|
|
109
|
+
node?.children?.forEach((child) => traverse(child));
|
|
88
110
|
}
|
|
89
111
|
}
|
|
90
112
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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;
|
|
96
131
|
}
|
|
97
|
-
|
|
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;
|
|
98
177
|
};
|
|
99
178
|
|
|
100
179
|
/**
|
|
101
180
|
* Validates the tags based on the provided parameters.
|
|
102
181
|
* @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
182
|
*/
|
|
110
183
|
export const validateTags = ({
|
|
111
184
|
content,
|
|
112
185
|
tagsParam,
|
|
186
|
+
injectedTagsParams,
|
|
113
187
|
location,
|
|
114
188
|
tagModule,
|
|
189
|
+
eventContextTags,
|
|
115
190
|
isFullMode,
|
|
116
191
|
}) => {
|
|
117
192
|
const tags = tagsParam;
|
|
193
|
+
const injectedTags = transformInjectedTags(injectedTagsParams);
|
|
118
194
|
let currentModule = location?.query?.module ? location?.query?.module : DEFAULT;
|
|
119
195
|
if (tagModule) {
|
|
120
196
|
currentModule = tagModule;
|
|
121
197
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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;
|
|
129
269
|
};
|
|
130
270
|
|
|
131
271
|
/**
|