@capillarytech/creatives-library 8.0.306 → 8.0.307
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 +5 -1
- package/initialState.js +0 -2
- package/package.json +1 -1
- package/utils/common.js +5 -8
- package/utils/commonUtils.js +36 -93
- package/utils/tagValidations.js +83 -223
- package/utils/tests/commonUtil.test.js +147 -124
- package/utils/tests/tagValidations.test.js +441 -358
- package/v2Components/ErrorInfoNote/index.js +2 -5
- package/v2Components/FormBuilder/index.js +137 -203
- package/v2Components/FormBuilder/messages.js +0 -8
- package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
- package/v2Containers/Cap/mockData.js +0 -14
- package/v2Containers/Cap/reducer.js +3 -55
- package/v2Containers/Cap/tests/reducer.test.js +0 -102
- package/v2Containers/CreativesContainer/SlideBoxContent.js +5 -1
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -5
- package/v2Containers/CreativesContainer/constants.js +6 -0
- package/v2Containers/CreativesContainer/index.js +47 -7
- package/v2Containers/Email/index.js +1 -5
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +23 -70
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -120
- package/v2Containers/FTP/index.js +2 -51
- package/v2Containers/FTP/messages.js +0 -4
- package/v2Containers/InApp/index.js +35 -107
- package/v2Containers/InApp/tests/index.test.js +17 -6
- package/v2Containers/InappAdvance/index.js +4 -112
- package/v2Containers/InappAdvance/tests/index.test.js +2 -0
- package/v2Containers/Line/Container/Text/index.js +0 -1
- package/v2Containers/MobilePush/Create/index.js +59 -19
- package/v2Containers/MobilePush/Edit/index.js +48 -20
- package/v2Containers/MobilePushNew/index.js +12 -32
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/index.js +12 -37
- package/v2Containers/Sms/Create/index.js +39 -3
- package/v2Containers/Sms/Create/messages.js +4 -0
- package/v2Containers/Sms/Edit/index.js +35 -3
- package/v2Containers/Sms/commonMethods.js +3 -6
- package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
- package/v2Containers/SmsTrai/Edit/index.js +11 -47
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/TemplatesV2/index.js +28 -13
- package/v2Containers/Viber/index.js +0 -1
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
- package/v2Containers/WebPush/Create/index.js +2 -2
- package/v2Containers/WebPush/Create/utils/validation.js +17 -8
- package/v2Containers/WebPush/Create/utils/validation.test.js +44 -24
- package/v2Containers/Whatsapp/index.js +9 -17
- package/v2Containers/Zalo/index.js +3 -11
|
@@ -186,7 +186,6 @@ export const ErrorInfoNote = (props) => {
|
|
|
186
186
|
errorMessages,
|
|
187
187
|
onErrorClick,
|
|
188
188
|
onClose,
|
|
189
|
-
isLiquidEnabled = true,
|
|
190
189
|
intl,
|
|
191
190
|
useLegacyDisplay = false, // Use simple list display instead of tabs (for BEE Editor)
|
|
192
191
|
} = props;
|
|
@@ -230,7 +229,7 @@ export const ErrorInfoNote = (props) => {
|
|
|
230
229
|
const standardErrors = Array.isArray(rawStandardErrors) ? rawStandardErrors : [];
|
|
231
230
|
const liquidErrors = Array.isArray(rawLiquidErrors) ? rawLiquidErrors : [];
|
|
232
231
|
const hasStandardErrors = standardErrors.length > 0;
|
|
233
|
-
const hasLiquidErrors = liquidErrors.length > 0
|
|
232
|
+
const hasLiquidErrors = liquidErrors.length > 0;
|
|
234
233
|
|
|
235
234
|
if (!hasStandardErrors && !hasLiquidErrors) {
|
|
236
235
|
return null;
|
|
@@ -357,7 +356,7 @@ export const ErrorInfoNote = (props) => {
|
|
|
357
356
|
className="error-info-note__tabs"
|
|
358
357
|
/>
|
|
359
358
|
<CapRow className="error-info-note__actions">
|
|
360
|
-
{hasLiquidErrors &&
|
|
359
|
+
{hasLiquidErrors && (
|
|
361
360
|
<CapButton
|
|
362
361
|
type="flat"
|
|
363
362
|
className="error-info-note__liquid-doc"
|
|
@@ -452,7 +451,6 @@ ErrorInfoNote.defaultProps = {
|
|
|
452
451
|
},
|
|
453
452
|
onErrorClick: null,
|
|
454
453
|
onClose: null,
|
|
455
|
-
isLiquidEnabled: true,
|
|
456
454
|
intl: null,
|
|
457
455
|
useLegacyDisplay: false, // Use simple list display for BEE Editor
|
|
458
456
|
};
|
|
@@ -479,7 +477,6 @@ ErrorInfoNote.propTypes = {
|
|
|
479
477
|
}),
|
|
480
478
|
onErrorClick: PropTypes.func,
|
|
481
479
|
onClose: PropTypes.func,
|
|
482
|
-
isLiquidEnabled: PropTypes.bool,
|
|
483
480
|
intl: PropTypes.object,
|
|
484
481
|
useLegacyDisplay: PropTypes.bool, // Use simple list display for BEE Editor
|
|
485
482
|
};
|
|
@@ -50,7 +50,7 @@ import { makeSelectMetaEntities, selectCurrentOrgDetails, selectLiquidStateDetai
|
|
|
50
50
|
import * as actions from "../../v2Containers/Cap/actions";
|
|
51
51
|
import './_formBuilder.scss';
|
|
52
52
|
import {updateCharCount, checkUnicode} from "../../utils/smsCharCountV2";
|
|
53
|
-
import {
|
|
53
|
+
import { preprocessHtml, validateTagsCore, hasUnsubscribeTag } from '../../utils/tagValidations';
|
|
54
54
|
import { containsBase64Images } from '../../utils/content';
|
|
55
55
|
import { SMS, MOBILE_PUSH, LINE, ENABLE_AI_SUGGESTIONS,AI_CONTENT_BOT_DISABLED, EMAIL, LIQUID_SUPPORTED_CHANNELS, INAPP } from '../../v2Containers/CreativesContainer/constants';
|
|
56
56
|
import globalMessages from '../../v2Containers/Cap/messages';
|
|
@@ -60,7 +60,7 @@ import { GET_TRANSLATION_MAPPED } from '../../constants/unified';
|
|
|
60
60
|
import moment from 'moment';
|
|
61
61
|
import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS} from '../../constants/unified';
|
|
62
62
|
import { REQUEST } from '../../v2Containers/Cap/constants'
|
|
63
|
-
import {
|
|
63
|
+
import { isEmailUnsubscribeTagOptional } from '../../utils/common';
|
|
64
64
|
import { isUrl } from '../../v2Containers/Line/Container/Wrapper/utils';
|
|
65
65
|
import { bindActionCreators } from 'redux';
|
|
66
66
|
import { getChannelData, hasPersonalizationTags, validateLiquidTemplateContent, validateMobilePushContent } from '../../utils/commonUtils';
|
|
@@ -72,10 +72,8 @@ const {CapRadioGroup} = CapRadio;
|
|
|
72
72
|
|
|
73
73
|
const tagsTypes = {
|
|
74
74
|
MISSING_TAGS: 'missingTags',
|
|
75
|
-
UNSUPPORTED_TAGS: 'unsupportedTags',
|
|
76
75
|
};
|
|
77
76
|
const errorMessageForTags = {
|
|
78
|
-
UNSUPPORTED_TAG_ERROR: 'unsupportedTagsError',
|
|
79
77
|
MISSING_TAG_ERROR: 'missingTagsError',
|
|
80
78
|
GENERIC_VALIDATION_ERROR: 'genericValidationError',
|
|
81
79
|
TAG_BRACKET_COUNT_MISMATCH_ERROR: 'tagBracketCountMismatchError'
|
|
@@ -138,7 +136,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
138
136
|
this.handleSetRadioValue = this.handleSetRadioValue.bind(this);
|
|
139
137
|
this.formElements = [];
|
|
140
138
|
// Check if the liquid flow feature is supported and the channel is in the supported list.
|
|
141
|
-
this.
|
|
139
|
+
this.isLiquidFlowSupportedByChannel = this.isLiquidFlowSupportedByChannel.bind(this);
|
|
142
140
|
this.onSubmitWrapper = this.onSubmitWrapper.bind(this);
|
|
143
141
|
|
|
144
142
|
// Performance optimization: Debounced functions for high-frequency updates
|
|
@@ -330,8 +328,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
330
328
|
return updatedFormData;
|
|
331
329
|
}
|
|
332
330
|
|
|
333
|
-
|
|
334
|
-
return Boolean(LIQUID_SUPPORTED_CHANNELS.includes(this.props?.schema?.channel?.toUpperCase())
|
|
331
|
+
isLiquidFlowSupportedByChannel = () => {
|
|
332
|
+
return Boolean(LIQUID_SUPPORTED_CHANNELS.includes(this.props?.schema?.channel?.toUpperCase()));
|
|
335
333
|
}
|
|
336
334
|
|
|
337
335
|
componentWillMount() {
|
|
@@ -714,7 +712,9 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
714
712
|
if (channel && channel.toUpperCase() === SMS) {
|
|
715
713
|
for (let count = 0; count < this.state.tabCount; count += 1) {
|
|
716
714
|
if (_.isEmpty(errorData[count])) {
|
|
717
|
-
return
|
|
715
|
+
// Do not return early. An empty tab object can appear transiently and returning here
|
|
716
|
+
// prevents onFormValidityChange from firing, which makes Done appear unresponsive.
|
|
717
|
+
errorData[count] = {};
|
|
718
718
|
}
|
|
719
719
|
const index = count + 1;
|
|
720
720
|
if (!this.state.formData[count]) {
|
|
@@ -726,17 +726,19 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
726
726
|
|
|
727
727
|
let tagValidationResponse = false;
|
|
728
728
|
if (content) {
|
|
729
|
-
tagValidationResponse = this.validateTags(content, tags,
|
|
729
|
+
tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
|
|
730
730
|
}
|
|
731
|
-
|
|
732
|
-
|
|
731
|
+
|
|
732
|
+
const tagResult = tagValidationResponse && typeof tagValidationResponse === 'object'
|
|
733
|
+
? tagValidationResponse
|
|
734
|
+
: { valid: false, missingTags: [], isBraceError: false };
|
|
735
|
+
if (tagResult.valid) {
|
|
733
736
|
errorData[count][`sms-editor${index > 1 ? index : ''}`] = false;
|
|
734
737
|
} else {
|
|
735
|
-
|
|
736
|
-
const {
|
|
737
|
-
|
|
738
|
-
errorData[count][`
|
|
739
|
-
errorData[count][`bracket-error`] = tagValidationResponse.isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
|
|
738
|
+
const { MISSING_TAG_ERROR, GENERIC_VALIDATION_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags || {};
|
|
739
|
+
const { missingTags, isBraceError } = tagResult;
|
|
740
|
+
errorData[count][`sms-editor${index > 1 ? index : ''}`] = missingTags && missingTags.length ? MISSING_TAG_ERROR : (isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : GENERIC_VALIDATION_ERROR);
|
|
741
|
+
errorData[count][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
|
|
740
742
|
isValid = false;
|
|
741
743
|
}
|
|
742
744
|
if(content !== '' && (ifUnicode && !unicodeCheck)) {
|
|
@@ -764,7 +766,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
764
766
|
if (this.state.formData['message-editor'] !== undefined ) {
|
|
765
767
|
const content = this.state.formData['0']['message-editor'] || '';
|
|
766
768
|
|
|
767
|
-
const tagValidationResponse = this.validateTags((content), tags,
|
|
769
|
+
const tagValidationResponse = this.validateTags((content), tags, false, this.props?.isFullMode);
|
|
768
770
|
|
|
769
771
|
if (tagValidationResponse.valid) {
|
|
770
772
|
errorData = {
|
|
@@ -847,7 +849,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
847
849
|
errorData[index] = true;
|
|
848
850
|
isValid = false;
|
|
849
851
|
} else {
|
|
850
|
-
const tagValidationResponse = this.validateTags(content, tags,
|
|
852
|
+
const tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
|
|
851
853
|
|
|
852
854
|
if (tagValidationResponse.valid) {
|
|
853
855
|
errorData[index] = false;
|
|
@@ -913,14 +915,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
913
915
|
isCurrentTabValid = false;
|
|
914
916
|
} else {
|
|
915
917
|
errorData[parseInt(index)][`message-editor${selector}`] = false;
|
|
916
|
-
const tagValidationResponse = this.validateTags(message, tags,
|
|
918
|
+
const tagValidationResponse = this.validateTags(message, tags, false, this.props?.isFullMode);
|
|
917
919
|
|
|
918
920
|
if (tagValidationResponse.valid) {
|
|
919
921
|
errorData[parseInt(index)][`message-editor${selector}`] = false;
|
|
920
922
|
} else {
|
|
921
|
-
const {isBraceError} = tagValidationResponse;
|
|
923
|
+
const { isBraceError } = tagValidationResponse;
|
|
922
924
|
errorData[parseInt(index)][`message-editor${selector}`] = isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : true;
|
|
923
|
-
errorData[parseInt(index)]['invalid-tags'] = tagValidationResponse.unsupportedTags;
|
|
924
925
|
errorData[parseInt(index)][`bracket-error`] = isBraceError && TAG_BRACKET_COUNT_MISMATCH_ERROR;
|
|
925
926
|
isValid = false;
|
|
926
927
|
}
|
|
@@ -946,7 +947,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
946
947
|
isCurrentTabValid = false;
|
|
947
948
|
} else {
|
|
948
949
|
errorData[parseInt(index)][`message-title${selector}`] = false;
|
|
949
|
-
const tagValidationResponse = this.validateTags(title, tags,
|
|
950
|
+
const tagValidationResponse = this.validateTags(title, tags, false, this.props?.isFullMode);
|
|
950
951
|
|
|
951
952
|
if (tagValidationResponse.valid) {
|
|
952
953
|
errorData[parseInt(index)][`message-title${selector}`] = false;
|
|
@@ -1200,8 +1201,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1200
1201
|
if (!content) {
|
|
1201
1202
|
return false;
|
|
1202
1203
|
}
|
|
1203
|
-
const tagValidationResponse = this.validateTags(content, tags,
|
|
1204
|
-
|
|
1204
|
+
const tagValidationResponse = this.validateTags(content, tags, isEmail, this.props?.isFullMode);
|
|
1205
1205
|
// Check for base64 images in email content
|
|
1206
1206
|
isEmail && containsBase64Images({content, callback:()=>{
|
|
1207
1207
|
tagValidationResponse.valid = false;
|
|
@@ -1217,23 +1217,20 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1217
1217
|
errorData[index][currentLang]['template-content'] = true;
|
|
1218
1218
|
isValid = false;
|
|
1219
1219
|
isLiquidValid = false;
|
|
1220
|
-
if ((showMessages && !isNaN(index)) || this.
|
|
1221
|
-
if (tagValidationResponse?.missingTags?.length > 0
|
|
1220
|
+
if ((showMessages && !isNaN(index)) || this.isLiquidFlowSupportedByChannel()) {
|
|
1221
|
+
if (tagValidationResponse?.missingTags?.length > 0) {
|
|
1222
1222
|
errorString += `${this.props.intl.formatMessage(messages.contentNotValidLanguage)} ${currentLang}\n`;
|
|
1223
1223
|
}
|
|
1224
1224
|
if (tagValidationResponse?.missingTags?.length > 0) {
|
|
1225
1225
|
errorString += `${this.props.intl.formatMessage(messages.missingTags)} ${tagValidationResponse.missingTags.toString()}\n`;
|
|
1226
1226
|
}
|
|
1227
|
-
if (tagValidationResponse?.unsupportedTags?.length > 0) {
|
|
1228
|
-
errorString += `${this.props.intl.formatMessage(messages.unsupportedTags)} ${tagValidationResponse.unsupportedTags.toString()}\n`;
|
|
1229
|
-
}
|
|
1230
1227
|
if (tagValidationResponse?.isBraceError){
|
|
1231
1228
|
errorString += this.props.intl.formatMessage(globalMessages.unbalanacedCurlyBraces);
|
|
1232
1229
|
}
|
|
1233
1230
|
if (tagValidationResponse?.isContentEmpty) {
|
|
1234
1231
|
errorString += this.props.intl.formatMessage(messages.emailBodyEmptyError);
|
|
1235
1232
|
// Adds a bypass for cases where content is initially empty in the creation flow.
|
|
1236
|
-
if(this.
|
|
1233
|
+
if(this.isLiquidFlowSupportedByChannel()){
|
|
1237
1234
|
errorString = "";
|
|
1238
1235
|
isLiquidValid = true;
|
|
1239
1236
|
}
|
|
@@ -1245,7 +1242,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1245
1242
|
}
|
|
1246
1243
|
}
|
|
1247
1244
|
if (errorString) {
|
|
1248
|
-
if (this.
|
|
1245
|
+
if (this.isLiquidFlowSupportedByChannel()) {
|
|
1249
1246
|
this.setState(
|
|
1250
1247
|
(prevState) => ({
|
|
1251
1248
|
liquidErrorMessage: {
|
|
@@ -1257,8 +1254,11 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1257
1254
|
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
|
|
1258
1255
|
}
|
|
1259
1256
|
);
|
|
1260
|
-
//
|
|
1261
|
-
this.
|
|
1257
|
+
// Footer shows the error; skip notification for Email CK/BEE (non-HTML) flow to avoid duplicate feedback
|
|
1258
|
+
const isEmailChannel = this.props?.schema?.channel?.toUpperCase() === EMAIL;
|
|
1259
|
+
if (!isEmailChannel) {
|
|
1260
|
+
this.openNotificationWithIcon('error', errorString, 'email-validation-error');
|
|
1261
|
+
}
|
|
1262
1262
|
} else {
|
|
1263
1263
|
this.openNotificationWithIcon('error', errorString, "email-validation-error");
|
|
1264
1264
|
}
|
|
@@ -1272,7 +1272,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1272
1272
|
});
|
|
1273
1273
|
}
|
|
1274
1274
|
|
|
1275
|
-
const isTemplateValid = this.
|
|
1275
|
+
const isTemplateValid = this.isLiquidFlowSupportedByChannel() ? isLiquidValid : isValid;
|
|
1276
1276
|
//Updating the state with the error data
|
|
1277
1277
|
this.setState((prevState) => ({
|
|
1278
1278
|
isFormValid: isTemplateValid,
|
|
@@ -1331,55 +1331,59 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1331
1331
|
}
|
|
1332
1332
|
onSubmitWrapper = (args) => {
|
|
1333
1333
|
const {singleTab = null} = args || {};
|
|
1334
|
-
|
|
1334
|
+
// Liquid validation (extractTags + Aira) only in library mode
|
|
1335
|
+
const runLiquidValidation = this.isLiquidFlowSupportedByChannel() && !this.props.isFullMode;
|
|
1336
|
+
if (runLiquidValidation) {
|
|
1335
1337
|
// For MPUSH, we need to validate both Android and iOS content separately
|
|
1336
1338
|
if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
|
|
1337
1339
|
this.validateFormBuilderMPush(this.state.formData, singleTab);
|
|
1338
1340
|
return;
|
|
1339
1341
|
}
|
|
1340
|
-
|
|
1341
|
-
// For other channels (EMAIL, SMS, INAPP)
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
(prevState) => ({
|
|
1348
|
-
liquidErrorMessage: {
|
|
1349
|
-
...prevState.liquidErrorMessage,
|
|
1350
|
-
STANDARD_ERROR_MSG: standardErrors,
|
|
1351
|
-
LIQUID_ERROR_MSG: liquidErrors,
|
|
1352
|
-
},
|
|
1353
|
-
}),
|
|
1354
|
-
() => {
|
|
1355
|
-
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage);
|
|
1356
|
-
this.props.stopValidation();
|
|
1357
|
-
}
|
|
1358
|
-
);
|
|
1359
|
-
};
|
|
1360
|
-
|
|
1361
|
-
const onSuccess = (contentToSubmit) => {
|
|
1362
|
-
const channel = this.props.channel || this.props?.schema?.channel?.toUpperCase();
|
|
1363
|
-
if(channel === EMAIL) {
|
|
1364
|
-
const content = this.state.formData?.base?.[this.props.baseLanguage]?.["template-content"] || "";
|
|
1365
|
-
this.handleLiquidTemplateSubmit(content);
|
|
1366
|
-
} else {
|
|
1367
|
-
this.handleLiquidTemplateSubmit(contentToSubmit);
|
|
1342
|
+
|
|
1343
|
+
// For other channels (EMAIL, SMS, INAPP): only call extractTags if there are no brace/empty errors already.
|
|
1344
|
+
// Run sync validation first; if it fails, block and show errors without calling the API.
|
|
1345
|
+
this.validateForm(null, null, true, false, () => {
|
|
1346
|
+
if (!this.state.isFormValid) {
|
|
1347
|
+
this.props.stopValidation();
|
|
1348
|
+
return;
|
|
1368
1349
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1350
|
+
const content = getChannelData(this.props.schema.channel || this.props.channel, this.state.formData, this.props.baseLanguage);
|
|
1351
|
+
|
|
1352
|
+
const onError = ({ standardErrors, liquidErrors }) => {
|
|
1353
|
+
this.setState(
|
|
1354
|
+
(prevState) => ({
|
|
1355
|
+
liquidErrorMessage: {
|
|
1356
|
+
...prevState.liquidErrorMessage,
|
|
1357
|
+
STANDARD_ERROR_MSG: standardErrors,
|
|
1358
|
+
LIQUID_ERROR_MSG: liquidErrors,
|
|
1359
|
+
},
|
|
1360
|
+
}),
|
|
1361
|
+
() => {
|
|
1362
|
+
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage);
|
|
1363
|
+
this.props.stopValidation();
|
|
1364
|
+
this.props.onFormValidityChange(false, this.state.errorData);
|
|
1365
|
+
}
|
|
1366
|
+
);
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
const onSuccess = (contentToSubmit) => {
|
|
1370
|
+
const channel = this.props.channel || this.props?.schema?.channel?.toUpperCase();
|
|
1371
|
+
if(channel === EMAIL) {
|
|
1372
|
+
const content = this.state.formData?.base?.[this.props.baseLanguage]?.["template-content"] || "";
|
|
1373
|
+
this.handleLiquidTemplateSubmit(content);
|
|
1374
|
+
} else {
|
|
1375
|
+
this.handleLiquidTemplateSubmit(contentToSubmit);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
validateLiquidTemplateContent(content, {
|
|
1380
|
+
getLiquidTags: this.props.actions.getLiquidTags,
|
|
1381
|
+
formatMessage: this.props.intl.formatMessage,
|
|
1382
|
+
messages,
|
|
1383
|
+
onError,
|
|
1384
|
+
onSuccess,
|
|
1385
|
+
skipTags: this.skipTags.bind(this)
|
|
1386
|
+
});
|
|
1383
1387
|
});
|
|
1384
1388
|
} else {
|
|
1385
1389
|
this.props.onSubmit(this.state.formData);
|
|
@@ -1406,7 +1410,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1406
1410
|
|
|
1407
1411
|
// Set up callbacks for error and success handling
|
|
1408
1412
|
const onLiquidError = ({ standardErrors, liquidErrors }) => {
|
|
1409
|
-
|
|
1410
1413
|
this.setState(
|
|
1411
1414
|
(prevState) => ({
|
|
1412
1415
|
liquidErrorMessage: {
|
|
@@ -1418,6 +1421,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1418
1421
|
() => {
|
|
1419
1422
|
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.state.currentTab);
|
|
1420
1423
|
this.props.stopValidation();
|
|
1424
|
+
// Block save: tell parent form is invalid so Done/submit is blocked
|
|
1425
|
+
this.props.onFormValidityChange(false, this.state.errorData);
|
|
1421
1426
|
}
|
|
1422
1427
|
);
|
|
1423
1428
|
};
|
|
@@ -1447,13 +1452,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1447
1452
|
getLiquidTags: this.props.actions.getLiquidTags,
|
|
1448
1453
|
formatMessage: this.props.intl.formatMessage,
|
|
1449
1454
|
messages: messages,
|
|
1450
|
-
tagLookupMap: this.props?.metaEntities?.tagLookupMap,
|
|
1451
|
-
eventContextTags: this.props?.eventContextTags,
|
|
1452
|
-
isLiquidFlow: this.liquidFlow(), // Use the method instead of props
|
|
1453
|
-
forwardedTags: this.props?.isLoyaltyModule ? this.props?.forwardedTags : {},
|
|
1454
|
-
skipTags: this.skipTags.bind(this),
|
|
1455
|
-
extractNames,
|
|
1456
|
-
checkSupport,
|
|
1457
1455
|
singleTab: singleTab?.toUpperCase(),
|
|
1458
1456
|
});
|
|
1459
1457
|
}
|
|
@@ -1508,115 +1506,40 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1508
1506
|
});
|
|
1509
1507
|
}
|
|
1510
1508
|
|
|
1511
|
-
validateTags(content, tagsParam,
|
|
1512
|
-
const type = (this.props.location && this.props.location.query.type) ? this.props.location.query.type : 'full';
|
|
1509
|
+
validateTags(content, tagsParam, isEmail = false, isFullMode = this.props?.isFullMode) {
|
|
1513
1510
|
let currentModule = this.props.location.query.module ? this.props.location.query.module : 'default';
|
|
1514
1511
|
if (this.props.tagModule) {
|
|
1515
1512
|
currentModule = this.props.tagModule;
|
|
1516
1513
|
}
|
|
1517
1514
|
const tags = tagsParam ? tagsParam : this.props.tags;
|
|
1518
|
-
const
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
response
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
if (tags && tags.length && !isFullMode) {
|
|
1536
|
-
_.forEach(tags, (tag) => {
|
|
1537
|
-
_.forEach(tag.definition.supportedModules, (module) => {
|
|
1538
|
-
if (module.mandatory && (currentModule === module.context)) {
|
|
1539
|
-
if (content.indexOf(`{{${tag.definition.value}}}`) === -1) {
|
|
1540
|
-
response.valid = false;
|
|
1541
|
-
response.missingTags.push(tag.definition.value);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
});
|
|
1545
|
-
});
|
|
1546
|
-
// Legacy Email (CK Editor): when unsubscribe is required, ensure we validate it even if tag schema didn't mark it mandatory for this module
|
|
1547
|
-
if (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
|
|
1548
|
-
const hasUnsubscribeInContent = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g.test(content);
|
|
1549
|
-
if (!hasUnsubscribeInContent && response.missingTags.indexOf('unsubscribe') === -1) {
|
|
1550
|
-
response.valid = false;
|
|
1551
|
-
response.missingTags.push('unsubscribe');
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
|
|
1555
|
-
let match = regex.exec(content);
|
|
1556
|
-
const regexImgSrc=/<img[^>]*\bsrc\s*=\s*"[^"]*{{customer_barcode}}[^"]*"/;
|
|
1557
|
-
let matchImg = regexImgSrc.exec(content);
|
|
1558
|
-
const regexCustomerBarcode = /{{customer_barcode}}(?![^<]*>)/g;
|
|
1559
|
-
let matchCustomerBarcode = regexCustomerBarcode.exec(content);
|
|
1560
|
-
// \S matches anything other than a space, a tab, a newline, or a carriage return.
|
|
1561
|
-
const validString= /\S/.test(contentForValidation);
|
|
1562
|
-
if (isEmailUnsubscribeTagMandatory() && isEmail && isModuleTypeOutbound) {
|
|
1563
|
-
const missingTagIndex = response?.missingTags?.indexOf("unsubscribe");
|
|
1564
|
-
if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
|
|
1565
|
-
response?.missingTags?.splice(missingTagIndex, 1);
|
|
1566
|
-
}
|
|
1567
|
-
if (validString) {
|
|
1568
|
-
response.valid = true;
|
|
1569
|
-
} else {
|
|
1570
|
-
response.isContentEmpty = true;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
while (match !== null ) {
|
|
1574
|
-
const tagValue = match[0].substring(this.indexOfEnd(match[0], '{{'), match[0].indexOf('}}'));
|
|
1575
|
-
const tagIndex = match?.index;
|
|
1576
|
-
match = regex.exec(content);
|
|
1577
|
-
let ifSupported = false;
|
|
1578
|
-
_.forEach(tags, (tag) => {
|
|
1579
|
-
if (tag.definition.value === tagValue) {
|
|
1580
|
-
ifSupported = true;
|
|
1581
|
-
}
|
|
1582
|
-
if(tagValue === CUSTOMER_BARCODE_TAG && (matchImg === null || matchCustomerBarcode !== null)){
|
|
1583
|
-
ifSupported = false;
|
|
1584
|
-
}
|
|
1585
|
-
});
|
|
1586
|
-
const ifSkipped = this.skipTags(tagValue);
|
|
1587
|
-
if (ifSkipped) {
|
|
1588
|
-
ifSupported = true;
|
|
1589
|
-
let isUnsubscribeSkipped = tagValue.indexOf("unsubscribe") != -1 ;
|
|
1590
|
-
if (isUnsubscribeSkipped) {
|
|
1591
|
-
const missingTagIndex = response.missingTags.indexOf("unsubscribe");
|
|
1592
|
-
if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
|
|
1593
|
-
response.missingTags.splice(missingTagIndex, 1);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
// Event Context Tags support
|
|
1599
|
-
this.props?.eventContextTags?.forEach((tag) => {
|
|
1600
|
-
if (tagValue === tag?.tagName) {
|
|
1601
|
-
ifSupported = true;
|
|
1602
|
-
}
|
|
1603
|
-
});
|
|
1604
|
-
|
|
1605
|
-
ifSupported = ifSupported || this.checkIfSupportedTag(tagValue, injectedTags);
|
|
1606
|
-
// Only add to unsupportedTags if not inside a {% ... %} block (scenario 3: liquid orgs also get unsupported-tag errors)
|
|
1607
|
-
if (!ifSupported && !isInsideLiquidBlock(content, tagIndex)) {
|
|
1608
|
-
response.unsupportedTags.push(tagValue);
|
|
1609
|
-
response.valid = false;
|
|
1610
|
-
}
|
|
1515
|
+
const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content;
|
|
1516
|
+
const isOutboundModule = (currentModule || '').toUpperCase() === OUTBOUND;
|
|
1517
|
+
|
|
1518
|
+
const initialMissingTags = (tags && tags.length && !isFullMode && isEmail && isOutboundModule && !isEmailUnsubscribeTagOptional() && !hasUnsubscribeTag(content))
|
|
1519
|
+
? ['unsubscribe']
|
|
1520
|
+
: [];
|
|
1521
|
+
|
|
1522
|
+
const response = validateTagsCore({
|
|
1523
|
+
contentForBraceCheck: contentForValidation,
|
|
1524
|
+
contentForUnsubscribeScan: content,
|
|
1525
|
+
tags,
|
|
1526
|
+
currentModule,
|
|
1527
|
+
isFullMode,
|
|
1528
|
+
initialMissingTags, // [] or ['unsubscribe']; core uses this instead of definition-based when provided
|
|
1529
|
+
skipTagsFn: this.skipTags.bind(this),
|
|
1530
|
+
includeIsContentEmpty: true,
|
|
1531
|
+
});
|
|
1611
1532
|
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1533
|
+
// When unsubscribe tag is optional (isEmailUnsubscribeTagOptional): do not enforce unsubscribe (defensive splice); set isContentEmpty only when content is empty. Do not override response.valid so brace/format errors are preserved.
|
|
1534
|
+
const validString = /\S/.test(contentForValidation);
|
|
1535
|
+
if (isEmailUnsubscribeTagOptional() && isEmail && isOutboundModule) {
|
|
1536
|
+
const missingTagIndex = response.missingTags.indexOf('unsubscribe');
|
|
1537
|
+
if (missingTagIndex !== -1) {
|
|
1538
|
+
response.missingTags.splice(missingTagIndex, 1);
|
|
1539
|
+
}
|
|
1540
|
+
if (!validString) {
|
|
1541
|
+
response.isContentEmpty = true;
|
|
1615
1542
|
}
|
|
1616
|
-
}
|
|
1617
|
-
if(!validateIfTagClosed(contentForValidation)){
|
|
1618
|
-
response.isBraceError = true;
|
|
1619
|
-
response.valid = false;
|
|
1620
1543
|
}
|
|
1621
1544
|
return response;
|
|
1622
1545
|
}
|
|
@@ -2850,21 +2773,21 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2850
2773
|
|
|
2851
2774
|
|
|
2852
2775
|
getMissingOrUnsupportedTagsName = (content = '', type) => {
|
|
2853
|
-
const { MISSING_TAGS
|
|
2776
|
+
const { MISSING_TAGS } = tagsTypes;
|
|
2854
2777
|
const tagValidationResponse = this.validateTags(content);
|
|
2855
|
-
if (type &&
|
|
2856
|
-
return tagValidationResponse[type].join(', ').toString();
|
|
2778
|
+
if (type && type === MISSING_TAGS) {
|
|
2779
|
+
return (tagValidationResponse[type] || []).join(', ').toString();
|
|
2857
2780
|
}
|
|
2858
2781
|
return null;
|
|
2859
2782
|
};
|
|
2860
2783
|
|
|
2861
2784
|
renderTextAreaContent = (styling, columns, val, isVersionEnable, rows, cols, offset = 0) => {
|
|
2862
2785
|
const { checkValidation, errorData, currentTab, formData } = this.state;
|
|
2863
|
-
const { MISSING_TAGS
|
|
2786
|
+
const { MISSING_TAGS } = tagsTypes;
|
|
2864
2787
|
const errorType = (isVersionEnable ? errorData[`${currentTab - 1}`][val.id] : errorData[val.id]);
|
|
2865
2788
|
const ifError = checkValidation && errorType;
|
|
2866
2789
|
const messageContent = isVersionEnable ? formData[`${currentTab - 1}`][val.id] : formData[val.id];
|
|
2867
|
-
const { MISSING_TAG_ERROR,
|
|
2790
|
+
const { MISSING_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
|
|
2868
2791
|
const { formatMessage } = this.props.intl;
|
|
2869
2792
|
|
|
2870
2793
|
const { accessibleFeatures = [] } = this.props.currentOrgDetails || {};
|
|
@@ -2877,9 +2800,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2877
2800
|
case MISSING_TAG_ERROR:
|
|
2878
2801
|
errorMessageText = formatMessage(messages.missingTagsValidationError, {missingTags: this.getMissingOrUnsupportedTagsName(messageContent, MISSING_TAGS)});
|
|
2879
2802
|
break;
|
|
2880
|
-
case UNSUPPORTED_TAG_ERROR:
|
|
2881
|
-
errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags: this.getMissingOrUnsupportedTagsName(messageContent, UNSUPPORTED_TAGS)});
|
|
2882
|
-
break;
|
|
2883
2803
|
case TAG_BRACKET_COUNT_MISMATCH_ERROR:
|
|
2884
2804
|
errorMessageText = formatMessage(globalMessages.unbalanacedCurlyBraces);
|
|
2885
2805
|
break;
|
|
@@ -2893,8 +2813,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2893
2813
|
if (this.props.restrictPersonalization && hasPersonalizationTags(messageContent)) {
|
|
2894
2814
|
errorMessageText = formatMessage(messages.personalizationTagsErrorMessage);
|
|
2895
2815
|
}
|
|
2816
|
+
// Empty/required error: only show after user has triggered validation (ifError / "Done").
|
|
2817
|
+
// All other errors (brace, personalization, missing tags, generic): show in real time while typing.
|
|
2818
|
+
const isContentEmpty = !messageContent || !/\S/.test(String(messageContent).trim());
|
|
2819
|
+
const isEmptyError = errorType && isContentEmpty;
|
|
2820
|
+
const showError = errorType && (isEmptyError ? ifError : true);
|
|
2896
2821
|
const prevErrorMessage = this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.[0];
|
|
2897
|
-
if (prevErrorMessage !== errorMessageText && errorMessageText && this.
|
|
2822
|
+
if (prevErrorMessage !== errorMessageText && errorMessageText && this.isLiquidFlowSupportedByChannel()) {
|
|
2898
2823
|
this.setState((prevState) => ({
|
|
2899
2824
|
liquidErrorMessage: {
|
|
2900
2825
|
...prevState.liquidErrorMessage,
|
|
@@ -2905,15 +2830,25 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2905
2830
|
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
|
|
2906
2831
|
});
|
|
2907
2832
|
}
|
|
2908
|
-
|
|
2833
|
+
// Always render the textarea regardless of the liquid error state update above.
|
|
2834
|
+
// Previously the textarea was inside the `else` branch, so it was not rendered during the
|
|
2835
|
+
// one render cycle when the liquid error message was first set – causing the input to lose
|
|
2836
|
+
// focus whenever a brace/tag error first appeared.
|
|
2909
2837
|
if (styling === 'semantic') {
|
|
2910
2838
|
columns.push(
|
|
2911
2839
|
<CapColumn key="input" span={val.width} offset={offset}>
|
|
2912
2840
|
<TextArea
|
|
2913
2841
|
id={val.id}
|
|
2914
2842
|
placeholder={val.placeholder ? val.placeholder : ''}
|
|
2915
|
-
className={`${
|
|
2916
|
-
errorMessage={
|
|
2843
|
+
className={`${showError ? 'error-form-builder' : ''}`}
|
|
2844
|
+
errorMessage={
|
|
2845
|
+
showError && errorMessageText && (
|
|
2846
|
+
!this.isLiquidFlowSupportedByChannel() ||
|
|
2847
|
+
[MOBILE_PUSH, INAPP].includes(this.props.schema?.channel?.toUpperCase())
|
|
2848
|
+
)
|
|
2849
|
+
? errorMessageText
|
|
2850
|
+
: ''
|
|
2851
|
+
}
|
|
2917
2852
|
label={val.label}
|
|
2918
2853
|
autosize={val.autosize ? val.autosizeParams : false}
|
|
2919
2854
|
onChange={(e) => this.updateFormData(e.target.value, val)}
|
|
@@ -2944,7 +2879,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2944
2879
|
</CapColumn>
|
|
2945
2880
|
);
|
|
2946
2881
|
}
|
|
2947
|
-
}
|
|
2948
2882
|
};
|
|
2949
2883
|
|
|
2950
2884
|
|
|
@@ -4007,7 +3941,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
4007
3941
|
<CapColumn
|
|
4008
3942
|
style={val.colStyle ? val.colStyle : {border : ""}}
|
|
4009
3943
|
span={val.width}
|
|
4010
|
-
className={
|
|
3944
|
+
className={(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.isLiquidFlowSupportedByChannel() ? "error-boundary" : ""}
|
|
4011
3945
|
>
|
|
4012
3946
|
<CKEditor
|
|
4013
3947
|
id={val.id}
|
|
@@ -4051,7 +3985,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
4051
3985
|
isModuleFilterEnabled = this.props.isFullMode;
|
|
4052
3986
|
}
|
|
4053
3987
|
columns.push(
|
|
4054
|
-
<CapColumn style={val.colStyle ? val.colStyle : {}} span={val.width} className={
|
|
3988
|
+
<CapColumn style={val.colStyle ? val.colStyle : {}} span={val.width} className={(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.isLiquidFlowSupportedByChannel() ? "error-boundary" : ""}>
|
|
4055
3989
|
<BeeEditor
|
|
4056
3990
|
uid={uuid}
|
|
4057
3991
|
tokenData={beeToken}
|
|
@@ -4337,7 +4271,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
4337
4271
|
|
|
4338
4272
|
|
|
4339
4273
|
return (
|
|
4340
|
-
<CapSpin spinning={Boolean((this.
|
|
4274
|
+
<CapSpin spinning={Boolean((this.isLiquidFlowSupportedByChannel() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
|
|
4341
4275
|
<CapRow>
|
|
4342
4276
|
{this.props.schema && this.renderForm()}
|
|
4343
4277
|
<SlideBox
|
|
@@ -26,10 +26,6 @@ export default defineMessages({
|
|
|
26
26
|
id: 'creatives.components.FormBuilder.missingTagsValidationError',
|
|
27
27
|
defaultMessage: 'Missing tags: {missingTags}. Please add them to this message.',
|
|
28
28
|
},
|
|
29
|
-
unsupportedTagsValidationError: {
|
|
30
|
-
id: 'creatives.components.FormBuilder.unsupportedTagsValidationError',
|
|
31
|
-
defaultMessage: 'Unsupported tags: {unsupportedTags}. Please remove them from this message.',
|
|
32
|
-
},
|
|
33
29
|
genericTagsValidationError: {
|
|
34
30
|
id: 'creatives.components.FormBuilder.genericTagsValidationError',
|
|
35
31
|
defaultMessage: 'Please check the message content for unsupported/missing tags',
|
|
@@ -42,10 +38,6 @@ export default defineMessages({
|
|
|
42
38
|
id: 'creatives.componentsV2.FormBuilder.missingTags',
|
|
43
39
|
defaultMessage: 'Missing tags are:',
|
|
44
40
|
},
|
|
45
|
-
unsupportedTags: {
|
|
46
|
-
id: 'creatives.componentsV2.FormBuilder.unsupportedTags',
|
|
47
|
-
defaultMessage: 'Unsupported tags are:',
|
|
48
|
-
},
|
|
49
41
|
upload: {
|
|
50
42
|
id: 'creatives.componentsV2.FormBuilder.upload',
|
|
51
43
|
defaultMessage: 'Upload',
|