@capillarytech/creatives-library 8.0.223-alpha.0 → 8.0.223
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 +9 -14
- package/utils/tests/commonUtil.test.js +145 -0
- package/utils/tests/tagValidations.test.js +86 -0
- package/v2Components/FormBuilder/index.js +6 -13
- package/v2Components/TestAndPreviewSlidebox/SendTestMessage.js +1 -7
- package/v2Components/TestAndPreviewSlidebox/index.js +20 -251
- package/v2Components/TestAndPreviewSlidebox/messages.js +0 -8
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
|
@@ -221,7 +221,7 @@ export const validateTags = ({
|
|
|
221
221
|
/**
|
|
222
222
|
* Checks if the given tag is supported based on the injected tags.
|
|
223
223
|
* @param {string} checkingTag - The tag to check.
|
|
224
|
-
* @param {
|
|
224
|
+
* @param {Object} injectedTags - The injected tags.
|
|
225
225
|
* @returns {boolean} - True if the tag is supported, false otherwise.
|
|
226
226
|
*/
|
|
227
227
|
export const checkIfSupportedTag = (checkingTag, injectedTags) => {
|
|
@@ -235,7 +235,7 @@ export const checkIfSupportedTag = (checkingTag, injectedTags) => {
|
|
|
235
235
|
});
|
|
236
236
|
|
|
237
237
|
return result;
|
|
238
|
-
}
|
|
238
|
+
};
|
|
239
239
|
|
|
240
240
|
const indexOfEnd = (targetString, string) => {
|
|
241
241
|
let io = targetString.indexOf(string);
|
|
@@ -247,19 +247,14 @@ export const skipTags = (tag) => {
|
|
|
247
247
|
if (tag?.match(ENTRY_TRIGGER_TAG_REGEX)) {
|
|
248
248
|
return false;
|
|
249
249
|
}
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
skipped = true;
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
return true;
|
|
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);
|
|
260
256
|
});
|
|
261
|
-
|
|
262
|
-
}
|
|
257
|
+
};
|
|
263
258
|
|
|
264
259
|
export const transformInjectedTags = (tags) => {
|
|
265
260
|
lodashForEach(tags, (tag) => {
|
|
@@ -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
|
|
|
@@ -1491,20 +1491,13 @@ 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]*\\)"];
|
|
1495
|
-
//
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
let match = groupRegex.exec(tag);
|
|
1501
|
-
if (match !== null ) {
|
|
1502
|
-
skipped = true;
|
|
1503
|
-
return true;
|
|
1504
|
-
}
|
|
1505
|
-
return true;
|
|
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);
|
|
1506
1500
|
});
|
|
1507
|
-
return skipped;
|
|
1508
1501
|
}
|
|
1509
1502
|
|
|
1510
1503
|
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 } from '../../utils/tagValidations';
|
|
56
55
|
|
|
57
56
|
const TestAndPreviewSlidebox = (props) => {
|
|
58
57
|
const {
|
|
@@ -104,147 +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
|
-
// Function to validate tags in content
|
|
114
|
-
const validateContentTags = (content) => {
|
|
115
|
-
if (!content) return true;
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
// Convert HTML to text (same as what's used for tag extraction)
|
|
119
|
-
// This ensures we validate the same content that will be used for tag extraction
|
|
120
|
-
const textContent = convert(content, GLOBAL_CONVERT_OPTIONS);
|
|
121
|
-
|
|
122
|
-
// Check if there are any braces in the content
|
|
123
|
-
const hasBraces = textContent.includes('{') || textContent.includes('}');
|
|
124
|
-
|
|
125
|
-
// If no braces exist, content is valid (no tag validation needed)
|
|
126
|
-
if (!hasBraces) {
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// First check if tags are properly closed using the utility function
|
|
131
|
-
// This validates that all opening braces have corresponding closing braces
|
|
132
|
-
if (!validateIfTagClosed(textContent)) {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Now validate tag format: tags must be in format {{tag_name}}
|
|
137
|
-
// Find all valid tag patterns {{tag_name}}
|
|
138
|
-
const tagPattern = /{{[^}]*}}/g;
|
|
139
|
-
const matches = textContent.match(tagPattern) || [];
|
|
140
|
-
|
|
141
|
-
// Remove all valid {{tag}} patterns from content to check for invalid braces
|
|
142
|
-
let contentWithoutValidTags = textContent;
|
|
143
|
-
matches.forEach((match) => {
|
|
144
|
-
contentWithoutValidTags = contentWithoutValidTags.replace(match, '');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Check if there are any remaining braces (single braces or unclosed braces)
|
|
148
|
-
// These would be invalid patterns like {tag}, {first, first}, etc.
|
|
149
|
-
if (contentWithoutValidTags.includes('{') || contentWithoutValidTags.includes('}')) {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Check each tag for valid format
|
|
154
|
-
for (const match of matches) {
|
|
155
|
-
// Valid tag format: {{tag_name}} - must start with {{ and end with }}
|
|
156
|
-
if (!match.startsWith('{{') || !match.endsWith('}}')) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Extract tag name (content between {{ and }})
|
|
161
|
-
const tagName = match.slice(2, -2).trim();
|
|
162
|
-
|
|
163
|
-
// Tag name should not be empty
|
|
164
|
-
if (!tagName) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Check for invalid patterns in tag name
|
|
169
|
-
// Invalid patterns: "first or first", "first and first", etc.
|
|
170
|
-
const invalidPatterns = [
|
|
171
|
-
/\s+or\s+/i, // " or " as separate word (e.g., "first or first")
|
|
172
|
-
/\s+and\s+/i, // " and " as separate word
|
|
173
|
-
];
|
|
174
|
-
|
|
175
|
-
for (const pattern of invalidPatterns) {
|
|
176
|
-
if (pattern.test(tagName)) {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Check for unclosed single braces in tag name (e.g., {{first{name}})
|
|
182
|
-
const singleOpenBraces = (tagName.match(/{/g) || []).length;
|
|
183
|
-
const singleCloseBraces = (tagName.match(/}/g) || []).length;
|
|
184
|
-
if (singleOpenBraces !== singleCloseBraces) {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return true;
|
|
190
|
-
} catch (error) {
|
|
191
|
-
// If conversion fails, fall back to validating the original content
|
|
192
|
-
console.warn('Error converting content for validation:', error);
|
|
193
|
-
const hasBraces = content.includes('{') || content.includes('}');
|
|
194
|
-
if (!hasBraces) {
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Apply same validation to original content
|
|
199
|
-
if (!validateIfTagClosed(content)) {
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const tagPattern = /{{[^}]*}}/g;
|
|
204
|
-
const matches = content.match(tagPattern) || [];
|
|
205
|
-
|
|
206
|
-
// Remove all valid {{tag}} patterns from content to check for invalid braces
|
|
207
|
-
let contentWithoutValidTags = content;
|
|
208
|
-
matches.forEach((match) => {
|
|
209
|
-
contentWithoutValidTags = contentWithoutValidTags.replace(match, '');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// Check if there are any remaining braces (single braces or unclosed braces)
|
|
213
|
-
if (contentWithoutValidTags.includes('{') || contentWithoutValidTags.includes('}')) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
for (const match of matches) {
|
|
218
|
-
if (!match.startsWith('{{') || !match.endsWith('}}')) {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const tagName = match.slice(2, -2).trim();
|
|
223
|
-
if (!tagName) {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const invalidPatterns = [
|
|
228
|
-
/\s+or\s+/i,
|
|
229
|
-
/\s+and\s+/i,
|
|
230
|
-
];
|
|
231
|
-
|
|
232
|
-
for (const pattern of invalidPatterns) {
|
|
233
|
-
if (pattern.test(tagName)) {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const singleOpenBraces = (tagName.match(/{/g) || []).length;
|
|
239
|
-
const singleCloseBraces = (tagName.match(/}/g) || []).length;
|
|
240
|
-
if (singleOpenBraces !== singleCloseBraces) {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
108
|
+
requiredTags.some((tag) => !customValues[tag.fullPath])
|
|
109
|
+
), [requiredTags, customValues]);
|
|
248
110
|
|
|
249
111
|
// Function to resolve tags in text with custom values
|
|
250
112
|
const resolveTagsInText = (text, tagValues) => {
|
|
@@ -291,10 +153,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
291
153
|
if (existingContent && existingContent.trim() !== '') {
|
|
292
154
|
// We already have content, update local state only if it's different
|
|
293
155
|
if (existingContent !== previousBeeContentRef.current) {
|
|
294
|
-
// Validate content tags for BEE editor
|
|
295
|
-
const isValid = validateContentTags(existingContent);
|
|
296
|
-
setIsContentValid(isValid);
|
|
297
|
-
|
|
298
156
|
previousBeeContentRef.current = existingContent;
|
|
299
157
|
setBeeContent(existingContent);
|
|
300
158
|
setPreviewDataHtml({
|
|
@@ -328,10 +186,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
328
186
|
}
|
|
329
187
|
|
|
330
188
|
if (htmlFile) {
|
|
331
|
-
// Validate content tags
|
|
332
|
-
const isValid = validateContentTags(htmlFile);
|
|
333
|
-
setIsContentValid(isValid);
|
|
334
|
-
|
|
335
189
|
// Update our states
|
|
336
190
|
previousBeeContentRef.current = htmlFile;
|
|
337
191
|
setBeeContent(htmlFile);
|
|
@@ -340,16 +194,9 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
340
194
|
resolvedTitle: formData['template-subject'] || ''
|
|
341
195
|
});
|
|
342
196
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
347
|
-
} else {
|
|
348
|
-
// Show error notification for invalid content
|
|
349
|
-
CapNotification.error({
|
|
350
|
-
message: formatMessage(messages.contentInvalid),
|
|
351
|
-
});
|
|
352
|
-
}
|
|
197
|
+
// Always extract tags when content changes
|
|
198
|
+
const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
|
|
199
|
+
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
353
200
|
}
|
|
354
201
|
|
|
355
202
|
// Restore original handler
|
|
@@ -364,45 +211,23 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
364
211
|
const templateContent = currentTabData?.[activeTab]?.['template-content'];
|
|
365
212
|
|
|
366
213
|
if (templateContent) {
|
|
367
|
-
// Validate content tags
|
|
368
|
-
const isValid = validateContentTags(templateContent);
|
|
369
|
-
setIsContentValid(isValid);
|
|
370
|
-
|
|
371
214
|
// Update preview with initial content
|
|
372
215
|
setPreviewDataHtml({
|
|
373
216
|
resolvedBody: templateContent,
|
|
374
217
|
resolvedTitle: formData['template-subject'] || ''
|
|
375
218
|
});
|
|
376
219
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
381
|
-
} else {
|
|
382
|
-
// Show error notification for invalid content
|
|
383
|
-
CapNotification.error({
|
|
384
|
-
message: formatMessage(messages.contentInvalid),
|
|
385
|
-
});
|
|
386
|
-
}
|
|
220
|
+
// Always extract tags when showing
|
|
221
|
+
const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
|
|
222
|
+
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
387
223
|
} else {
|
|
388
224
|
// Fallback to content prop if no template content
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
const payloadContent = convert(
|
|
396
|
-
contentToValidate,
|
|
397
|
-
GLOBAL_CONVERT_OPTIONS
|
|
398
|
-
);
|
|
399
|
-
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
400
|
-
} else {
|
|
401
|
-
// Show error notification for invalid content
|
|
402
|
-
CapNotification.error({
|
|
403
|
-
message: formatMessage(messages.contentInvalid),
|
|
404
|
-
});
|
|
405
|
-
}
|
|
225
|
+
const payloadContent = convert(
|
|
226
|
+
getCurrentContent,
|
|
227
|
+
GLOBAL_CONVERT_OPTIONS
|
|
228
|
+
);
|
|
229
|
+
// Always extract tags when showing
|
|
230
|
+
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
406
231
|
}
|
|
407
232
|
}
|
|
408
233
|
|
|
@@ -418,28 +243,17 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
418
243
|
const isDragDrop = currentTabData?.[activeTab]?.is_drag_drop;
|
|
419
244
|
const templateContent = currentTabData?.[activeTab]?.['template-content'];
|
|
420
245
|
|
|
421
|
-
if (templateContent && templateContent.trim() !== ''
|
|
422
|
-
// Common function to handle content update
|
|
246
|
+
if (templateContent && templateContent.trim() !== '') {
|
|
247
|
+
// Common function to handle content update
|
|
423
248
|
const handleContentUpdate = (content) => {
|
|
424
|
-
// Validate content tags for each update
|
|
425
|
-
const isValid = validateContentTags(content);
|
|
426
|
-
setIsContentValid(isValid);
|
|
427
|
-
|
|
428
249
|
setPreviewDataHtml({
|
|
429
250
|
resolvedBody: content,
|
|
430
251
|
resolvedTitle: formData['template-subject'] || ''
|
|
431
252
|
});
|
|
432
253
|
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
437
|
-
} else {
|
|
438
|
-
// Show error notification for invalid content
|
|
439
|
-
CapNotification.error({
|
|
440
|
-
message: formatMessage(messages.contentInvalid),
|
|
441
|
-
});
|
|
442
|
-
}
|
|
254
|
+
// Extract tags from content
|
|
255
|
+
const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
|
|
256
|
+
actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
|
|
443
257
|
};
|
|
444
258
|
|
|
445
259
|
if (isDragDrop) {
|
|
@@ -473,7 +287,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
473
287
|
setTagsExtracted(false);
|
|
474
288
|
setPreviewDevice('desktop');
|
|
475
289
|
setSelectedTestEntities([]);
|
|
476
|
-
setIsContentValid(true);
|
|
477
290
|
actions.clearPrefilledValues();
|
|
478
291
|
}
|
|
479
292
|
}, [show]);
|
|
@@ -717,22 +530,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
717
530
|
|
|
718
531
|
// Handle update preview
|
|
719
532
|
const handleUpdatePreview = async () => {
|
|
720
|
-
// Re-validate content to get latest state (in case liquid errors were fixed)
|
|
721
|
-
const currentTabData = formData[currentTab - 1];
|
|
722
|
-
const activeTab = currentTabData?.activeTab;
|
|
723
|
-
const templateContent = currentTabData?.[activeTab]?.['template-content'];
|
|
724
|
-
const contentToValidate = templateContent || getCurrentContent;
|
|
725
|
-
const isValid = validateContentTags(contentToValidate);
|
|
726
|
-
setIsContentValid(isValid);
|
|
727
|
-
|
|
728
|
-
// Check if content is valid before updating preview
|
|
729
|
-
if (!isValid) {
|
|
730
|
-
CapNotification.error({
|
|
731
|
-
message: formatMessage(messages.contentInvalid),
|
|
732
|
-
});
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
533
|
try {
|
|
737
534
|
// Include unsubscribe tag if content contains it
|
|
738
535
|
const resolvedTags = { ...customValues };
|
|
@@ -762,20 +559,9 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
762
559
|
const currentTabData = formData[currentTab - 1];
|
|
763
560
|
const activeTab = currentTabData?.activeTab;
|
|
764
561
|
const templateContent = currentTabData?.[activeTab]?.['template-content'];
|
|
765
|
-
const content = templateContent || getCurrentContent;
|
|
766
|
-
|
|
767
|
-
// Validate content tags before extracting
|
|
768
|
-
const isValid = validateContentTags(content);
|
|
769
|
-
setIsContentValid(isValid);
|
|
770
|
-
|
|
771
|
-
if (!isValid) {
|
|
772
|
-
CapNotification.error({
|
|
773
|
-
message: formatMessage(messages.contentInvalid),
|
|
774
|
-
});
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
562
|
|
|
778
563
|
// Check for personalization tags (excluding unsubscribe)
|
|
564
|
+
const content = templateContent || getCurrentContent;
|
|
779
565
|
const tags = content.match(/{{[^}]+}}/g) || [];
|
|
780
566
|
const hasPersonalizationTags = tags.some(tag => !tag.includes('unsubscribe'));
|
|
781
567
|
|
|
@@ -804,22 +590,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
804
590
|
};
|
|
805
591
|
|
|
806
592
|
const handleSendTestMessage = () => {
|
|
807
|
-
// Re-validate content to get latest state (in case liquid errors were fixed)
|
|
808
|
-
const currentTabData = formData[currentTab - 1];
|
|
809
|
-
const activeTab = currentTabData?.activeTab;
|
|
810
|
-
const templateContent = currentTabData?.[activeTab]?.['template-content'];
|
|
811
|
-
const contentToValidate = templateContent || getCurrentContent;
|
|
812
|
-
const isValid = validateContentTags(contentToValidate);
|
|
813
|
-
setIsContentValid(isValid);
|
|
814
|
-
|
|
815
|
-
// Check if content is valid before sending test message
|
|
816
|
-
if (!isValid) {
|
|
817
|
-
CapNotification.error({
|
|
818
|
-
message: formatMessage(messages.contentInvalid),
|
|
819
|
-
});
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
593
|
const allUserIds = [];
|
|
824
594
|
selectedTestEntities.forEach((entityId) => {
|
|
825
595
|
const group = testGroups.find((g) => g.groupId === entityId);
|
|
@@ -915,7 +685,6 @@ const TestAndPreviewSlidebox = (props) => {
|
|
|
915
685
|
formData={formData}
|
|
916
686
|
isSendingTestMessage={isSendingTestMessage}
|
|
917
687
|
formatMessage={formatMessage}
|
|
918
|
-
isContentValid={isContentValid}
|
|
919
688
|
/>
|
|
920
689
|
);
|
|
921
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
|
});
|