@capillarytech/creatives-library 8.0.231 → 8.0.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/utils/commonUtils.js +51 -3
- package/utils/tagValidations.js +112 -77
- package/utils/tests/commonUtil.test.js +104 -0
- package/v2Components/FormBuilder/index.js +5 -2
package/package.json
CHANGED
package/utils/commonUtils.js
CHANGED
|
@@ -16,7 +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 } from "./tagValidations";
|
|
19
|
+
import { checkSupport, extractNames, skipTags as defaultSkipTags, isInsideLiquidBlock } from "./tagValidations";
|
|
20
20
|
import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
|
|
21
21
|
export const apiMessageFormatHandler = (id, fallback) => (
|
|
22
22
|
<FormattedMessage id={id} defaultMessage={fallback} />
|
|
@@ -204,9 +204,57 @@ export const validateLiquidTemplateContent = async (
|
|
|
204
204
|
isLiquidFlow,
|
|
205
205
|
forwardedTags
|
|
206
206
|
);
|
|
207
|
-
//
|
|
207
|
+
// Helper function to check if a tag appears only inside {% %} blocks
|
|
208
|
+
const isTagOnlyInsideLiquidBlocks = (tagName) => {
|
|
209
|
+
// Escape special regex characters in tag name, including dots
|
|
210
|
+
// Dots need to be escaped to match literally (item.name should match item.name, not item or name)
|
|
211
|
+
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
212
|
+
|
|
213
|
+
// First, check if tag appears in {% %} syntax itself (like "order.items" in "{% for item in order.items %}")
|
|
214
|
+
// This is a tag used in Liquid logic, not output, so it should always be skipped
|
|
215
|
+
// Match the tag name as a whole (with dots escaped), optionally surrounded by word boundaries or non-word chars
|
|
216
|
+
// For tags with dots, we match them directly; for simple tags, we use word boundaries
|
|
217
|
+
const hasDot = tagName.includes('.');
|
|
218
|
+
const liquidSyntaxPattern = hasDot
|
|
219
|
+
? `{%[^%]*${escapedTagName}[^%]*%}`
|
|
220
|
+
: `{%[^%]*\\b${escapedTagName}\\b[^%]*%}`;
|
|
221
|
+
const liquidSyntaxRegex = new RegExp(liquidSyntaxPattern, 'g');
|
|
222
|
+
const liquidSyntaxMatches = Array.from(content.matchAll(liquidSyntaxRegex));
|
|
223
|
+
|
|
224
|
+
// Find all occurrences of {{tagName}} in the content (output tags)
|
|
225
|
+
// Match patterns like: {{tagName}}, {{ tagName }}, {{tagName }}, {{ tagName}}
|
|
226
|
+
// Use non-word-boundary approach for tags with dots (item.name should match item.name, not item or name separately)
|
|
227
|
+
const outputTagRegex = new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g');
|
|
228
|
+
const outputTagMatches = Array.from(content.matchAll(outputTagRegex));
|
|
229
|
+
const outputTagPositions = outputTagMatches.map((match) => match.index);
|
|
230
|
+
|
|
231
|
+
// If tag appears in {% %} syntax, skip validation
|
|
232
|
+
if (liquidSyntaxMatches.length > 0) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// If no output tag matches found, don't skip validation
|
|
237
|
+
// The tag was extracted by the API, so it exists somewhere and should be validated
|
|
238
|
+
if (outputTagPositions.length === 0) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if all output tag occurrences are inside {% %} blocks
|
|
243
|
+
// We check the position of {{ to see if it's inside a block
|
|
244
|
+
// Only skip validation if ALL occurrences are inside blocks
|
|
245
|
+
return outputTagPositions.every((tagIndex) => {
|
|
246
|
+
if (tagIndex === undefined || tagIndex === null) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return isInsideLiquidBlock(content, tagIndex);
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Find unsupported tags, excluding those that are only inside {% %} blocks
|
|
208
254
|
const unsupportedLiquidTags = extractedLiquidTags?.filter(
|
|
209
|
-
(tag) => !supportedLiquidTags?.includes(tag)
|
|
255
|
+
(tag) => !supportedLiquidTags?.includes(tag)
|
|
256
|
+
&& !skipTags(tag)
|
|
257
|
+
&& !isTagOnlyInsideLiquidBlocks(tag)
|
|
210
258
|
);
|
|
211
259
|
// Handle unsupported tags
|
|
212
260
|
if (unsupportedLiquidTags?.length > 0) {
|
package/utils/tagValidations.js
CHANGED
|
@@ -19,7 +19,6 @@ const SUBTAGS = 'subtags';
|
|
|
19
19
|
* @param {Object} tagObject - The tagLookupMap.
|
|
20
20
|
*/
|
|
21
21
|
export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [], isLiquidFlow = false, forwardedTags = {}) => {
|
|
22
|
-
|
|
23
22
|
const supportedList = [];
|
|
24
23
|
// Verifies the presence of the tag in the 'Add Labels' section.
|
|
25
24
|
// Incase of journey event context the tags won't be available in the tagObject(tagLookupMap).
|
|
@@ -40,7 +39,7 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
40
39
|
let updatedChildName = childName;
|
|
41
40
|
let updatedWithoutDotChildName = childName;
|
|
42
41
|
if (childName?.includes(".")) {
|
|
43
|
-
updatedChildName =
|
|
42
|
+
updatedChildName = `.${childName?.split(".")?.[1]}`;
|
|
44
43
|
updatedWithoutDotChildName = childName?.split(".")?.[1];
|
|
45
44
|
}
|
|
46
45
|
if (tagObject?.[parentTag]) {
|
|
@@ -71,7 +70,6 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
71
70
|
if (item?.children?.length) {
|
|
72
71
|
processChildren(item?.name, item?.children);
|
|
73
72
|
}
|
|
74
|
-
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
|
|
@@ -80,19 +78,19 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
80
78
|
|
|
81
79
|
const handleForwardedTags = (forwardedTags) => {
|
|
82
80
|
const result = [];
|
|
83
|
-
Object.keys(forwardedTags).forEach(key => {
|
|
81
|
+
Object.keys(forwardedTags).forEach((key) => {
|
|
84
82
|
result.push(key); // Add the main key to the result array
|
|
85
|
-
|
|
83
|
+
|
|
86
84
|
// Check if there are subtags for the current key
|
|
87
85
|
if (forwardedTags[key].subtags) {
|
|
88
86
|
// If subtags exist, add all subtag keys to the result array
|
|
89
|
-
Object.keys(forwardedTags[key].subtags).forEach(subkey => {
|
|
90
|
-
|
|
87
|
+
Object.keys(forwardedTags[key].subtags).forEach((subkey) => {
|
|
88
|
+
result.push(subkey);
|
|
91
89
|
});
|
|
92
90
|
}
|
|
93
91
|
});
|
|
94
92
|
return result;
|
|
95
|
-
}
|
|
93
|
+
};
|
|
96
94
|
|
|
97
95
|
/**
|
|
98
96
|
* Extracts the names from the given data.
|
|
@@ -118,17 +116,64 @@ export function extractNames(data) {
|
|
|
118
116
|
}
|
|
119
117
|
|
|
120
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
121
|
export const isInsideLiquidBlock = (content, tagIndex) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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;
|
|
130
131
|
}
|
|
131
|
-
|
|
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;
|
|
132
177
|
};
|
|
133
178
|
|
|
134
179
|
/**
|
|
@@ -155,7 +200,7 @@ export const validateTags = ({
|
|
|
155
200
|
unsupportedTags: [],
|
|
156
201
|
isBraceError: false,
|
|
157
202
|
};
|
|
158
|
-
if(tags && tags.length) {
|
|
203
|
+
if (tags && tags.length) {
|
|
159
204
|
lodashForEach(tags, ({
|
|
160
205
|
definition: {
|
|
161
206
|
supportedModules,
|
|
@@ -216,7 +261,7 @@ export const validateTags = ({
|
|
|
216
261
|
// validations (eg button) are handled on valid property coming from the response.
|
|
217
262
|
response.isBraceError ? response.valid = false : response.valid = true;
|
|
218
263
|
return response;
|
|
219
|
-
}
|
|
264
|
+
};
|
|
220
265
|
|
|
221
266
|
/**
|
|
222
267
|
* Checks if the given tag is supported based on the injected tags.
|
|
@@ -233,14 +278,14 @@ export const checkIfSupportedTag = (checkingTag, injectedTags) => {
|
|
|
233
278
|
result = true;
|
|
234
279
|
}
|
|
235
280
|
});
|
|
236
|
-
|
|
281
|
+
|
|
237
282
|
return result;
|
|
238
|
-
}
|
|
283
|
+
};
|
|
239
284
|
|
|
240
285
|
const indexOfEnd = (targetString, string) => {
|
|
241
|
-
|
|
286
|
+
const io = targetString.indexOf(string);
|
|
242
287
|
return io == -1 ? -1 : io + string.length;
|
|
243
|
-
}
|
|
288
|
+
};
|
|
244
289
|
|
|
245
290
|
export const skipTags = (tag) => {
|
|
246
291
|
// If the tag contains the word "entryTrigger.", then it's an event context tag and should not be skipped.
|
|
@@ -268,35 +313,33 @@ export const transformInjectedTags = (tags) => {
|
|
|
268
313
|
if (subKey !== '') {
|
|
269
314
|
temp['tag-header'] = true;
|
|
270
315
|
if (subKey !== SUBTAGS) {
|
|
271
|
-
temp.subtags =lodashCloneDeep(temp[subKey]);
|
|
316
|
+
temp.subtags = lodashCloneDeep(temp[subKey]);
|
|
272
317
|
delete temp[subKey];
|
|
273
318
|
}
|
|
274
319
|
temp.subtags = transformInjectedTags(temp.subtags);
|
|
275
320
|
}
|
|
276
321
|
});
|
|
277
322
|
return tags;
|
|
278
|
-
}
|
|
323
|
+
};
|
|
279
324
|
|
|
280
325
|
//checks if the opening curly brackets have corresponding closing brackets
|
|
281
326
|
export const validateIfTagClosed = (value) => {
|
|
282
327
|
if (value.includes("{{{{") || value.includes("}}}}")) {
|
|
283
328
|
return false;
|
|
284
329
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
330
|
+
const regex1 = /{{.*?}}/g;
|
|
331
|
+
const regex2 = /{{/g;
|
|
332
|
+
const regex3 = /}}/g;
|
|
288
333
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
334
|
+
const l1 = value.match(regex1)?.length;
|
|
335
|
+
const l2 = value.match(regex2)?.length;
|
|
336
|
+
const l3 = value.match(regex3)?.length;
|
|
292
337
|
|
|
293
338
|
return (l1 == l2 && l2 == l3 && l1 == l3);
|
|
294
|
-
|
|
295
339
|
};
|
|
296
340
|
|
|
297
341
|
//replaces encoded string with their respective characters
|
|
298
342
|
export const preprocessHtml = (content) => {
|
|
299
|
-
|
|
300
343
|
const replacements = {
|
|
301
344
|
"'": "'",
|
|
302
345
|
""": "'",
|
|
@@ -304,7 +347,7 @@ export const preprocessHtml = (content) => {
|
|
|
304
347
|
"&": "&",
|
|
305
348
|
"<": "<",
|
|
306
349
|
">": ">",
|
|
307
|
-
"\n": "",
|
|
350
|
+
"\n": "", // Handling newlines by replacing them with an empty string
|
|
308
351
|
};
|
|
309
352
|
|
|
310
353
|
|
|
@@ -318,28 +361,22 @@ export const preprocessHtml = (content) => {
|
|
|
318
361
|
});
|
|
319
362
|
|
|
320
363
|
// Step 2: Perform the standard replacements on the entire content
|
|
321
|
-
return contentWithStyleFixes?.replace(/'|"|&|<|>|"|\n/g, match => replacements[match]);
|
|
364
|
+
return contentWithStyleFixes?.replace(/'|"|&|<|>|"|\n/g, (match) => replacements[match]);
|
|
322
365
|
};
|
|
323
366
|
|
|
324
367
|
//this is used to get the subtags from custom or extended tags
|
|
325
|
-
export const getTagMapValue = (object = {}) =>
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return { ...acc, ...(current.subtags ?? {}) };
|
|
338
|
-
}
|
|
339
|
-
// If no subtags → keep the tag itself
|
|
340
|
-
return { ...acc, [key]: current };
|
|
341
|
-
}, {});
|
|
342
|
-
};
|
|
368
|
+
export const getTagMapValue = (object = {}) => Object.values(
|
|
369
|
+
object
|
|
370
|
+
).reduce((acc, current) => ({ ...acc, ...current?.subtags ?? {} }), {});
|
|
371
|
+
|
|
372
|
+
export const getLoyaltyTagsMapValue = (object = {}) => Object.entries(object).reduce((acc, [key, current]) => {
|
|
373
|
+
if (current?.subtags && Object.keys(current.subtags).length > 0) {
|
|
374
|
+
// If subtags exist → merge them
|
|
375
|
+
return { ...acc, ...(current.subtags ?? {}) };
|
|
376
|
+
}
|
|
377
|
+
// If no subtags → keep the tag itself
|
|
378
|
+
return { ...acc, [key]: current };
|
|
379
|
+
}, {});
|
|
343
380
|
|
|
344
381
|
|
|
345
382
|
/**
|
|
@@ -348,27 +385,25 @@ export const getLoyaltyTagsMapValue = (object = {}) => {
|
|
|
348
385
|
* @param {Object} object - The input object containing top-level keys with optional subtags.
|
|
349
386
|
* @returns {Object} - A flat map containing all top-level keys and their subtags.
|
|
350
387
|
*/
|
|
351
|
-
export const getForwardedMapValues = (object = {}) => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}, {});
|
|
374
|
-
};
|
|
388
|
+
export const getForwardedMapValues = (object = {}) => Object?.entries(object)?.reduce((acc, [key, current]) => {
|
|
389
|
+
// Check if current has 'subtags' and it's an object
|
|
390
|
+
if (current && current?.subtags && typeof current?.subtags === 'object') {
|
|
391
|
+
// Add the top-level key with its 'name' and 'desc'
|
|
392
|
+
acc[key] = {
|
|
393
|
+
name: current?.name,
|
|
394
|
+
desc: current?.desc,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// Merge the subtags into the accumulator
|
|
398
|
+
acc = { ...acc, ...current?.subtags };
|
|
399
|
+
} else if (current && typeof current === 'object') {
|
|
400
|
+
// If no 'subtags', add the top-level key with its 'name' and 'desc'
|
|
401
|
+
acc[key] = {
|
|
402
|
+
name: current?.name,
|
|
403
|
+
desc: current?.desc,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// If the current entry is not an object or lacks 'name'/'desc', skip it
|
|
408
|
+
return acc;
|
|
409
|
+
}, {});
|
|
@@ -297,6 +297,110 @@ describe("validateLiquidTemplateContent", () => {
|
|
|
297
297
|
expect(onSuccess).not.toHaveBeenCalled();
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
it("should skip tags that appear in {% %} syntax (like order.items in for loop)", async () => {
|
|
301
|
+
const content = '{% for item in order.items %} Hello {% endfor %}';
|
|
302
|
+
const getLiquidTags = jest.fn((content, cb) =>
|
|
303
|
+
cb({ askAiraResponse: { errors: [], data: [{ name: "order.items" }] }, isError: false })
|
|
304
|
+
);
|
|
305
|
+
await validateLiquidTemplateContent(content, {
|
|
306
|
+
getLiquidTags,
|
|
307
|
+
formatMessage,
|
|
308
|
+
messages,
|
|
309
|
+
onError,
|
|
310
|
+
onSuccess,
|
|
311
|
+
tagLookupMap,
|
|
312
|
+
eventContextTags
|
|
313
|
+
});
|
|
314
|
+
// order.items appears in {% %} syntax, so it should be skipped and validation should pass
|
|
315
|
+
expect(onSuccess).toHaveBeenCalledWith(content, undefined);
|
|
316
|
+
expect(onError).not.toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("should skip tags that appear only inside {% %} blocks (like item.name in for loop)", async () => {
|
|
320
|
+
const content = '{% for item in order.items %} {{ item.name }} - {{ item.quantity }} {% endfor %}';
|
|
321
|
+
const getLiquidTags = jest.fn((content, cb) =>
|
|
322
|
+
cb({ askAiraResponse: { errors: [], data: [{ name: "item.name" }, { name: "item.quantity" }] }, isError: false })
|
|
323
|
+
);
|
|
324
|
+
await validateLiquidTemplateContent(content, {
|
|
325
|
+
getLiquidTags,
|
|
326
|
+
formatMessage,
|
|
327
|
+
messages,
|
|
328
|
+
onError,
|
|
329
|
+
onSuccess,
|
|
330
|
+
tagLookupMap,
|
|
331
|
+
eventContextTags
|
|
332
|
+
});
|
|
333
|
+
// item.name and item.quantity appear inside {% for %} block, so they should be skipped
|
|
334
|
+
expect(onSuccess).toHaveBeenCalledWith(content, undefined);
|
|
335
|
+
expect(onError).not.toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("should validate tags that don't appear in content but are extracted by API", async () => {
|
|
339
|
+
const content = 'Some content without the tag';
|
|
340
|
+
const getLiquidTags = jest.fn((content, cb) =>
|
|
341
|
+
cb({ askAiraResponse: { errors: [], data: [{ name: "extractedTag" }] }, isError: false })
|
|
342
|
+
);
|
|
343
|
+
await validateLiquidTemplateContent(content, {
|
|
344
|
+
getLiquidTags,
|
|
345
|
+
formatMessage,
|
|
346
|
+
messages,
|
|
347
|
+
onError,
|
|
348
|
+
onSuccess,
|
|
349
|
+
tagLookupMap,
|
|
350
|
+
eventContextTags
|
|
351
|
+
});
|
|
352
|
+
// extractedTag doesn't appear in content but was extracted by API, should be validated
|
|
353
|
+
expect(onError).toHaveBeenCalledWith({
|
|
354
|
+
standardErrors: [],
|
|
355
|
+
liquidErrors: [undefined],
|
|
356
|
+
tabType: undefined
|
|
357
|
+
});
|
|
358
|
+
expect(onSuccess).not.toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should validate tags that appear outside {% %} blocks", async () => {
|
|
362
|
+
const content = '{{ outsideTag }} {% for item in order.items %} {{ item.name }} {% endfor %}';
|
|
363
|
+
const getLiquidTags = jest.fn((content, cb) =>
|
|
364
|
+
cb({ askAiraResponse: { errors: [], data: [{ name: "outsideTag" }, { name: "item.name" }] }, isError: false })
|
|
365
|
+
);
|
|
366
|
+
await validateLiquidTemplateContent(content, {
|
|
367
|
+
getLiquidTags,
|
|
368
|
+
formatMessage,
|
|
369
|
+
messages,
|
|
370
|
+
onError,
|
|
371
|
+
onSuccess,
|
|
372
|
+
tagLookupMap,
|
|
373
|
+
eventContextTags
|
|
374
|
+
});
|
|
375
|
+
// outsideTag appears outside {% %} block, so it should be validated
|
|
376
|
+
// item.name appears inside block, so it should be skipped
|
|
377
|
+
expect(onError).toHaveBeenCalledWith({
|
|
378
|
+
standardErrors: [],
|
|
379
|
+
liquidErrors: [undefined],
|
|
380
|
+
tabType: undefined
|
|
381
|
+
});
|
|
382
|
+
expect(onSuccess).not.toHaveBeenCalled();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should skip tags with dots that appear in {% %} syntax", async () => {
|
|
386
|
+
const content = '{% assign myVar = order.items %} Some text';
|
|
387
|
+
const getLiquidTags = jest.fn((content, cb) =>
|
|
388
|
+
cb({ askAiraResponse: { errors: [], data: [{ name: "order.items" }] }, isError: false })
|
|
389
|
+
);
|
|
390
|
+
await validateLiquidTemplateContent(content, {
|
|
391
|
+
getLiquidTags,
|
|
392
|
+
formatMessage,
|
|
393
|
+
messages,
|
|
394
|
+
onError,
|
|
395
|
+
onSuccess,
|
|
396
|
+
tagLookupMap,
|
|
397
|
+
eventContextTags
|
|
398
|
+
});
|
|
399
|
+
// order.items appears in {% %} syntax, so it should be skipped
|
|
400
|
+
expect(onSuccess).toHaveBeenCalledWith(content, undefined);
|
|
401
|
+
expect(onError).not.toHaveBeenCalled();
|
|
402
|
+
});
|
|
403
|
+
|
|
300
404
|
describe("addBaseToTemplate - subject property logic", () => {
|
|
301
405
|
it("should use template.versions.base.subject if it exists", () => {
|
|
302
406
|
const template = {
|
|
@@ -49,7 +49,7 @@ import { makeSelectMetaEntities, selectCurrentOrgDetails, selectLiquidStateDetai
|
|
|
49
49
|
import * as actions from "../../v2Containers/Cap/actions";
|
|
50
50
|
import './_formBuilder.scss';
|
|
51
51
|
import {updateCharCount, checkUnicode} from "../../utils/smsCharCountV2";
|
|
52
|
-
import { checkSupport, extractNames, preprocessHtml, validateIfTagClosed} from '../../utils/tagValidations';
|
|
52
|
+
import { checkSupport, extractNames, preprocessHtml, validateIfTagClosed, isInsideLiquidBlock} from '../../utils/tagValidations';
|
|
53
53
|
import { containsBase64Images } from '../../utils/content';
|
|
54
54
|
import { SMS, MOBILE_PUSH, LINE, ENABLE_AI_SUGGESTIONS,AI_CONTENT_BOT_DISABLED, EMAIL, LIQUID_SUPPORTED_CHANNELS, INAPP } from '../../v2Containers/CreativesContainer/constants';
|
|
55
55
|
import globalMessages from '../../v2Containers/Cap/messages';
|
|
@@ -1549,6 +1549,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1549
1549
|
}
|
|
1550
1550
|
while (match !== null ) {
|
|
1551
1551
|
const tagValue = match[0].substring(this.indexOfEnd(match[0], '{{'), match[0].indexOf('}}'));
|
|
1552
|
+
const tagIndex = match?.index;
|
|
1552
1553
|
match = regex.exec(content);
|
|
1553
1554
|
let ifSupported = false;
|
|
1554
1555
|
_.forEach(tags, (tag) => {
|
|
@@ -1579,7 +1580,9 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1579
1580
|
});
|
|
1580
1581
|
|
|
1581
1582
|
ifSupported = ifSupported || this.checkIfSupportedTag(tagValue, injectedTags);
|
|
1582
|
-
if
|
|
1583
|
+
// Only add to unsupportedTags if not inside a {% ... %} block and not in liquid flow
|
|
1584
|
+
// Tags inside {% %} blocks can contain any dynamic tags and should not be validated
|
|
1585
|
+
if (!ifSupported && !this.liquidFlow() && !isInsideLiquidBlock(content, tagIndex)) {
|
|
1583
1586
|
response.unsupportedTags.push(tagValue);
|
|
1584
1587
|
response.valid = false;
|
|
1585
1588
|
}
|