@capillarytech/creatives-library 8.0.224 → 8.0.226
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 -0
- package/package.json +1 -1
- package/utils/commonUtils.js +2 -2
- package/utils/tagValidations.js +74 -135
- package/utils/tests/commonUtil.test.js +145 -0
- package/utils/tests/tagValidations.test.js +86 -0
- package/v2Components/FormBuilder/index.js +6 -14
- package/v2Components/TestAndPreviewSlidebox/SendTestMessage.js +1 -7
- package/v2Components/TestAndPreviewSlidebox/index.js +20 -162
- package/v2Components/TestAndPreviewSlidebox/messages.js +0 -8
package/constants/unified.js
CHANGED
|
@@ -149,6 +149,7 @@ export const BADGES_ISSUE = 'BADGES_ISSUE';
|
|
|
149
149
|
export const CUSTOMER_BARCODE_TAG = 'customer_barcode';
|
|
150
150
|
export const COPY_OF = 'Copy of';
|
|
151
151
|
export const ENTRY_TRIGGER_TAG_REGEX = /\bentryTrigger\.\w+(?:\.\w+)?(?:\(\w+\))?/g;
|
|
152
|
+
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"];
|
|
152
153
|
|
|
153
154
|
export const GET_TRANSLATION_MAPPED = {
|
|
154
155
|
'en': 'en-US',
|
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 } from "./tagValidations";
|
|
19
|
+
import { checkSupport, extractNames, skipTags as defaultSkipTags } 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 =
|
|
149
|
+
skipTags = defaultSkipTags,
|
|
150
150
|
} = options;
|
|
151
151
|
const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
|
|
152
152
|
const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
|
package/utils/tagValidations.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import lodashForEach from 'lodash/forEach';
|
|
10
10
|
import lodashCloneDeep from 'lodash/cloneDeep';
|
|
11
|
-
import { ENTRY_TRIGGER_TAG_REGEX } from '../constants/unified';
|
|
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';
|
|
@@ -19,6 +19,7 @@ const SUBTAGS = 'subtags';
|
|
|
19
19
|
* @param {Object} tagObject - The tagLookupMap.
|
|
20
20
|
*/
|
|
21
21
|
export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [], isLiquidFlow = false, forwardedTags = {}) => {
|
|
22
|
+
|
|
22
23
|
const supportedList = [];
|
|
23
24
|
// Verifies the presence of the tag in the 'Add Labels' section.
|
|
24
25
|
// Incase of journey event context the tags won't be available in the tagObject(tagLookupMap).
|
|
@@ -39,7 +40,7 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
39
40
|
let updatedChildName = childName;
|
|
40
41
|
let updatedWithoutDotChildName = childName;
|
|
41
42
|
if (childName?.includes(".")) {
|
|
42
|
-
updatedChildName =
|
|
43
|
+
updatedChildName = "." + childName?.split(".")?.[1];
|
|
43
44
|
updatedWithoutDotChildName = childName?.split(".")?.[1];
|
|
44
45
|
}
|
|
45
46
|
if (tagObject?.[parentTag]) {
|
|
@@ -70,6 +71,7 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
70
71
|
if (item?.children?.length) {
|
|
71
72
|
processChildren(item?.name, item?.children);
|
|
72
73
|
}
|
|
74
|
+
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
|
|
@@ -78,19 +80,19 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
78
80
|
|
|
79
81
|
const handleForwardedTags = (forwardedTags) => {
|
|
80
82
|
const result = [];
|
|
81
|
-
Object.keys(forwardedTags).forEach(
|
|
83
|
+
Object.keys(forwardedTags).forEach(key => {
|
|
82
84
|
result.push(key); // Add the main key to the result array
|
|
83
|
-
|
|
85
|
+
|
|
84
86
|
// Check if there are subtags for the current key
|
|
85
87
|
if (forwardedTags[key].subtags) {
|
|
86
88
|
// If subtags exist, add all subtag keys to the result array
|
|
87
|
-
Object.keys(forwardedTags[key].subtags).forEach(
|
|
88
|
-
|
|
89
|
+
Object.keys(forwardedTags[key].subtags).forEach(subkey => {
|
|
90
|
+
result.push(subkey);
|
|
89
91
|
});
|
|
90
92
|
}
|
|
91
93
|
});
|
|
92
94
|
return result;
|
|
93
|
-
}
|
|
95
|
+
}
|
|
94
96
|
|
|
95
97
|
/**
|
|
96
98
|
* Extracts the names from the given data.
|
|
@@ -153,7 +155,7 @@ export const validateTags = ({
|
|
|
153
155
|
unsupportedTags: [],
|
|
154
156
|
isBraceError: false,
|
|
155
157
|
};
|
|
156
|
-
if
|
|
158
|
+
if(tags && tags.length) {
|
|
157
159
|
lodashForEach(tags, ({
|
|
158
160
|
definition: {
|
|
159
161
|
supportedModules,
|
|
@@ -214,12 +216,12 @@ export const validateTags = ({
|
|
|
214
216
|
// validations (eg button) are handled on valid property coming from the response.
|
|
215
217
|
response.isBraceError ? response.valid = false : response.valid = true;
|
|
216
218
|
return response;
|
|
217
|
-
}
|
|
219
|
+
}
|
|
218
220
|
|
|
219
221
|
/**
|
|
220
222
|
* Checks if the given tag is supported based on the injected tags.
|
|
221
223
|
* @param {string} checkingTag - The tag to check.
|
|
222
|
-
* @param {
|
|
224
|
+
* @param {Object} injectedTags - The injected tags.
|
|
223
225
|
* @returns {boolean} - True if the tag is supported, false otherwise.
|
|
224
226
|
*/
|
|
225
227
|
export const checkIfSupportedTag = (checkingTag, injectedTags) => {
|
|
@@ -231,32 +233,26 @@ export const checkIfSupportedTag = (checkingTag, injectedTags) => {
|
|
|
231
233
|
result = true;
|
|
232
234
|
}
|
|
233
235
|
});
|
|
234
|
-
|
|
236
|
+
|
|
235
237
|
return result;
|
|
236
238
|
};
|
|
237
239
|
|
|
238
240
|
const indexOfEnd = (targetString, string) => {
|
|
239
|
-
|
|
241
|
+
let io = targetString.indexOf(string);
|
|
240
242
|
return io == -1 ? -1 : io + string.length;
|
|
241
|
-
}
|
|
243
|
+
}
|
|
242
244
|
|
|
243
245
|
export const skipTags = (tag) => {
|
|
244
246
|
// If the tag contains the word "entryTrigger.", then it's an event context tag and should not be skipped.
|
|
245
247
|
if (tag?.match(ENTRY_TRIGGER_TAG_REGEX)) {
|
|
246
248
|
return false;
|
|
247
249
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const groupRegex = new RegExp(group
|
|
252
|
-
|
|
253
|
-
if (match !== null ) {
|
|
254
|
-
skipped = true;
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
return true;
|
|
250
|
+
// Use some() to check if any pattern matches (stops on first match)
|
|
251
|
+
return SKIP_TAGS_REGEX_GROUPS.some((group) => {
|
|
252
|
+
// Create a new RegExp for each test to avoid state issues with global flag
|
|
253
|
+
const groupRegex = new RegExp(group);
|
|
254
|
+
return groupRegex.test(tag);
|
|
258
255
|
});
|
|
259
|
-
return skipped;
|
|
260
256
|
};
|
|
261
257
|
|
|
262
258
|
export const transformInjectedTags = (tags) => {
|
|
@@ -272,100 +268,35 @@ export const transformInjectedTags = (tags) => {
|
|
|
272
268
|
if (subKey !== '') {
|
|
273
269
|
temp['tag-header'] = true;
|
|
274
270
|
if (subKey !== SUBTAGS) {
|
|
275
|
-
temp.subtags =
|
|
271
|
+
temp.subtags =lodashCloneDeep(temp[subKey]);
|
|
276
272
|
delete temp[subKey];
|
|
277
273
|
}
|
|
278
274
|
temp.subtags = transformInjectedTags(temp.subtags);
|
|
279
275
|
}
|
|
280
276
|
});
|
|
281
277
|
return tags;
|
|
282
|
-
}
|
|
278
|
+
}
|
|
283
279
|
|
|
284
280
|
//checks if the opening curly brackets have corresponding closing brackets
|
|
285
281
|
export const validateIfTagClosed = (value) => {
|
|
286
282
|
if (value.includes("{{{{") || value.includes("}}}}")) {
|
|
287
283
|
return false;
|
|
288
284
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
let regex1 = /{{.*?}}/g;
|
|
286
|
+
let regex2 = /{{/g;
|
|
287
|
+
let regex3 = /}}/g;
|
|
292
288
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
let l1 = value.match(regex1)?.length;
|
|
290
|
+
let l2 = value.match(regex2)?.length;
|
|
291
|
+
let l3 = value.match(regex3)?.length;
|
|
296
292
|
|
|
297
293
|
return (l1 == l2 && l2 == l3 && l1 == l3);
|
|
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;
|
|
294
|
+
|
|
365
295
|
};
|
|
366
296
|
|
|
367
297
|
//replaces encoded string with their respective characters
|
|
368
298
|
export const preprocessHtml = (content) => {
|
|
299
|
+
|
|
369
300
|
const replacements = {
|
|
370
301
|
"'": "'",
|
|
371
302
|
""": "'",
|
|
@@ -373,7 +304,7 @@ export const preprocessHtml = (content) => {
|
|
|
373
304
|
"&": "&",
|
|
374
305
|
"<": "<",
|
|
375
306
|
">": ">",
|
|
376
|
-
"\n": "",
|
|
307
|
+
"\n": "", // Handling newlines by replacing them with an empty string
|
|
377
308
|
};
|
|
378
309
|
|
|
379
310
|
|
|
@@ -387,22 +318,28 @@ export const preprocessHtml = (content) => {
|
|
|
387
318
|
});
|
|
388
319
|
|
|
389
320
|
// Step 2: Perform the standard replacements on the entire content
|
|
390
|
-
return contentWithStyleFixes?.replace(/'|"|&|<|>|"|\n/g,
|
|
321
|
+
return contentWithStyleFixes?.replace(/'|"|&|<|>|"|\n/g, match => replacements[match]);
|
|
391
322
|
};
|
|
392
323
|
|
|
393
324
|
//this is used to get the subtags from custom or extended tags
|
|
394
|
-
export const getTagMapValue = (object = {}) =>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
+
};
|
|
406
343
|
|
|
407
344
|
|
|
408
345
|
/**
|
|
@@ -411,25 +348,27 @@ export const getLoyaltyTagsMapValue = (object = {}) => Object.entries(object).re
|
|
|
411
348
|
* @param {Object} object - The input object containing top-level keys with optional subtags.
|
|
412
349
|
* @returns {Object} - A flat map containing all top-level keys and their subtags.
|
|
413
350
|
*/
|
|
414
|
-
export const getForwardedMapValues = (object = {}) =>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
addBaseToTemplate,
|
|
8
8
|
validateCarouselCards,
|
|
9
9
|
} from "../commonUtils";
|
|
10
|
+
import { skipTags } from "../tagValidations";
|
|
10
11
|
import { SMS_TRAI_VAR } from '../../v2Containers/SmsTrai/Edit/constants';
|
|
11
12
|
import { ANDROID, IOS } from '../../v2Containers/CreativesContainer/constants';
|
|
12
13
|
|
|
@@ -122,6 +123,150 @@ describe("validateLiquidTemplateContent", () => {
|
|
|
122
123
|
expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
|
|
123
124
|
});
|
|
124
125
|
|
|
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
|
+
|
|
125
270
|
it("calls onError with emailBodyEmptyError when validString is falsy", async () => {
|
|
126
271
|
const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
|
|
127
272
|
const formatMessage = jest.fn((msg) => msg.id);
|
|
@@ -952,6 +952,92 @@ 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
|
+
});
|
|
955
1041
|
});
|
|
956
1042
|
|
|
957
1043
|
|
|
@@ -57,7 +57,7 @@ import { convert } from 'html-to-text';
|
|
|
57
57
|
import { OUTBOUND, ADD_LANGUAGE, UPLOAD, USE_EDITOR, COPY_PRIMARY_LANGUAGE } from './constants';
|
|
58
58
|
import { GET_TRANSLATION_MAPPED } from '../../constants/unified';
|
|
59
59
|
import moment from 'moment';
|
|
60
|
-
import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX} from '../../constants/unified';
|
|
60
|
+
import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS} from '../../constants/unified';
|
|
61
61
|
import { REQUEST } from '../../v2Containers/Cap/constants'
|
|
62
62
|
import { hasLiquidSupportFeature, isEmailUnsubscribeTagMandatory } from '../../utils/common';
|
|
63
63
|
import { isUrl } from '../../v2Containers/Line/Container/Wrapper/utils';
|
|
@@ -1491,20 +1491,12 @@ 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
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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;
|
|
1494
|
+
// Use some() to check if any pattern matches (stops on first match)
|
|
1495
|
+
return SKIP_TAGS_REGEX_GROUPS.some((group) => {
|
|
1496
|
+
// Create a new RegExp for each test to avoid state issues with global flag
|
|
1497
|
+
const groupRegex = new RegExp(group);
|
|
1498
|
+
return groupRegex.test(tag);
|
|
1506
1499
|
});
|
|
1507
|
-
return skipped;
|
|
1508
1500
|
}
|
|
1509
1501
|
|
|
1510
1502
|
validateTags(content, tagsParam, injectedTagsParams, isEmail = false) {
|
|
@@ -19,7 +19,6 @@ const SendTestMessage = ({
|
|
|
19
19
|
formData,
|
|
20
20
|
isSendingTestMessage,
|
|
21
21
|
formatMessage,
|
|
22
|
-
isContentValid = true,
|
|
23
22
|
}) => (
|
|
24
23
|
<CapStepsAccordian
|
|
25
24
|
showNumberSteps={false}
|
|
@@ -44,11 +43,7 @@ const SendTestMessage = ({
|
|
|
44
43
|
multiple
|
|
45
44
|
placeholder={formatMessage(messages.testCustomersPlaceholder)}
|
|
46
45
|
/>
|
|
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
|
-
>
|
|
46
|
+
<CapButton onClick={handleSendTestMessage} disabled={isEmpty(selectedTestEntities) || (isEmpty(formData['template-subject']) && isEmpty(formData[0]?.['template-subject'])) || isSendingTestMessage}>
|
|
52
47
|
<FormattedMessage {...messages.sendTestButton} />
|
|
53
48
|
</CapButton>
|
|
54
49
|
</CapRow>),
|
|
@@ -68,7 +63,6 @@ SendTestMessage.propTypes = {
|
|
|
68
63
|
formData: PropTypes.object.isRequired,
|
|
69
64
|
isSendingTestMessage: PropTypes.bool.isRequired,
|
|
70
65
|
formatMessage: PropTypes.func.isRequired,
|
|
71
|
-
isContentValid: PropTypes.bool,
|
|
72
66
|
};
|
|
73
67
|
|
|
74
68
|
export default SendTestMessage;
|
|
@@ -52,7 +52,6 @@ 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';
|
|
56
55
|
|
|
57
56
|
const TestAndPreviewSlidebox = (props) => {
|
|
58
57
|
const {
|
|
@@ -104,58 +103,10 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
104
103
|
const [selectedTestEntities, setSelectedTestEntities] = useState([]);
|
|
105
104
|
const [beeContent, setBeeContent] = useState(''); // Track BEE editor content separately
|
|
106
105
|
const previousBeeContentRef = useRef(''); // Track previous BEE content to prevent unnecessary updates
|
|
107
|
-
const [isContentValid, setIsContentValid] = useState(true); // Track if content tags are valid
|
|
108
106
|
|
|
109
107
|
const isUpdatePreviewDisabled = useMemo(() => (
|
|
110
|
-
requiredTags.some((tag) => !customValues[tag.fullPath])
|
|
111
|
-
), [requiredTags, customValues
|
|
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
|
-
};
|
|
108
|
+
requiredTags.some((tag) => !customValues[tag.fullPath])
|
|
109
|
+
), [requiredTags, customValues]);
|
|
159
110
|
|
|
160
111
|
// Function to resolve tags in text with custom values
|
|
161
112
|
const resolveTagsInText = (text, tagValues) => {
|
|
@@ -202,10 +153,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
202
153
|
if (existingContent && existingContent.trim() !== '') {
|
|
203
154
|
// We already have content, update local state only if it's different
|
|
204
155
|
if (existingContent !== previousBeeContentRef.current) {
|
|
205
|
-
// Validate content tags for BEE editor
|
|
206
|
-
const isValid = validateContentTags(existingContent);
|
|
207
|
-
setIsContentValid(isValid);
|
|
208
|
-
|
|
209
156
|
previousBeeContentRef.current = existingContent;
|
|
210
157
|
setBeeContent(existingContent);
|
|
211
158
|
setPreviewDataHtml({
|
|
@@ -239,10 +186,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
239
186
|
}
|
|
240
187
|
|
|
241
188
|
if (htmlFile) {
|
|
242
|
-
// Validate content tags
|
|
243
|
-
const isValid = validateContentTags(htmlFile);
|
|
244
|
-
setIsContentValid(isValid);
|
|
245
|
-
|
|
246
189
|
// Update our states
|
|
247
190
|
previousBeeContentRef.current = htmlFile;
|
|
248
191
|
setBeeContent(htmlFile);
|
|
@@ -251,16 +194,9 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
251
194
|
resolvedTitle: formData['template-subject'] || ''
|
|
252
195
|
});
|
|
253
196
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
}
|
|
197
|
+
// Always extract tags when content changes
|
|
198
|
+
const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
|
|
199
|
+
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
264
200
|
}
|
|
265
201
|
|
|
266
202
|
// Restore original handler
|
|
@@ -275,45 +211,23 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
275
211
|
const templateContent = currentTabData?.[activeTab]?.['template-content'];
|
|
276
212
|
|
|
277
213
|
if (templateContent) {
|
|
278
|
-
// Validate content tags
|
|
279
|
-
const isValid = validateContentTags(templateContent);
|
|
280
|
-
setIsContentValid(isValid);
|
|
281
|
-
|
|
282
214
|
// Update preview with initial content
|
|
283
215
|
setPreviewDataHtml({
|
|
284
216
|
resolvedBody: templateContent,
|
|
285
217
|
resolvedTitle: formData['template-subject'] || ''
|
|
286
218
|
});
|
|
287
219
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
}
|
|
220
|
+
// Always extract tags when showing
|
|
221
|
+
const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
|
|
222
|
+
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
298
223
|
} else {
|
|
299
224
|
// Fallback to content prop if no template content
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
|
|
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
|
-
}
|
|
225
|
+
const payloadContent = convert(
|
|
226
|
+
getCurrentContent,
|
|
227
|
+
GLOBAL_CONVERT_OPTIONS
|
|
228
|
+
);
|
|
229
|
+
// Always extract tags when showing
|
|
230
|
+
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
317
231
|
}
|
|
318
232
|
}
|
|
319
233
|
|
|
@@ -329,28 +243,17 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
329
243
|
const isDragDrop = currentTabData?.[activeTab]?.is_drag_drop;
|
|
330
244
|
const templateContent = currentTabData?.[activeTab]?.['template-content'];
|
|
331
245
|
|
|
332
|
-
if (templateContent && templateContent.trim() !== ''
|
|
333
|
-
// Common function to handle content update
|
|
246
|
+
if (templateContent && templateContent.trim() !== '') {
|
|
247
|
+
// Common function to handle content update
|
|
334
248
|
const handleContentUpdate = (content) => {
|
|
335
|
-
// Validate content tags for each update
|
|
336
|
-
const isValid = validateContentTags(content);
|
|
337
|
-
setIsContentValid(isValid);
|
|
338
|
-
|
|
339
249
|
setPreviewDataHtml({
|
|
340
250
|
resolvedBody: content,
|
|
341
251
|
resolvedTitle: formData['template-subject'] || ''
|
|
342
252
|
});
|
|
343
253
|
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
}
|
|
254
|
+
// Extract tags from content
|
|
255
|
+
const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
|
|
256
|
+
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
354
257
|
};
|
|
355
258
|
|
|
356
259
|
if (isDragDrop) {
|
|
@@ -384,7 +287,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
384
287
|
setTagsExtracted(false);
|
|
385
288
|
setPreviewDevice('desktop');
|
|
386
289
|
setSelectedTestEntities([]);
|
|
387
|
-
setIsContentValid(true);
|
|
388
290
|
actions.clearPrefilledValues();
|
|
389
291
|
}
|
|
390
292
|
}, [show]);
|
|
@@ -628,22 +530,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
628
530
|
|
|
629
531
|
// Handle update preview
|
|
630
532
|
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
|
-
|
|
647
533
|
try {
|
|
648
534
|
// Include unsubscribe tag if content contains it
|
|
649
535
|
const resolvedTags = { ...customValues };
|
|
@@ -673,20 +559,9 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
673
559
|
const currentTabData = formData[currentTab - 1];
|
|
674
560
|
const activeTab = currentTabData?.activeTab;
|
|
675
561
|
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
|
-
}
|
|
688
562
|
|
|
689
563
|
// Check for personalization tags (excluding unsubscribe)
|
|
564
|
+
const content = templateContent || getCurrentContent;
|
|
690
565
|
const tags = content.match(/{{[^}]+}}/g) || [];
|
|
691
566
|
const hasPersonalizationTags = tags.some(tag => !tag.includes('unsubscribe'));
|
|
692
567
|
|
|
@@ -715,22 +590,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
715
590
|
};
|
|
716
591
|
|
|
717
592
|
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
|
-
|
|
734
593
|
const allUserIds = [];
|
|
735
594
|
selectedTestEntities.forEach((entityId) => {
|
|
736
595
|
const group = testGroups.find((g) => g.groupId === entityId);
|
|
@@ -826,7 +685,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
826
685
|
formData={formData}
|
|
827
686
|
isSendingTestMessage={isSendingTestMessage}
|
|
828
687
|
formatMessage={formatMessage}
|
|
829
|
-
isContentValid={isContentValid}
|
|
830
688
|
/>
|
|
831
689
|
);
|
|
832
690
|
|
|
@@ -144,12 +144,4 @@ 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
|
-
},
|
|
155
147
|
});
|