@capillarytech/creatives-library 8.0.310 → 8.0.311

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.
Files changed (54) hide show
  1. package/constants/unified.js +5 -1
  2. package/initialState.js +0 -2
  3. package/package.json +1 -1
  4. package/utils/common.js +5 -8
  5. package/utils/commonUtils.js +36 -93
  6. package/utils/tagValidations.js +83 -223
  7. package/utils/tests/commonUtil.test.js +147 -124
  8. package/utils/tests/tagValidations.test.js +441 -358
  9. package/v2Components/ErrorInfoNote/index.js +2 -5
  10. package/v2Components/FormBuilder/index.js +137 -203
  11. package/v2Components/FormBuilder/messages.js +0 -8
  12. package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
  15. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
  16. package/v2Containers/Cap/mockData.js +0 -14
  17. package/v2Containers/Cap/reducer.js +3 -55
  18. package/v2Containers/Cap/tests/reducer.test.js +0 -102
  19. package/v2Containers/CreativesContainer/SlideBoxContent.js +5 -1
  20. package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -5
  21. package/v2Containers/CreativesContainer/constants.js +6 -0
  22. package/v2Containers/CreativesContainer/index.js +47 -7
  23. package/v2Containers/Email/index.js +1 -5
  24. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +23 -70
  25. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -120
  26. package/v2Containers/FTP/index.js +2 -51
  27. package/v2Containers/FTP/messages.js +0 -4
  28. package/v2Containers/InApp/index.js +35 -107
  29. package/v2Containers/InApp/tests/index.test.js +17 -6
  30. package/v2Containers/InappAdvance/index.js +4 -112
  31. package/v2Containers/InappAdvance/tests/index.test.js +2 -0
  32. package/v2Containers/Line/Container/Text/index.js +0 -1
  33. package/v2Containers/MobilePush/Create/index.js +59 -19
  34. package/v2Containers/MobilePush/Edit/index.js +48 -20
  35. package/v2Containers/MobilePushNew/index.js +12 -32
  36. package/v2Containers/MobilepushWrapper/index.js +3 -1
  37. package/v2Containers/Rcs/index.js +12 -37
  38. package/v2Containers/Sms/Create/index.js +39 -3
  39. package/v2Containers/Sms/Create/messages.js +4 -0
  40. package/v2Containers/Sms/Edit/index.js +35 -3
  41. package/v2Containers/Sms/commonMethods.js +3 -6
  42. package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
  43. package/v2Containers/SmsTrai/Edit/index.js +11 -47
  44. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  45. package/v2Containers/SmsWrapper/index.js +2 -0
  46. package/v2Containers/TemplatesV2/index.js +28 -13
  47. package/v2Containers/Viber/index.js +0 -1
  48. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
  49. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  50. package/v2Containers/WebPush/Create/index.js +2 -2
  51. package/v2Containers/WebPush/Create/utils/validation.js +17 -8
  52. package/v2Containers/WebPush/Create/utils/validation.test.js +44 -24
  53. package/v2Containers/Whatsapp/index.js +9 -17
  54. 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 && isLiquidEnabled;
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 && isLiquidEnabled && (
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 { checkSupport, extractNames, preprocessHtml, validateIfTagClosed, isInsideLiquidBlock} from '../../utils/tagValidations';
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 { hasLiquidSupportFeature, isEmailUnsubscribeTagMandatory } from '../../utils/common';
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.liquidFlow = this.isLiquidFlowSupported.bind(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
- isLiquidFlowSupported = () => {
334
- return Boolean(LIQUID_SUPPORTED_CHANNELS.includes(this.props?.schema?.channel?.toUpperCase()) && hasLiquidSupportFeature());
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, injectedTags, false, this.props?.isFullMode);
729
+ tagValidationResponse = this.validateTags(content, tags, false, this.props?.isFullMode);
730
730
  }
731
-
732
- if (tagValidationResponse.valid) {
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
- errorData[count]['invalid-tags'] = tagValidationResponse.unsupportedTags;
736
- const { MISSING_TAG_ERROR, UNSUPPORTED_TAG_ERROR, GENERIC_VALIDATION_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags || {};
737
- const { missingTags, unsupportedTags, isBraceError} = tagValidationResponse;
738
- errorData[count][`sms-editor${index > 1 ? index : ''}`] = missingTags && missingTags.length ? MISSING_TAG_ERROR : ( unsupportedTags && unsupportedTags.length ? UNSUPPORTED_TAG_ERROR : (isBraceError ? TAG_BRACKET_COUNT_MISMATCH_ERROR : GENERIC_VALIDATION_ERROR));
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, injectedTags, false, this.props?.isFullMode);
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, injectedTags, false, this.props?.isFullMode);
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, injectedTags, false, this.props?.isFullMode);
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, injectedTags, false, this.props?.isFullMode);
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, injectedTags, isEmail, this.props?.isFullMode);
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.liquidFlow()) {
1221
- if (tagValidationResponse?.missingTags?.length > 0 || tagValidationResponse?.unsupportedTags?.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.liquidFlow()){
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.liquidFlow()) {
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
- // Show toast for liquid flow too so user sees error (scenario 3)
1261
- this.openNotificationWithIcon('error', errorString, "email-validation-error");
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.liquidFlow() ? isLiquidValid : isValid;
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
- if (this.liquidFlow() && !this.props?.isFullMode) {
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
- const content = getChannelData(this.props.schema.channel || this.props.channel, this.state.formData, this.props.baseLanguage);
1343
-
1344
- // Set up callbacks for error and success handling
1345
- const onError = ({ standardErrors, liquidErrors }) => {
1346
- this.setState(
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
- // Call the common validation function
1372
- validateLiquidTemplateContent(content, {
1373
- getLiquidTags: this.props.actions.getLiquidTags,
1374
- formatMessage: this.props.intl.formatMessage,
1375
- messages,
1376
- onError,
1377
- onSuccess,
1378
- tagLookupMap: this.props?.metaEntities?.tagLookupMap,
1379
- eventContextTags: this.props?.eventContextTags,
1380
- isLiquidFlow: this.liquidFlow(),
1381
- forwardedTags: this.props?.isLoyaltyModule ? this.props?.forwardedTags : {},
1382
- skipTags: this.skipTags.bind(this)
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, injectedTagsParams, isEmail = false, isFullMode = this.props?.isFullMode) {
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 injectedTags = this.transformInjectedTags(injectedTagsParams ? injectedTagsParams : this.props.injectedTags);
1519
- const excludedTags = ['user_id_b64', 'outbox_id_b64'];
1520
-
1521
-
1522
- const response = {};
1523
- response.valid = true;
1524
- response.missingTags = [];
1525
- response.unsupportedTags = [];
1526
- response.isBraceError = false;
1527
- response.isContentEmpty = false;
1528
- const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content ;
1529
- const isModuleTypeOutbound = (this.props?.moduleType || '').toUpperCase() === OUTBOUND;
1530
- // Run tag validation (missing + unsupported): library mode, or full mode with liquid support, or
1531
- // legacy Email (CK Editor) when unsubscribe is required (EMAIL_UNSUBSCRIBE_TAG_MANDATORY false) so missing-unsubscribe error shows
1532
- const shouldRunTagValidation = !isFullMode
1533
- || (isEmail && hasLiquidSupportFeature())
1534
- || (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound);
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
- if (response?.unsupportedTags?.length == 0 && response?.missingTags?.length == 0 ) {
1613
- response.valid = true;
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, UNSUPPORTED_TAGS} = tagsTypes;
2776
+ const { MISSING_TAGS } = tagsTypes;
2854
2777
  const tagValidationResponse = this.validateTags(content);
2855
- if (type && (type === MISSING_TAGS || type === UNSUPPORTED_TAGS)) {
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, UNSUPPORTED_TAGS } = tagsTypes;
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, UNSUPPORTED_TAG_ERROR, TAG_BRACKET_COUNT_MISMATCH_ERROR } = errorMessageForTags;
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.liquidFlow()) {
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
- else{
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={`${ifError ? 'error-form-builder' : ''}`}
2916
- errorMessage={errorMessageText && !this.liquidFlow() ? errorMessageText : ''}
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={`${(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.liquidFlow() && "error-boundary"} `}
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={`${(this.state.liquidErrorMessage?.LIQUID_ERROR_MSG?.length || this.state.liquidErrorMessage?.STANDARD_ERROR_MSG?.length) && this.liquidFlow() && "error-boundary"}`}>
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.liquidFlow() && this.props.liquidExtractionInProgress) || this.props.metaDataStatus === REQUEST)} tip={this.props.intl.formatMessage(messages.liquidSpinText)} >
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',