@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.231",
4
+ "version": "8.0.232",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -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
- // Find unsupported tags
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) && !skipTags(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) {
@@ -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 = "." + childName?.split(".")?.[1];
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
- result.push(subkey);
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
- const blockRegex = /{%(.*?)%}/gs;
123
- let match;
124
- for (match = blockRegex.exec(content); match !== null; match = blockRegex.exec(content)) {
125
- const blockStart = match.index;
126
- const blockEnd = blockStart + match[0].length;
127
- if (tagIndex >= blockStart && tagIndex < blockEnd) {
128
- return true;
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
- return false;
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
- let io = targetString.indexOf(string);
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
- let regex1 = /{{.*?}}/g;
286
- let regex2 = /{{/g;
287
- let regex3 = /}}/g;
330
+ const regex1 = /{{.*?}}/g;
331
+ const regex2 = /{{/g;
332
+ const regex3 = /}}/g;
288
333
 
289
- let l1 = value.match(regex1)?.length;
290
- let l2 = value.match(regex2)?.length;
291
- let l3 = value.match(regex3)?.length;
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
  "&#39;": "'",
302
345
  "&quot;": "'",
@@ -304,7 +347,7 @@ export const preprocessHtml = (content) => {
304
347
  "&amp;": "&",
305
348
  "&lt;": "<",
306
349
  "&gt;": ">",
307
- "\n": "", // Handling newlines by replacing them with an empty string
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(/&#39;|&quot;|&amp;|&lt;|&gt;|"|\n/g, match => replacements[match]);
364
+ return contentWithStyleFixes?.replace(/&#39;|&quot;|&amp;|&lt;|&gt;|"|\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
- return Object.values(
327
- object
328
- ).reduce((acc, current) => {
329
- return { ...acc, ...current?.subtags ?? {} };
330
- }, {});
331
- };
332
-
333
- export const getLoyaltyTagsMapValue = (object = {}) => {
334
- return Object.entries(object).reduce((acc, [key, current]) => {
335
- if (current?.subtags && Object.keys(current.subtags).length > 0) {
336
- // If subtags exist → merge them
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
- return Object?.entries(object)?.reduce((acc, [key, current]) => {
353
- // Check if current has 'subtags' and it's an object
354
- if (current && current?.subtags && typeof current?.subtags === 'object') {
355
- // Add the top-level key with its 'name' and 'desc'
356
- acc[key] = {
357
- name: current?.name,
358
- desc: current?.desc,
359
- };
360
-
361
- // Merge the subtags into the accumulator
362
- acc = { ...acc, ...current?.subtags };
363
- } else if (current && typeof current === 'object') {
364
- // If no 'subtags', add the top-level key with its 'name' and 'desc'
365
- acc[key] = {
366
- name: current?.name,
367
- desc: current?.desc,
368
- };
369
- }
370
-
371
- // If the current entry is not an object or lacks 'name'/'desc', skip it
372
- return acc;
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 (!ifSupported && !this.liquidFlow()) {
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
  }