@capillarytech/creatives-library 8.0.223 → 8.0.224

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.223",
4
+ "version": "8.0.224",
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 } 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} />
@@ -146,7 +146,7 @@ export const validateLiquidTemplateContent = async (
146
146
  isLiquidFlow,
147
147
  forwardedTags = {},
148
148
  tabType,
149
- skipTags = defaultSkipTags,
149
+ skipTags = () => false,
150
150
  } = options;
151
151
  const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
152
152
  const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
@@ -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.
@@ -155,7 +153,7 @@ export const validateTags = ({
155
153
  unsupportedTags: [],
156
154
  isBraceError: false,
157
155
  };
158
- if(tags && tags.length) {
156
+ if (tags && tags.length) {
159
157
  lodashForEach(tags, ({
160
158
  definition: {
161
159
  supportedModules,
@@ -216,12 +214,12 @@ export const validateTags = ({
216
214
  // validations (eg button) are handled on valid property coming from the response.
217
215
  response.isBraceError ? response.valid = false : response.valid = true;
218
216
  return response;
219
- }
217
+ };
220
218
 
221
219
  /**
222
220
  * Checks if the given tag is supported based on the injected tags.
223
221
  * @param {string} checkingTag - The tag to check.
224
- * @param {Object} injectedTags - The injected tags.
222
+ * @param {Array} injectedTags - The injected tags.
225
223
  * @returns {boolean} - True if the tag is supported, false otherwise.
226
224
  */
227
225
  export const checkIfSupportedTag = (checkingTag, injectedTags) => {
@@ -233,27 +231,32 @@ export const checkIfSupportedTag = (checkingTag, injectedTags) => {
233
231
  result = true;
234
232
  }
235
233
  });
236
-
234
+
237
235
  return result;
238
236
  };
239
237
 
240
238
  const indexOfEnd = (targetString, string) => {
241
- let io = targetString.indexOf(string);
239
+ const io = targetString.indexOf(string);
242
240
  return io == -1 ? -1 : io + string.length;
243
- }
241
+ };
244
242
 
245
243
  export const skipTags = (tag) => {
246
244
  // If the tag contains the word "entryTrigger.", then it's an event context tag and should not be skipped.
247
245
  if (tag?.match(ENTRY_TRIGGER_TAG_REGEX)) {
248
246
  return false;
249
247
  }
250
- const regexGroups = ["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"];
251
- // Use some() to check if any pattern matches (stops on first match)
252
- return regexGroups.some((group) => {
253
- // Create a new RegExp for each test to avoid state issues with global flag
254
- const groupRegex = new RegExp(group);
255
- return groupRegex.test(tag);
248
+ const regexGroups = ["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]*\\)"];
249
+ let skipped = false;
250
+ lodashForEach(regexGroups, (group) => {
251
+ const groupRegex = new RegExp(group, "g");
252
+ const match = groupRegex.exec(tag);
253
+ if (match !== null ) {
254
+ skipped = true;
255
+ return true;
256
+ }
257
+ return true;
256
258
  });
259
+ return skipped;
257
260
  };
258
261
 
259
262
  export const transformInjectedTags = (tags) => {
@@ -269,35 +272,100 @@ export const transformInjectedTags = (tags) => {
269
272
  if (subKey !== '') {
270
273
  temp['tag-header'] = true;
271
274
  if (subKey !== SUBTAGS) {
272
- temp.subtags =lodashCloneDeep(temp[subKey]);
275
+ temp.subtags = lodashCloneDeep(temp[subKey]);
273
276
  delete temp[subKey];
274
277
  }
275
278
  temp.subtags = transformInjectedTags(temp.subtags);
276
279
  }
277
280
  });
278
281
  return tags;
279
- }
282
+ };
280
283
 
281
284
  //checks if the opening curly brackets have corresponding closing brackets
282
285
  export const validateIfTagClosed = (value) => {
283
286
  if (value.includes("{{{{") || value.includes("}}}}")) {
284
287
  return false;
285
288
  }
286
- let regex1 = /{{.*?}}/g;
287
- let regex2 = /{{/g;
288
- let regex3 = /}}/g;
289
+ const regex1 = /{{.*?}}/g;
290
+ const regex2 = /{{/g;
291
+ const regex3 = /}}/g;
289
292
 
290
- let l1 = value.match(regex1)?.length;
291
- let l2 = value.match(regex2)?.length;
292
- let l3 = value.match(regex3)?.length;
293
+ const l1 = value.match(regex1)?.length;
294
+ const l2 = value.match(regex2)?.length;
295
+ const l3 = value.match(regex3)?.length;
293
296
 
294
297
  return (l1 == l2 && l2 == l3 && l1 == l3);
295
-
298
+ };
299
+
300
+ /**
301
+ * Validates tag format: ensures tags are in format {{tag_name}} and checks for invalid patterns
302
+ * Validates against:
303
+ * - Single braces like {tag} (must be {{tag}})
304
+ * - Invalid patterns like {{first or first}}, {{first and first}}
305
+ * - Empty tag names
306
+ * - Unclosed single braces within tag names
307
+ * @param {string} textContent - The text content to validate
308
+ * @returns {boolean} - True if all tags have valid format, false otherwise
309
+ */
310
+ export const validateTagFormat = (textContent) => {
311
+ // Find all potential tag patterns {{tag_name}}
312
+ const tagPattern = /{{[^}]*}}/g;
313
+ const matches = textContent.match(tagPattern) || [];
314
+
315
+ // Remove all valid {{tag}} patterns from content to check for invalid braces
316
+ let contentWithoutValidTags = textContent;
317
+ matches.forEach((match) => {
318
+ contentWithoutValidTags = contentWithoutValidTags.replace(match, '');
319
+ });
320
+
321
+ // Check if there are any remaining braces (single braces or unclosed braces)
322
+ // These would be invalid patterns like {tag}, {first, first}, etc.
323
+ if (contentWithoutValidTags.includes('{') || contentWithoutValidTags.includes('}')) {
324
+ return false;
325
+ }
326
+
327
+ // Check each tag for valid format
328
+ const allTagsValid = matches.every((match) => {
329
+ // Valid tag format: {{tag_name}} - must start with {{ and end with }}
330
+ if (!match.startsWith('{{') || !match.endsWith('}}')) {
331
+ return false;
332
+ }
333
+
334
+ // Extract tag name (content between {{ and }})
335
+ const tagName = match.slice(2, -2).trim();
336
+
337
+ // Tag name should not be empty
338
+ if (!tagName) {
339
+ return false;
340
+ }
341
+
342
+ // Check for invalid patterns in tag name
343
+ // Invalid patterns: "first or first", "first and first", etc.
344
+ const invalidPatterns = [
345
+ /\s+or\s+/i, // " or " as separate word (e.g., "first or first")
346
+ /\s+and\s+/i, // " and " as separate word
347
+ ];
348
+
349
+ const hasInvalidPattern = invalidPatterns.some((pattern) => pattern.test(tagName));
350
+ if (hasInvalidPattern) {
351
+ return false;
352
+ }
353
+
354
+ // Check for unclosed single braces in tag name (e.g., {{first{name}})
355
+ const singleOpenBraces = (tagName.match(/{/g) || []).length;
356
+ const singleCloseBraces = (tagName.match(/}/g) || []).length;
357
+ if (singleOpenBraces !== singleCloseBraces) {
358
+ return false;
359
+ }
360
+
361
+ return true;
362
+ });
363
+
364
+ return allTagsValid;
296
365
  };
297
366
 
298
367
  //replaces encoded string with their respective characters
299
368
  export const preprocessHtml = (content) => {
300
-
301
369
  const replacements = {
302
370
  "&#39;": "'",
303
371
  "&quot;": "'",
@@ -305,7 +373,7 @@ export const preprocessHtml = (content) => {
305
373
  "&amp;": "&",
306
374
  "&lt;": "<",
307
375
  "&gt;": ">",
308
- "\n": "", // Handling newlines by replacing them with an empty string
376
+ "\n": "", // Handling newlines by replacing them with an empty string
309
377
  };
310
378
 
311
379
 
@@ -319,28 +387,22 @@ export const preprocessHtml = (content) => {
319
387
  });
320
388
 
321
389
  // Step 2: Perform the standard replacements on the entire content
322
- return contentWithStyleFixes?.replace(/&#39;|&quot;|&amp;|&lt;|&gt;|"|\n/g, match => replacements[match]);
390
+ return contentWithStyleFixes?.replace(/&#39;|&quot;|&amp;|&lt;|&gt;|"|\n/g, (match) => replacements[match]);
323
391
  };
324
392
 
325
393
  //this is used to get the subtags from custom or extended tags
326
- export const getTagMapValue = (object = {}) => {
327
- return Object.values(
328
- object
329
- ).reduce((acc, current) => {
330
- return { ...acc, ...current?.subtags ?? {} };
331
- }, {});
332
- };
333
-
334
- export const getLoyaltyTagsMapValue = (object = {}) => {
335
- return Object.entries(object).reduce((acc, [key, current]) => {
336
- if (current?.subtags && Object.keys(current.subtags).length > 0) {
337
- // If subtags exist → merge them
338
- return { ...acc, ...(current.subtags ?? {}) };
339
- }
340
- // If no subtags → keep the tag itself
341
- return { ...acc, [key]: current };
342
- }, {});
343
- };
394
+ export const getTagMapValue = (object = {}) => Object.values(
395
+ object
396
+ ).reduce((acc, current) => ({ ...acc, ...current?.subtags ?? {} }), {});
397
+
398
+ export const getLoyaltyTagsMapValue = (object = {}) => Object.entries(object).reduce((acc, [key, current]) => {
399
+ if (current?.subtags && Object.keys(current.subtags).length > 0) {
400
+ // If subtags exist → merge them
401
+ return { ...acc, ...(current.subtags ?? {}) };
402
+ }
403
+ // If no subtags keep the tag itself
404
+ return { ...acc, [key]: current };
405
+ }, {});
344
406
 
345
407
 
346
408
  /**
@@ -349,27 +411,25 @@ export const getLoyaltyTagsMapValue = (object = {}) => {
349
411
  * @param {Object} object - The input object containing top-level keys with optional subtags.
350
412
  * @returns {Object} - A flat map containing all top-level keys and their subtags.
351
413
  */
352
- export const getForwardedMapValues = (object = {}) => {
353
- return Object?.entries(object)?.reduce((acc, [key, current]) => {
354
- // Check if current has 'subtags' and it's an object
355
- if (current && current?.subtags && typeof current?.subtags === 'object') {
356
- // Add the top-level key with its 'name' and 'desc'
357
- acc[key] = {
358
- name: current?.name,
359
- desc: current?.desc,
360
- };
361
-
362
- // Merge the subtags into the accumulator
363
- acc = { ...acc, ...current?.subtags };
364
- } else if (current && typeof current === 'object') {
365
- // If no 'subtags', add the top-level key with its 'name' and 'desc'
366
- acc[key] = {
367
- name: current?.name,
368
- desc: current?.desc,
369
- };
370
- }
371
-
372
- // If the current entry is not an object or lacks 'name'/'desc', skip it
373
- return acc;
374
- }, {});
375
- };
414
+ export const getForwardedMapValues = (object = {}) => Object?.entries(object)?.reduce((acc, [key, current]) => {
415
+ // Check if current has 'subtags' and it's an object
416
+ if (current && current?.subtags && typeof current?.subtags === 'object') {
417
+ // Add the top-level key with its 'name' and 'desc'
418
+ acc[key] = {
419
+ name: current?.name,
420
+ desc: current?.desc,
421
+ };
422
+
423
+ // Merge the subtags into the accumulator
424
+ acc = { ...acc, ...current?.subtags };
425
+ } else if (current && typeof current === 'object') {
426
+ // If no 'subtags', add the top-level key with its 'name' and 'desc'
427
+ acc[key] = {
428
+ name: current?.name,
429
+ desc: current?.desc,
430
+ };
431
+ }
432
+
433
+ // If the current entry is not an object or lacks 'name'/'desc', skip it
434
+ return acc;
435
+ }, {});
@@ -7,7 +7,6 @@ import {
7
7
  addBaseToTemplate,
8
8
  validateCarouselCards,
9
9
  } from "../commonUtils";
10
- import { skipTags } from "../tagValidations";
11
10
  import { SMS_TRAI_VAR } from '../../v2Containers/SmsTrai/Edit/constants';
12
11
  import { ANDROID, IOS } from '../../v2Containers/CreativesContainer/constants';
13
12
 
@@ -123,150 +122,6 @@ describe("validateLiquidTemplateContent", () => {
123
122
  expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
124
123
  });
125
124
 
126
- it("should skip referral_unique_code tags and not trigger unsupported tag error", async () => {
127
- const getLiquidTags = jest.fn((content, cb) =>
128
- cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_C6SOE_userid" }] }, isError: false })
129
- );
130
- await validateLiquidTemplateContent("foo", {
131
- getLiquidTags,
132
- formatMessage,
133
- messages,
134
- onError,
135
- onSuccess,
136
- tagLookupMap,
137
- eventContextTags,
138
- skipTags
139
- });
140
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
141
- expect(onError).not.toHaveBeenCalled();
142
- });
143
-
144
- it("should skip referral_unique_url tags and not trigger unsupported tag error", async () => {
145
- const getLiquidTags = jest.fn((content, cb) =>
146
- cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_url_C6SOE_userid" }] }, isError: false })
147
- );
148
- await validateLiquidTemplateContent("foo", {
149
- getLiquidTags,
150
- formatMessage,
151
- messages,
152
- onError,
153
- onSuccess,
154
- tagLookupMap,
155
- eventContextTags,
156
- skipTags
157
- });
158
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
159
- expect(onError).not.toHaveBeenCalled();
160
- });
161
-
162
- it("should skip referral_unique_code tags with different codes", async () => {
163
- const getLiquidTags = jest.fn((content, cb) =>
164
- cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_ABC123_userid" }] }, isError: false })
165
- );
166
- await validateLiquidTemplateContent("foo", {
167
- getLiquidTags,
168
- formatMessage,
169
- messages,
170
- onError,
171
- onSuccess,
172
- tagLookupMap,
173
- eventContextTags,
174
- skipTags
175
- });
176
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
177
- expect(onError).not.toHaveBeenCalled();
178
- });
179
-
180
- it("should skip referral_unique_url tags with different codes", async () => {
181
- const getLiquidTags = jest.fn((content, cb) =>
182
- cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_url_ABC123_userid" }] }, isError: false })
183
- );
184
- await validateLiquidTemplateContent("foo", {
185
- getLiquidTags,
186
- formatMessage,
187
- messages,
188
- onError,
189
- onSuccess,
190
- tagLookupMap,
191
- eventContextTags,
192
- skipTags
193
- });
194
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
195
- expect(onError).not.toHaveBeenCalled();
196
- });
197
-
198
- it("should skip referral_unique_code tags with alphanumeric codes", async () => {
199
- const getLiquidTags = jest.fn((content, cb) =>
200
- cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_XYZ789_userid" }] }, isError: false })
201
- );
202
- await validateLiquidTemplateContent("foo", {
203
- getLiquidTags,
204
- formatMessage,
205
- messages,
206
- onError,
207
- onSuccess,
208
- tagLookupMap,
209
- eventContextTags,
210
- skipTags
211
- });
212
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
213
- expect(onError).not.toHaveBeenCalled();
214
- });
215
-
216
- it("should skip referral_unique_url tags with alphanumeric codes", async () => {
217
- const getLiquidTags = jest.fn((content, cb) =>
218
- cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_url_XYZ789_userid" }] }, isError: false })
219
- );
220
- await validateLiquidTemplateContent("foo", {
221
- getLiquidTags,
222
- formatMessage,
223
- messages,
224
- onError,
225
- onSuccess,
226
- tagLookupMap,
227
- eventContextTags,
228
- skipTags
229
- });
230
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
231
- expect(onError).not.toHaveBeenCalled();
232
- });
233
-
234
- it("should skip both referral_unique_code and referral_unique_url tags in the same content", async () => {
235
- const getLiquidTags = jest.fn((content, cb) =>
236
- cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_C6SOE_userid" }, { name: "referral_unique_url_C6SOE_userid" }] }, isError: false })
237
- );
238
- await validateLiquidTemplateContent("foo", {
239
- getLiquidTags,
240
- formatMessage,
241
- messages,
242
- onError,
243
- onSuccess,
244
- tagLookupMap,
245
- eventContextTags,
246
- skipTags
247
- });
248
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
249
- expect(onError).not.toHaveBeenCalled();
250
- });
251
-
252
- it("should skip referral_unique_code_632L7_userid tag (real-world example)", async () => {
253
- const getLiquidTags = jest.fn((content, cb) =>
254
- cb({ askAiraResponse: { errors: [], data: [{ name: "referral_unique_code_632L7_userid" }] }, isError: false })
255
- );
256
- await validateLiquidTemplateContent("foo", {
257
- getLiquidTags,
258
- formatMessage,
259
- messages,
260
- onError,
261
- onSuccess,
262
- tagLookupMap,
263
- eventContextTags,
264
- skipTags
265
- });
266
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
267
- expect(onError).not.toHaveBeenCalled();
268
- });
269
-
270
125
  it("calls onError with emailBodyEmptyError when validString is falsy", async () => {
271
126
  const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
272
127
  const formatMessage = jest.fn((msg) => msg.id);
@@ -952,92 +952,6 @@ describe("skipTags", () => {
952
952
  const result = skipTags(tag);
953
953
  expect(result).toEqual(true);
954
954
  });
955
-
956
- it("should return true for referral unique code tags with userid", () => {
957
- const tag = "referral_unique_code_C6SOE_userid";
958
- const result = skipTags(tag);
959
- expect(result).toEqual(true);
960
- });
961
-
962
- it("should return true for referral unique url tags with userid", () => {
963
- const tag = "referral_unique_url_C6SOE_userid";
964
- const result = skipTags(tag);
965
- expect(result).toEqual(true);
966
- });
967
-
968
- it("should return true for referral unique code tags with different codes", () => {
969
- const tag = "referral_unique_code_ABC123_userid";
970
- const result = skipTags(tag);
971
- expect(result).toEqual(true);
972
- });
973
-
974
- it("should return true for referral unique url tags with different codes", () => {
975
- const tag = "referral_unique_url_ABC123_userid";
976
- const result = skipTags(tag);
977
- expect(result).toEqual(true);
978
- });
979
-
980
- it("should return true for referral unique code tags with alphanumeric codes", () => {
981
- const tag = "referral_unique_code_XYZ789_userid";
982
- const result = skipTags(tag);
983
- expect(result).toEqual(true);
984
- });
985
-
986
- it("should return true for referral unique url tags with alphanumeric codes", () => {
987
- const tag = "referral_unique_url_XYZ789_userid";
988
- const result = skipTags(tag);
989
- expect(result).toEqual(true);
990
- });
991
-
992
- it("should return false for tags that don't match referral pattern", () => {
993
- const tag = "referral_code_userid";
994
- const result = skipTags(tag);
995
- expect(result).toEqual(false);
996
- });
997
-
998
- it("should return false for tags with referral but missing unique_code or unique_url", () => {
999
- const tag = "referral_C6SOE_userid";
1000
- const result = skipTags(tag);
1001
- expect(result).toEqual(false);
1002
- });
1003
-
1004
- it("should return false for tags with referral_unique_code but missing userid", () => {
1005
- const tag = "referral_unique_code_C6SOE";
1006
- const result = skipTags(tag);
1007
- expect(result).toEqual(false);
1008
- });
1009
-
1010
- it("should return false for tags with referral_unique_url but missing userid", () => {
1011
- const tag = "referral_unique_url_C6SOE";
1012
- const result = skipTags(tag);
1013
- expect(result).toEqual(false);
1014
- });
1015
-
1016
- it("should return true for referral_unique_code tags with numeric codes like 632L7", () => {
1017
- const tag = "referral_unique_code_632L7_userid";
1018
- const result = skipTags(tag);
1019
- expect(result).toEqual(true);
1020
- });
1021
-
1022
- it("should return true for referral_unique_url tags with numeric codes like 632L7", () => {
1023
- const tag = "referral_unique_url_632L7_userid";
1024
- const result = skipTags(tag);
1025
- expect(result).toEqual(true);
1026
- });
1027
-
1028
- it("should return true for referral tags with various token formats", () => {
1029
- const tags = [
1030
- "referral_unique_code_123_userid",
1031
- "referral_unique_code_ABC_userid",
1032
- "referral_unique_code_123ABC_userid",
1033
- "referral_unique_url_456_userid",
1034
- "referral_unique_url_DEF_userid",
1035
- "referral_unique_url_456DEF_userid",
1036
- ];
1037
- tags.forEach((tag) => {
1038
- expect(skipTags(tag)).toEqual(true);
1039
- });
1040
- });
1041
955
  });
1042
956
 
1043
957
 
@@ -1491,13 +1491,20 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1491
1491
  if (tag?.match(ENTRY_TRIGGER_TAG_REGEX)) {
1492
1492
  return false;
1493
1493
  }
1494
- const regexGroups = ["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"];
1495
- // Use some() to check if any pattern matches (stops on first match)
1496
- return regexGroups.some((group) => {
1497
- // Create a new RegExp for each test to avoid state issues with global flag
1498
- const groupRegex = new RegExp(group);
1499
- return groupRegex.test(tag);
1494
+ const regexGroups = ["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]*\\)"];
1495
+ //const regexGroups = [];
1496
+ let skipped = false;
1497
+ _.forEach(regexGroups, (group) => {
1498
+ //const groupRegex = /dynamic_expiry_date_after_\(\d+\)_days.FORMAT_\d/g;
1499
+ const groupRegex = new RegExp(group, "g");
1500
+ let match = groupRegex.exec(tag);
1501
+ if (match !== null ) {
1502
+ skipped = true;
1503
+ return true;
1504
+ }
1505
+ return true;
1500
1506
  });
1507
+ return skipped;
1501
1508
  }
1502
1509
 
1503
1510
  validateTags(content, tagsParam, injectedTagsParams, isEmail = false) {
@@ -19,6 +19,7 @@ const SendTestMessage = ({
19
19
  formData,
20
20
  isSendingTestMessage,
21
21
  formatMessage,
22
+ isContentValid = true,
22
23
  }) => (
23
24
  <CapStepsAccordian
24
25
  showNumberSteps={false}
@@ -43,7 +44,11 @@ const SendTestMessage = ({
43
44
  multiple
44
45
  placeholder={formatMessage(messages.testCustomersPlaceholder)}
45
46
  />
46
- <CapButton onClick={handleSendTestMessage} disabled={isEmpty(selectedTestEntities) || (isEmpty(formData['template-subject']) && isEmpty(formData[0]?.['template-subject'])) || isSendingTestMessage}>
47
+ <CapButton
48
+ onClick={handleSendTestMessage}
49
+ disabled={isEmpty(selectedTestEntities) || (isEmpty(formData['template-subject']) && isEmpty(formData[0]?.['template-subject'])) || isSendingTestMessage || !isContentValid}
50
+ title={!isContentValid ? formatMessage(messages.contentInvalid) : ''}
51
+ >
47
52
  <FormattedMessage {...messages.sendTestButton} />
48
53
  </CapButton>
49
54
  </CapRow>),
@@ -63,6 +68,7 @@ SendTestMessage.propTypes = {
63
68
  formData: PropTypes.object.isRequired,
64
69
  isSendingTestMessage: PropTypes.bool.isRequired,
65
70
  formatMessage: PropTypes.func.isRequired,
71
+ isContentValid: PropTypes.bool,
66
72
  };
67
73
 
68
74
  export default SendTestMessage;
@@ -52,6 +52,7 @@ import {
52
52
  INITIAL_PAYLOAD, EMAIL, TEST, DESKTOP, ACTIVE, MOBILE,
53
53
  } from './constants';
54
54
  import { GLOBAL_CONVERT_OPTIONS } from '../FormBuilder/constants';
55
+ import { validateIfTagClosed, validateTagFormat } from '../../utils/tagValidations';
55
56
 
56
57
  const TestAndPreviewSlidebox = (props) => {
57
58
  const {
@@ -103,10 +104,58 @@ const TestAndPreviewSlidebox = (props) => {
103
104
  const [selectedTestEntities, setSelectedTestEntities] = useState([]);
104
105
  const [beeContent, setBeeContent] = useState(''); // Track BEE editor content separately
105
106
  const previousBeeContentRef = useRef(''); // Track previous BEE content to prevent unnecessary updates
107
+ const [isContentValid, setIsContentValid] = useState(true); // Track if content tags are valid
106
108
 
107
109
  const isUpdatePreviewDisabled = useMemo(() => (
108
- requiredTags.some((tag) => !customValues[tag.fullPath])
109
- ), [requiredTags, customValues]);
110
+ requiredTags.some((tag) => !customValues[tag.fullPath]) || !isContentValid
111
+ ), [requiredTags, customValues, isContentValid]);
112
+
113
+ /**
114
+ * Validates tags in content: checks for proper tag format and balanced braces
115
+ * Uses validateIfTagClosed and validateTagFormat from utils/tagValidations
116
+ * @param {string} content - The HTML content to validate
117
+ * @returns {boolean} - True if content is valid, false otherwise
118
+ */
119
+ const validateContentTags = (content) => {
120
+ if (!content) return true;
121
+
122
+ try {
123
+ // Convert HTML to text (same as what's used for tag extraction)
124
+ // This ensures we validate the same content that will be used for tag extraction
125
+ const textContent = convert(content, GLOBAL_CONVERT_OPTIONS);
126
+
127
+ // Check if there are any braces in the content
128
+ const hasBraces = textContent.includes('{') || textContent.includes('}');
129
+
130
+ // If no braces exist, content is valid (no tag validation needed)
131
+ if (!hasBraces) {
132
+ return true;
133
+ }
134
+
135
+ // First check if tags are properly closed using the utility function from tagValidations
136
+ // This validates that all opening braces have corresponding closing braces
137
+ if (!validateIfTagClosed(textContent)) {
138
+ return false;
139
+ }
140
+
141
+ // Now validate tag format: tags must be in format {{tag_name}}
142
+ return validateTagFormat(textContent);
143
+ } catch (error) {
144
+ // If conversion fails, fall back to validating the original content
145
+ console.warn('Error converting content for validation:', error);
146
+ const hasBraces = content.includes('{') || content.includes('}');
147
+ if (!hasBraces) {
148
+ return true;
149
+ }
150
+
151
+ // Apply same validation to original content
152
+ if (!validateIfTagClosed(content)) {
153
+ return false;
154
+ }
155
+
156
+ return validateTagFormat(content);
157
+ }
158
+ };
110
159
 
111
160
  // Function to resolve tags in text with custom values
112
161
  const resolveTagsInText = (text, tagValues) => {
@@ -153,6 +202,10 @@ const TestAndPreviewSlidebox = (props) => {
153
202
  if (existingContent && existingContent.trim() !== '') {
154
203
  // We already have content, update local state only if it's different
155
204
  if (existingContent !== previousBeeContentRef.current) {
205
+ // Validate content tags for BEE editor
206
+ const isValid = validateContentTags(existingContent);
207
+ setIsContentValid(isValid);
208
+
156
209
  previousBeeContentRef.current = existingContent;
157
210
  setBeeContent(existingContent);
158
211
  setPreviewDataHtml({
@@ -186,6 +239,10 @@ const TestAndPreviewSlidebox = (props) => {
186
239
  }
187
240
 
188
241
  if (htmlFile) {
242
+ // Validate content tags
243
+ const isValid = validateContentTags(htmlFile);
244
+ setIsContentValid(isValid);
245
+
189
246
  // Update our states
190
247
  previousBeeContentRef.current = htmlFile;
191
248
  setBeeContent(htmlFile);
@@ -194,9 +251,16 @@ const TestAndPreviewSlidebox = (props) => {
194
251
  resolvedTitle: formData['template-subject'] || ''
195
252
  });
196
253
 
197
- // Always extract tags when content changes
198
- const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
199
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
254
+ // Only extract tags if content is valid
255
+ if (isValid) {
256
+ const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
257
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
258
+ } else {
259
+ // Show error notification for invalid content
260
+ CapNotification.error({
261
+ message: formatMessage(messages.contentInvalid),
262
+ });
263
+ }
200
264
  }
201
265
 
202
266
  // Restore original handler
@@ -211,23 +275,45 @@ const TestAndPreviewSlidebox = (props) => {
211
275
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
212
276
 
213
277
  if (templateContent) {
278
+ // Validate content tags
279
+ const isValid = validateContentTags(templateContent);
280
+ setIsContentValid(isValid);
281
+
214
282
  // Update preview with initial content
215
283
  setPreviewDataHtml({
216
284
  resolvedBody: templateContent,
217
285
  resolvedTitle: formData['template-subject'] || ''
218
286
  });
219
287
 
220
- // Always extract tags when showing
221
- const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
222
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
288
+ // Only extract tags if content is valid
289
+ if (isValid) {
290
+ const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
291
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
292
+ } else {
293
+ // Show error notification for invalid content
294
+ CapNotification.error({
295
+ message: formatMessage(messages.contentInvalid),
296
+ });
297
+ }
223
298
  } else {
224
299
  // Fallback to content prop if no template content
225
- const payloadContent = convert(
226
- getCurrentContent,
227
- GLOBAL_CONVERT_OPTIONS
228
- );
229
- // Always extract tags when showing
230
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
300
+ const contentToValidate = getCurrentContent;
301
+ const isValid = validateContentTags(contentToValidate);
302
+ setIsContentValid(isValid);
303
+
304
+ // Only extract tags if content is valid
305
+ if (isValid) {
306
+ const payloadContent = convert(
307
+ contentToValidate,
308
+ GLOBAL_CONVERT_OPTIONS
309
+ );
310
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
311
+ } else {
312
+ // Show error notification for invalid content
313
+ CapNotification.error({
314
+ message: formatMessage(messages.contentInvalid),
315
+ });
316
+ }
231
317
  }
232
318
  }
233
319
 
@@ -243,17 +329,28 @@ const TestAndPreviewSlidebox = (props) => {
243
329
  const isDragDrop = currentTabData?.[activeTab]?.is_drag_drop;
244
330
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
245
331
 
246
- if (templateContent && templateContent.trim() !== '') {
247
- // Common function to handle content update
332
+ if (templateContent && templateContent.trim() !== '' && show) {
333
+ // Common function to handle content update with validation
248
334
  const handleContentUpdate = (content) => {
335
+ // Validate content tags for each update
336
+ const isValid = validateContentTags(content);
337
+ setIsContentValid(isValid);
338
+
249
339
  setPreviewDataHtml({
250
340
  resolvedBody: content,
251
341
  resolvedTitle: formData['template-subject'] || ''
252
342
  });
253
343
 
254
- // Extract tags from content
255
- const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
256
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
344
+ // Only extract tags if content is valid
345
+ if (isValid) {
346
+ const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
347
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
348
+ } else {
349
+ // Show error notification for invalid content
350
+ CapNotification.error({
351
+ message: formatMessage(messages.contentInvalid),
352
+ });
353
+ }
257
354
  };
258
355
 
259
356
  if (isDragDrop) {
@@ -287,6 +384,7 @@ const TestAndPreviewSlidebox = (props) => {
287
384
  setTagsExtracted(false);
288
385
  setPreviewDevice('desktop');
289
386
  setSelectedTestEntities([]);
387
+ setIsContentValid(true);
290
388
  actions.clearPrefilledValues();
291
389
  }
292
390
  }, [show]);
@@ -530,6 +628,22 @@ const TestAndPreviewSlidebox = (props) => {
530
628
 
531
629
  // Handle update preview
532
630
  const handleUpdatePreview = async () => {
631
+ // Re-validate content to get latest state (in case liquid errors were fixed)
632
+ const currentTabData = formData[currentTab - 1];
633
+ const activeTab = currentTabData?.activeTab;
634
+ const templateContent = currentTabData?.[activeTab]?.['template-content'];
635
+ const contentToValidate = templateContent || getCurrentContent;
636
+ const isValid = validateContentTags(contentToValidate);
637
+ setIsContentValid(isValid);
638
+
639
+ // Check if content is valid before updating preview
640
+ if (!isValid) {
641
+ CapNotification.error({
642
+ message: formatMessage(messages.contentInvalid),
643
+ });
644
+ return;
645
+ }
646
+
533
647
  try {
534
648
  // Include unsubscribe tag if content contains it
535
649
  const resolvedTags = { ...customValues };
@@ -559,9 +673,20 @@ const TestAndPreviewSlidebox = (props) => {
559
673
  const currentTabData = formData[currentTab - 1];
560
674
  const activeTab = currentTabData?.activeTab;
561
675
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
676
+ const content = templateContent || getCurrentContent;
677
+
678
+ // Validate content tags before extracting
679
+ const isValid = validateContentTags(content);
680
+ setIsContentValid(isValid);
681
+
682
+ if (!isValid) {
683
+ CapNotification.error({
684
+ message: formatMessage(messages.contentInvalid),
685
+ });
686
+ return;
687
+ }
562
688
 
563
689
  // Check for personalization tags (excluding unsubscribe)
564
- const content = templateContent || getCurrentContent;
565
690
  const tags = content.match(/{{[^}]+}}/g) || [];
566
691
  const hasPersonalizationTags = tags.some(tag => !tag.includes('unsubscribe'));
567
692
 
@@ -590,6 +715,22 @@ const TestAndPreviewSlidebox = (props) => {
590
715
  };
591
716
 
592
717
  const handleSendTestMessage = () => {
718
+ // Re-validate content to get latest state (in case liquid errors were fixed)
719
+ const currentTabData = formData[currentTab - 1];
720
+ const activeTab = currentTabData?.activeTab;
721
+ const templateContent = currentTabData?.[activeTab]?.['template-content'];
722
+ const contentToValidate = templateContent || getCurrentContent;
723
+ const isValid = validateContentTags(contentToValidate);
724
+ setIsContentValid(isValid);
725
+
726
+ // Check if content is valid before sending test message
727
+ if (!isValid) {
728
+ CapNotification.error({
729
+ message: formatMessage(messages.contentInvalid),
730
+ });
731
+ return;
732
+ }
733
+
593
734
  const allUserIds = [];
594
735
  selectedTestEntities.forEach((entityId) => {
595
736
  const group = testGroups.find((g) => g.groupId === entityId);
@@ -685,6 +826,7 @@ const TestAndPreviewSlidebox = (props) => {
685
826
  formData={formData}
686
827
  isSendingTestMessage={isSendingTestMessage}
687
828
  formatMessage={formatMessage}
829
+ isContentValid={isContentValid}
688
830
  />
689
831
  );
690
832
 
@@ -144,4 +144,12 @@ export default defineMessages({
144
144
  id: `${scope}.invalidJSON`,
145
145
  defaultMessage: 'Invalid JSON input',
146
146
  },
147
+ contentInvalid: {
148
+ id: `${scope}.contentInvalid`,
149
+ defaultMessage: 'Content is invalid. Please fix the tags in your content before testing or previewing.',
150
+ },
151
+ previewUpdateError: {
152
+ id: `${scope}.previewUpdateError`,
153
+ defaultMessage: 'Failed to update preview',
154
+ },
147
155
  });