@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 +1 -1
- package/utils/commonUtils.js +2 -2
- package/utils/tagValidations.js +134 -74
- package/utils/tests/commonUtil.test.js +0 -145
- package/utils/tests/tagValidations.test.js +0 -86
- package/v2Components/FormBuilder/index.js +13 -6
- package/v2Components/TestAndPreviewSlidebox/SendTestMessage.js +7 -1
- package/v2Components/TestAndPreviewSlidebox/index.js +162 -20
- package/v2Components/TestAndPreviewSlidebox/messages.js +8 -0
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
|
|
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 =
|
|
149
|
+
skipTags = () => false,
|
|
150
150
|
} = options;
|
|
151
151
|
const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
|
|
152
152
|
const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
|
package/utils/tagValidations.js
CHANGED
|
@@ -19,7 +19,6 @@ const SUBTAGS = 'subtags';
|
|
|
19
19
|
* @param {Object} tagObject - The tagLookupMap.
|
|
20
20
|
*/
|
|
21
21
|
export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [], isLiquidFlow = false, forwardedTags = {}) => {
|
|
22
|
-
|
|
23
22
|
const supportedList = [];
|
|
24
23
|
// Verifies the presence of the tag in the 'Add Labels' section.
|
|
25
24
|
// Incase of journey event context the tags won't be available in the tagObject(tagLookupMap).
|
|
@@ -40,7 +39,7 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
40
39
|
let updatedChildName = childName;
|
|
41
40
|
let updatedWithoutDotChildName = childName;
|
|
42
41
|
if (childName?.includes(".")) {
|
|
43
|
-
updatedChildName =
|
|
42
|
+
updatedChildName = `.${childName?.split(".")?.[1]}`;
|
|
44
43
|
updatedWithoutDotChildName = childName?.split(".")?.[1];
|
|
45
44
|
}
|
|
46
45
|
if (tagObject?.[parentTag]) {
|
|
@@ -71,7 +70,6 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
71
70
|
if (item?.children?.length) {
|
|
72
71
|
processChildren(item?.name, item?.children);
|
|
73
72
|
}
|
|
74
|
-
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
|
|
@@ -80,19 +78,19 @@ export const checkSupport = (response = {}, tagObject = {}, eventContextTags = [
|
|
|
80
78
|
|
|
81
79
|
const handleForwardedTags = (forwardedTags) => {
|
|
82
80
|
const result = [];
|
|
83
|
-
Object.keys(forwardedTags).forEach(key => {
|
|
81
|
+
Object.keys(forwardedTags).forEach((key) => {
|
|
84
82
|
result.push(key); // Add the main key to the result array
|
|
85
|
-
|
|
83
|
+
|
|
86
84
|
// Check if there are subtags for the current key
|
|
87
85
|
if (forwardedTags[key].subtags) {
|
|
88
86
|
// If subtags exist, add all subtag keys to the result array
|
|
89
|
-
Object.keys(forwardedTags[key].subtags).forEach(subkey => {
|
|
90
|
-
|
|
87
|
+
Object.keys(forwardedTags[key].subtags).forEach((subkey) => {
|
|
88
|
+
result.push(subkey);
|
|
91
89
|
});
|
|
92
90
|
}
|
|
93
91
|
});
|
|
94
92
|
return result;
|
|
95
|
-
}
|
|
93
|
+
};
|
|
96
94
|
|
|
97
95
|
/**
|
|
98
96
|
* Extracts the names from the given data.
|
|
@@ -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 {
|
|
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
|
-
|
|
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]*\\)"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
+
const regex1 = /{{.*?}}/g;
|
|
290
|
+
const regex2 = /{{/g;
|
|
291
|
+
const regex3 = /}}/g;
|
|
289
292
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
"'": "'",
|
|
303
371
|
""": "'",
|
|
@@ -305,7 +373,7 @@ export const preprocessHtml = (content) => {
|
|
|
305
373
|
"&": "&",
|
|
306
374
|
"<": "<",
|
|
307
375
|
">": ">",
|
|
308
|
-
"\n": "",
|
|
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(/'|"|&|<|>|"|\n/g, match => replacements[match]);
|
|
390
|
+
return contentWithStyleFixes?.replace(/'|"|&|<|>|"|\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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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}\\)",
|
|
1495
|
-
//
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
const groupRegex =
|
|
1499
|
-
|
|
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
|
|
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
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
|
|
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
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
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
|
});
|