@capillarytech/creatives-library 8.0.309 → 8.0.310

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 (79) hide show
  1. package/constants/unified.js +1 -5
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/services/api.js +0 -17
  5. package/services/tests/api.test.js +0 -85
  6. package/utils/common.js +8 -5
  7. package/utils/commonUtils.js +93 -46
  8. package/utils/tagValidations.js +223 -83
  9. package/utils/tests/commonUtil.test.js +124 -316
  10. package/utils/tests/tagValidations.test.js +358 -441
  11. package/v2Components/CommonTestAndPreview/SendTestMessage.js +49 -78
  12. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +34 -134
  13. package/v2Components/CommonTestAndPreview/actions.js +0 -10
  14. package/v2Components/CommonTestAndPreview/constants.js +1 -15
  15. package/v2Components/CommonTestAndPreview/index.js +19 -80
  16. package/v2Components/CommonTestAndPreview/messages.js +0 -94
  17. package/v2Components/CommonTestAndPreview/reducer.js +0 -10
  18. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +0 -53
  19. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -31
  20. package/v2Components/CommonTestAndPreview/tests/index.test.js +0 -36
  21. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +0 -71
  22. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +0 -377
  23. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +0 -17
  24. package/v2Components/ErrorInfoNote/index.js +5 -2
  25. package/v2Components/FormBuilder/index.js +203 -137
  26. package/v2Components/FormBuilder/messages.js +8 -0
  27. package/v2Components/HtmlEditor/HTMLEditor.js +5 -0
  28. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  29. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  30. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +2 -1
  31. package/v2Containers/Cap/mockData.js +14 -0
  32. package/v2Containers/Cap/reducer.js +55 -3
  33. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  34. package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -5
  35. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  36. package/v2Containers/CreativesContainer/constants.js +0 -6
  37. package/v2Containers/CreativesContainer/index.js +7 -47
  38. package/v2Containers/Email/index.js +5 -1
  39. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
  40. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +120 -20
  41. package/v2Containers/FTP/index.js +51 -2
  42. package/v2Containers/FTP/messages.js +4 -0
  43. package/v2Containers/InApp/index.js +107 -35
  44. package/v2Containers/InApp/tests/index.test.js +6 -17
  45. package/v2Containers/InappAdvance/index.js +112 -4
  46. package/v2Containers/InappAdvance/tests/index.test.js +0 -2
  47. package/v2Containers/Line/Container/Text/index.js +1 -0
  48. package/v2Containers/MobilePush/Create/index.js +19 -59
  49. package/v2Containers/MobilePush/Edit/index.js +20 -48
  50. package/v2Containers/MobilePushNew/index.js +32 -12
  51. package/v2Containers/MobilepushWrapper/index.js +1 -3
  52. package/v2Containers/Rcs/index.js +37 -12
  53. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1276 -1408
  54. package/v2Containers/Sms/Create/index.js +3 -39
  55. package/v2Containers/Sms/Create/messages.js +0 -4
  56. package/v2Containers/Sms/Edit/index.js +3 -35
  57. package/v2Containers/Sms/commonMethods.js +6 -3
  58. package/v2Containers/SmsTrai/Edit/index.js +47 -11
  59. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +294 -327
  60. package/v2Containers/SmsWrapper/index.js +0 -2
  61. package/v2Containers/TemplatesV2/index.js +13 -28
  62. package/v2Containers/Viber/index.js +1 -0
  63. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  64. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  65. package/v2Containers/WebPush/Create/index.js +2 -2
  66. package/v2Containers/WebPush/Create/utils/validation.js +8 -17
  67. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -44
  68. package/v2Containers/Whatsapp/index.js +17 -9
  69. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +4872 -5246
  70. package/v2Containers/Zalo/index.js +11 -3
  71. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +0 -42
  72. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +0 -284
  73. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +0 -72
  74. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +0 -66
  75. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +0 -657
  76. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +0 -172
  77. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +0 -466
  78. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +0 -114
  79. package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
@@ -15,7 +15,7 @@ import HTMLEditor from '../../../v2Components/HtmlEditor';
15
15
  import CapTagListWithInput from '../../../v2Components/CapTagListWithInput';
16
16
  import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
17
17
  import { validateLiquidTemplateContent } from '../../../utils/commonUtils';
18
- import { isEmailUnsubscribeTagOptional } from '../../../utils/common';
18
+ import { hasLiquidSupportFeature, isEmailUnsubscribeTagMandatory } from '../../../utils/common';
19
19
  import history from '../../../utils/history';
20
20
  import messages from '../messages';
21
21
  import emailMessages from '../../Email/messages';
@@ -108,10 +108,13 @@ const EmailHTMLEditor = (props) => {
108
108
  standardErrors: [],
109
109
  });
110
110
 
111
- // Merge tag validation errors (missing) into apiValidationErrors so they show in ValidationErrorDisplay
111
+ // Merge tag validation errors (unsupported/missing) into apiValidationErrors so they show in ValidationErrorDisplay
112
112
  const mergedApiValidationErrors = useMemo(() => {
113
113
  const tagMessages = [];
114
- if (tagValidationError?.missingTags?.length && !isEmailUnsubscribeTagOptional()) {
114
+ if (tagValidationError?.unsupportedTags?.length) {
115
+ tagMessages.push(`Unsupported tags are: ${tagValidationError.unsupportedTags.join(', ')}`);
116
+ }
117
+ if (tagValidationError?.missingTags?.length && !isEmailUnsubscribeTagMandatory()) {
115
118
  tagMessages.push(`Missing tags are: ${tagValidationError.missingTags.join(', ')}`);
116
119
  }
117
120
  if (tagMessages.length === 0) {
@@ -187,6 +190,9 @@ const EmailHTMLEditor = (props) => {
187
190
  },
188
191
  }), [htmlContent, subject, currentOrgDetails]);
189
192
 
193
+ // Check if liquid support is enabled
194
+ const isLiquidEnabled = hasLiquidSupportFeature();
195
+
190
196
  // Detect edit mode: when isEditEmail is false (create flow), never treat as edit or fetch template details
191
197
  const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
192
198
  const currentTemplateId = isEditEmail
@@ -482,25 +488,15 @@ const EmailHTMLEditor = (props) => {
482
488
  const handleContentChange = useCallback((content) => {
483
489
  setHtmlContent(content);
484
490
 
485
- // Clear previous liquid/API validation errors so Done button can be enabled after user fixes content
486
- setApiValidationErrors({
487
- liquidErrors: [],
488
- standardErrors: [],
489
- });
490
- if (showLiquidErrorInFooter) {
491
- showLiquidErrorInFooter({
492
- STANDARD_ERROR_MSG: [],
493
- LIQUID_ERROR_MSG: [],
494
- });
495
- }
496
-
497
491
  // Validate tags
498
492
  if (tags.length > 0 || !isEmpty(injectedTags)) {
499
493
  const validationResult = validateTags({
500
494
  content,
501
495
  tagsParam: tags,
496
+ injectedTagsParams: injectedTags,
502
497
  location,
503
498
  tagModule: getDefaultTags,
499
+ eventContextTags,
504
500
  isFullMode,
505
501
  });
506
502
 
@@ -510,7 +506,7 @@ const EmailHTMLEditor = (props) => {
510
506
  setTagValidationError(null);
511
507
  }
512
508
  }
513
- }, [tags, injectedTags, location, getDefaultTags, eventContextTags, showLiquidErrorInFooter]);
509
+ }, [tags, injectedTags, location, getDefaultTags, eventContextTags]);
514
510
 
515
511
  // Store the last validation state received from HTMLEditor
516
512
  const lastValidationStateRef = useRef(null);
@@ -654,7 +650,7 @@ const EmailHTMLEditor = (props) => {
654
650
  // IMPORTANT: Clear API validation errors FIRST before checking for validation errors
655
651
  // This ensures that old API errors don't block the save when user fixes content and clicks Update again
656
652
  // We'll re-validate with fresh API call anyway
657
- if (getLiquidTags) {
653
+ if (isLiquidEnabled && getLiquidTags) {
658
654
  setApiValidationErrors({
659
655
  liquidErrors: [],
660
656
  standardErrors: [],
@@ -716,7 +712,7 @@ const EmailHTMLEditor = (props) => {
716
712
  // When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is false: validate and require unsubscribe tag.
717
713
  // Run for both library and full mode so liquid-enabled orgs also get the error (notification + ValidationErrorDisplay).
718
714
  const isModuleTypeOutbound = (moduleType || '').toUpperCase() === OUTBOUND;
719
- if (!isEmailUnsubscribeTagOptional() && isModuleTypeOutbound) {
715
+ if (!isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
720
716
  const unsubscribeRegex = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g; // eslint-disable-line no-useless-escape
721
717
  const hasUnsubscribeTag = unsubscribeRegex.test(htmlContent);
722
718
 
@@ -748,17 +744,52 @@ const EmailHTMLEditor = (props) => {
748
744
  const validationResult = validateTags({
749
745
  content: htmlContent,
750
746
  tagsParam: tags,
747
+ injectedTagsParams: injectedTags,
751
748
  location,
752
749
  tagModule: getDefaultTags,
750
+ eventContextTags,
753
751
  isFullMode,
754
752
  });
755
753
 
756
- if (!validationResult?.valid) {
754
+ const hasUnsupportedTags = validationResult?.unsupportedTags?.length > 0;
755
+ if (!validationResult?.valid || (hasUnsupportedTags && !isFullMode)) {
757
756
  setTagValidationError(validationResult);
758
- // For liquid orgs, show warning and continue (extractTags API will validate)
757
+ // IMPORTANT: For non-liquid orgs, block save (like CK/BEE editor)
758
+ // For liquid orgs, continue (extractTags API will validate)
759
+ if (!isLiquidEnabled) {
760
+ // Show notification popup like CK/BEE editor
761
+ const baseLanguage = get(currentOrgDetails, 'basic_details.base_language', 'en');
762
+
763
+ const contentNotValidMsg = intl.formatMessage(formBuilderMessages.contentNotValidLanguage);
764
+ let errorMessage = `${contentNotValidMsg} ${baseLanguage}`;
765
+
766
+ if (hasUnsupportedTags) {
767
+ const unsupportedTagsMsg = intl.formatMessage(formBuilderMessages.unsupportedTags);
768
+ errorMessage += `\n${unsupportedTagsMsg} ${validationResult?.unsupportedTags?.join(', ')}`;
769
+ }
770
+ if (validationResult?.missingTags?.length > 0) {
771
+ const missingTagsMsg = intl.formatMessage(formBuilderMessages.missingTags);
772
+ errorMessage += `\n${missingTagsMsg} ${validationResult?.missingTags?.join(', ')}`;
773
+ }
774
+
775
+ const type = 'error';
776
+ CapNotification[type]({
777
+ message: `${type.toUpperCase()} ! ! ! `,
778
+ description: errorMessage,
779
+ duration: 5,
780
+ });
781
+
782
+ // Reset parent state so next click is detected as a change
783
+ if (onValidationFail) {
784
+ onValidationFail();
785
+ }
786
+ // Block save for non-liquid orgs
787
+ return;
788
+ }
789
+ // For liquid orgs, just show warning and continue
759
790
  }
760
791
  // Clear tag errors if valid
761
- if (tagValidationError && validationResult?.valid) {
792
+ if (tagValidationError && validationResult?.valid && !hasUnsupportedTags) {
762
793
  setTagValidationError(null);
763
794
  }
764
795
  }
@@ -925,9 +956,13 @@ const EmailHTMLEditor = (props) => {
925
956
  }
926
957
  };
927
958
 
928
- // Liquid validation (extractTags) only in library mode
929
- if (getLiquidTags && !isFullMode) {
959
+ // If liquid enabled, validate first using extractTags API (skip in full/standalone mode)
960
+ if (isLiquidEnabled && getLiquidTags && !isFullMode) {
961
+ // Note: API validation errors are already cleared at the start of handleSave
962
+ // This ensures fresh validation on every save attempt
963
+
930
964
  const onError = ({ standardErrors, liquidErrors }) => {
965
+ // Store API validation errors in state so they can be displayed in UI
931
966
  setApiValidationErrors({
932
967
  liquidErrors: liquidErrors || [],
933
968
  standardErrors: standardErrors || [],
@@ -939,12 +974,15 @@ const EmailHTMLEditor = (props) => {
939
974
  LIQUID_ERROR_MSG: liquidErrors || [],
940
975
  });
941
976
  }
977
+ // Don't reset ref here - liquid validation is async and resetting causes infinite loop
978
+ // The parent's isGetFormData will be reset by onValidationFail, and the next click will be detected
942
979
  if (onValidationFail) {
943
980
  onValidationFail();
944
981
  }
945
982
  };
946
983
 
947
984
  const onSuccess = () => {
985
+ // Clear API validation errors on success
948
986
  setApiValidationErrors({
949
987
  liquidErrors: [],
950
988
  standardErrors: [],
@@ -960,6 +998,10 @@ const EmailHTMLEditor = (props) => {
960
998
  messages: formBuilderMessages,
961
999
  onError,
962
1000
  onSuccess,
1001
+ tagLookupMap: metaEntities?.tagLookupMap,
1002
+ eventContextTags,
1003
+ isLiquidFlow: true,
1004
+ forwardedTags: forwardedTags || {},
963
1005
  });
964
1006
  } else {
965
1007
  performSave();
@@ -971,6 +1013,7 @@ const EmailHTMLEditor = (props) => {
971
1013
  injectedTags,
972
1014
  location,
973
1015
  getDefaultTags,
1016
+ eventContextTags,
974
1017
  formatMessage,
975
1018
  subjectError,
976
1019
  isFullMode,
@@ -982,8 +1025,11 @@ const EmailHTMLEditor = (props) => {
982
1025
  emailActions,
983
1026
  getFormdata,
984
1027
  isGetFormData,
1028
+ isLiquidEnabled,
985
1029
  getLiquidTags,
986
1030
  showLiquidErrorInFooter,
1031
+ metaEntities,
1032
+ forwardedTags,
987
1033
  globalActions,
988
1034
  intl,
989
1035
  extractedTemplateName,
@@ -1129,6 +1175,7 @@ const EmailHTMLEditor = (props) => {
1129
1175
  userLocale={intl.locale || 'en'}
1130
1176
  moduleFilterEnabled={location?.query?.type !== EMBEDDED}
1131
1177
  onTagContextChange={handleOnTagsContextChange}
1178
+ isLiquidEnabled={isLiquidEnabled}
1132
1179
  isFullMode={isFullMode}
1133
1180
  onErrorAcknowledged={handleErrorAcknowledged}
1134
1181
  onValidationChange={handleValidationChange}
@@ -13,7 +13,7 @@ import { IntlProvider } from 'react-intl';
13
13
  import EmailHTMLEditor from '../EmailHTMLEditor';
14
14
  import { validateLiquidTemplateContent } from '../../../../utils/commonUtils';
15
15
  import { validateTags } from '../../../../utils/tagValidations';
16
- import { isEmailUnsubscribeTagOptional } from '../../../../utils/common';
16
+ import { isEmailUnsubscribeTagMandatory } from '../../../../utils/common';
17
17
 
18
18
  // Mock dependencies
19
19
  jest.mock('../../../../utils/commonUtils', () => ({
@@ -24,8 +24,11 @@ jest.mock('../../../../utils/tagValidations', () => ({
24
24
  validateTags: jest.fn(),
25
25
  }));
26
26
 
27
+ // Create mutable mock for hasLiquidSupportFeature
28
+ const mockHasLiquidSupportFeature = jest.fn(() => true);
27
29
  jest.mock('../../../../utils/common', () => ({
28
- isEmailUnsubscribeTagOptional: jest.fn(() => false),
30
+ hasLiquidSupportFeature: (...args) => mockHasLiquidSupportFeature(...args),
31
+ isEmailUnsubscribeTagMandatory: jest.fn(() => false),
29
32
  }));
30
33
 
31
34
  jest.mock('../../../../utils/history', () => ({
@@ -417,7 +420,7 @@ describe('EmailHTMLEditor', () => {
417
420
  jest.clearAllMocks();
418
421
  validateLiquidTemplateContent.mockResolvedValue(true);
419
422
  validateTags.mockReturnValue({ valid: true });
420
- isEmailUnsubscribeTagOptional.mockReturnValue(false);
423
+ isEmailUnsubscribeTagMandatory.mockReturnValue(false);
421
424
  // Reset mock functions
422
425
  mockGetAllIssues.mockReturnValue([]);
423
426
  mockGetValidationState.mockReturnValue({
@@ -425,6 +428,88 @@ describe('EmailHTMLEditor', () => {
425
428
  hasErrors: false,
426
429
  issueCounts: { errors: 0, warnings: 0, total: 0 },
427
430
  });
431
+ // Reset hasLiquidSupportFeature mock to return true by default
432
+ mockHasLiquidSupportFeature.mockReturnValue(true);
433
+ capturedApiValidationErrorsRef.current = null;
434
+ });
435
+
436
+ describe('mergedApiValidationErrors (lines 124-125)', () => {
437
+ beforeEach(() => {
438
+ global.__captureApiValidationErrorsRef = capturedApiValidationErrorsRef;
439
+ });
440
+
441
+ afterEach(() => {
442
+ delete global.__captureApiValidationErrorsRef;
443
+ });
444
+
445
+ it('merges tag unsupported errors into standardErrors when tagValidationError has unsupportedTags', async () => {
446
+ validateTags.mockReturnValue({
447
+ valid: false,
448
+ unsupportedTags: ['tagA', 'tagB'],
449
+ });
450
+ renderWithIntl({
451
+ metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
452
+ tags: [{ name: 'customer.name' }],
453
+ supportedTags: [],
454
+ });
455
+ const changeButton = screen.getByTestId('trigger-content-change');
456
+ await act(async () => {
457
+ fireEvent.click(changeButton);
458
+ });
459
+ await waitFor(() => {
460
+ expect(capturedApiValidationErrorsRef.current).not.toBeNull();
461
+ expect(capturedApiValidationErrorsRef.current.liquidErrors).toEqual([]);
462
+ expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
463
+ 'Unsupported tags are: tagA, tagB',
464
+ );
465
+ });
466
+ });
467
+
468
+ it('merges tag missing errors into standardErrors when tagValidationError has missingTags and unsubscribe not mandatory', async () => {
469
+ isEmailUnsubscribeTagMandatory.mockReturnValue(false);
470
+ validateTags.mockReturnValue({
471
+ valid: false,
472
+ missingTags: ['unsubscribe'],
473
+ });
474
+ renderWithIntl({
475
+ metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
476
+ tags: [{ name: 'customer.name' }],
477
+ supportedTags: [],
478
+ });
479
+ const changeButton = screen.getByTestId('trigger-content-change');
480
+ await act(async () => {
481
+ fireEvent.click(changeButton);
482
+ });
483
+ await waitFor(() => {
484
+ expect(capturedApiValidationErrorsRef.current).not.toBeNull();
485
+ expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
486
+ 'Missing tags are: unsubscribe',
487
+ );
488
+ });
489
+ });
490
+
491
+ it('uses apiValidationErrors.liquidErrors and concatenates apiValidationErrors.standardErrors with tag messages (merge shape)', async () => {
492
+ // When tag messages exist, mergedApiValidationErrors returns liquidErrors from apiValidationErrors
493
+ // and standardErrors = [...(apiValidationErrors?.standardErrors || []), ...tagMessages] (lines 124-125)
494
+ validateTags.mockReturnValue({
495
+ valid: false,
496
+ unsupportedTags: ['customTag'],
497
+ });
498
+ renderWithIntl({
499
+ metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
500
+ tags: [{ name: 'customer.name' }],
501
+ });
502
+ const changeButton = screen.getByTestId('trigger-content-change');
503
+ await act(async () => {
504
+ fireEvent.click(changeButton);
505
+ });
506
+ await waitFor(() => {
507
+ expect(capturedApiValidationErrorsRef.current).not.toBeNull();
508
+ const { liquidErrors, standardErrors } = capturedApiValidationErrorsRef.current;
509
+ expect(liquidErrors).toEqual([]);
510
+ expect(standardErrors).toContain('Unsupported tags are: customTag');
511
+ });
512
+ });
428
513
  });
429
514
 
430
515
  describe('Default Parameter Values (lines 60-63)', () => {
@@ -1021,7 +1106,7 @@ describe('EmailHTMLEditor', () => {
1021
1106
  });
1022
1107
 
1023
1108
  it('allows save when unsubscribe validation is on and tag is present', () => {
1024
- isEmailUnsubscribeTagOptional.mockReturnValue(false);
1109
+ isEmailUnsubscribeTagMandatory.mockReturnValue(false);
1025
1110
  renderWithIntl({
1026
1111
  isGetFormData: true,
1027
1112
  subject: 'Valid Subject',
@@ -1031,44 +1116,49 @@ describe('EmailHTMLEditor', () => {
1031
1116
  // Should proceed with save (validation passes)
1032
1117
  });
1033
1118
 
1034
- it('blocks save when liquid API validation fails for Email', async () => {
1035
- // Liquid validation runs in library mode (!isFullMode). Simulate API validation failure via mock onError.
1036
- validateLiquidTemplateContent.mockImplementation((content, options) => {
1037
- options.onError({ standardErrors: [], liquidErrors: ['Validation failed'] });
1038
- return Promise.resolve(false);
1119
+ it('blocks save for non-liquid orgs when tag validation fails', async () => {
1120
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1121
+ validateTags.mockReturnValue({
1122
+ valid: false,
1123
+ unsupportedTags: ['tag1'],
1124
+ missingTags: ['tag2'],
1039
1125
  });
1040
1126
  const onValidationFail = jest.fn();
1127
+ const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
1128
+
1129
+ // Set subject and content via component interactions
1041
1130
  const { rerender } = renderWithIntl({
1042
1131
  onValidationFail,
1043
1132
  isGetFormData: false,
1044
- isFullMode: false,
1045
1133
  metaEntities: {
1046
1134
  tags: {
1047
1135
  standard: [{ name: 'customer.name' }],
1048
1136
  },
1049
1137
  },
1050
- getLiquidTags: jest.fn((content, cb) => cb({ askAiraResponse: { data: [] }, isError: false })),
1138
+ getLiquidTags: null, // No liquid tags for non-liquid org
1051
1139
  });
1052
1140
  const input = screen.getByTestId('subject-input');
1053
1141
  fireEvent.change(input, { target: { value: 'Valid Subject' } });
1054
1142
  const changeButton = screen.getByTestId('trigger-content-change');
1055
1143
  fireEvent.click(changeButton);
1144
+ // Now trigger save
1056
1145
  rerender(
1057
1146
  <IntlProvider locale="en" messages={{}}>
1058
1147
  <EmailHTMLEditor
1059
1148
  {...defaultProps}
1060
1149
  onValidationFail={onValidationFail}
1061
1150
  isGetFormData
1062
- isFullMode={false}
1063
1151
  metaEntities={{
1064
1152
  tags: {
1065
1153
  standard: [{ name: 'customer.name' }],
1066
1154
  },
1067
1155
  }}
1068
- getLiquidTags={jest.fn((content, cb) => cb({ askAiraResponse: { data: [] }, isError: false }))} />
1156
+ getLiquidTags={null} />
1069
1157
  </IntlProvider>
1070
1158
  );
1159
+
1071
1160
  await waitFor(() => {
1161
+ expect(CapNotification.error).toHaveBeenCalled();
1072
1162
  expect(onValidationFail).toHaveBeenCalled();
1073
1163
  }, { timeout: 3000 });
1074
1164
  });
@@ -1085,7 +1175,7 @@ describe('EmailHTMLEditor', () => {
1085
1175
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1086
1176
  mockGetAllIssues.mockReturnValue([]);
1087
1177
 
1088
- // Liquid validation runs only in library mode
1178
+ // Set subject and content via component interactions
1089
1179
  // Use isFullMode: false (library mode) to test liquid validation path
1090
1180
  const { rerender } = renderWithIntl({
1091
1181
  isGetFormData: false,
@@ -1095,6 +1185,7 @@ describe('EmailHTMLEditor', () => {
1095
1185
  standard: [{ name: 'customer.name' }],
1096
1186
  },
1097
1187
  },
1188
+ isLiquidEnabled: true,
1098
1189
  getLiquidTags,
1099
1190
  });
1100
1191
  const input = screen.getByTestId('subject-input');
@@ -1140,11 +1231,12 @@ describe('EmailHTMLEditor', () => {
1140
1231
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1141
1232
  mockGetAllIssues.mockReturnValue([]);
1142
1233
 
1143
- // Liquid validation runs only in library mode
1234
+ // Set subject and content via component interactions
1144
1235
  // Use isFullMode: false (library mode) to test liquid validation path
1145
1236
  const { rerender } = renderWithIntl({
1146
1237
  isGetFormData: false,
1147
1238
  isFullMode: false,
1239
+ isLiquidEnabled: true,
1148
1240
  getLiquidTags,
1149
1241
  });
1150
1242
  const input = screen.getByTestId('subject-input');
@@ -1190,11 +1282,12 @@ describe('EmailHTMLEditor', () => {
1190
1282
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1191
1283
  mockGetAllIssues.mockReturnValue([]);
1192
1284
 
1193
- // Liquid validation runs only in library mode
1285
+ // Set subject and content via component interactions
1194
1286
  // Use isFullMode: false (library mode) to test liquid validation path
1195
1287
  const { rerender } = renderWithIntl({
1196
1288
  isGetFormData: false,
1197
1289
  isFullMode: false,
1290
+ isLiquidEnabled: true,
1198
1291
  getLiquidTags,
1199
1292
  showLiquidErrorInFooter,
1200
1293
  onValidationFail,
@@ -1242,15 +1335,14 @@ describe('EmailHTMLEditor', () => {
1242
1335
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1243
1336
  mockGetAllIssues.mockReturnValue([]);
1244
1337
 
1245
- // Liquid validation runs only in library mode; then performSave calls getFormdata
1338
+ // Set subject and content via component interactions
1246
1339
  // Use isFullMode: false (library mode) to test liquid validation path
1247
1340
  const { rerender } = renderWithIntl({
1248
1341
  isGetFormData: false,
1249
1342
  isFullMode: false,
1250
- isFullMode: false,
1251
1343
  getLiquidTags,
1252
1344
  getFormdata,
1253
- location: { query: { module: 'library' } },
1345
+ templateName: 'New Template',
1254
1346
  });
1255
1347
  const input = screen.getByTestId('subject-input');
1256
1348
  await act(async () => {
@@ -1268,7 +1360,7 @@ describe('EmailHTMLEditor', () => {
1268
1360
  await act(async () => {
1269
1361
  rerender(
1270
1362
  <IntlProvider locale="en" messages={{}}>
1271
- <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} getFormdata={getFormdata} location={{ query: { module: 'library' } }} />
1363
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} getFormdata={getFormdata} templateName="New Template" />
1272
1364
  </IntlProvider>
1273
1365
  );
1274
1366
  });
@@ -1284,6 +1376,7 @@ describe('EmailHTMLEditor', () => {
1284
1376
  });
1285
1377
 
1286
1378
  it('saves in full mode with create template', async () => {
1379
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1287
1380
  const emailActions = {
1288
1381
  ...defaultProps.emailActions,
1289
1382
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1317,6 +1410,7 @@ describe('EmailHTMLEditor', () => {
1317
1410
  });
1318
1411
 
1319
1412
  it('saves in full mode with edit template', async () => {
1413
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1320
1414
  const emailActions = {
1321
1415
  ...defaultProps.emailActions,
1322
1416
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1386,6 +1480,7 @@ describe('EmailHTMLEditor', () => {
1386
1480
  });
1387
1481
 
1388
1482
  it('handles create template error response', async () => {
1483
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1389
1484
  const emailActions = {
1390
1485
  ...defaultProps.emailActions,
1391
1486
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1420,6 +1515,7 @@ describe('EmailHTMLEditor', () => {
1420
1515
  });
1421
1516
 
1422
1517
  it('handles create template success with getFormdata', async () => {
1518
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1423
1519
  const emailActions = {
1424
1520
  ...defaultProps.emailActions,
1425
1521
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1456,6 +1552,7 @@ describe('EmailHTMLEditor', () => {
1456
1552
  });
1457
1553
 
1458
1554
  it('handles create template success without getFormdata', async () => {
1555
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1459
1556
  const emailActions = {
1460
1557
  ...defaultProps.emailActions,
1461
1558
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1491,6 +1588,7 @@ describe('EmailHTMLEditor', () => {
1491
1588
  });
1492
1589
 
1493
1590
  it('saves in library mode with getFormdata', async () => {
1591
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1494
1592
  const getFormdata = jest.fn();
1495
1593
 
1496
1594
  // Set subject and content via component interactions
@@ -1518,6 +1616,7 @@ describe('EmailHTMLEditor', () => {
1518
1616
  });
1519
1617
 
1520
1618
  it('saves in library mode without library module', async () => {
1619
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1521
1620
  const getFormdata = jest.fn();
1522
1621
 
1523
1622
  // Set subject and content via component interactions
@@ -1657,6 +1756,7 @@ describe('EmailHTMLEditor', () => {
1657
1756
  renderWithIntl({
1658
1757
  getLiquidTags: null,
1659
1758
  globalActions,
1759
+ isLiquidEnabled: true,
1660
1760
  isGetFormData: true,
1661
1761
  subject: 'Valid Subject',
1662
1762
  htmlContent: '<p>Content</p>',
@@ -22,7 +22,7 @@ import {
22
22
  CapTooltip,
23
23
  } from '@capillarytech/cap-ui-library';
24
24
  import { FONT_SIZE_L } from '@capillarytech/cap-ui-library/styled/variables';
25
- import _, { find, cloneDeep, findIndex, isEmpty, isEqual, filter, replace } from 'lodash';
25
+ import _, { find, cloneDeep, findIndex, isEmpty, isEqual, filter, flattenDeep, replace } from 'lodash';
26
26
  import * as actions from './actions';
27
27
  import { makeSelectFTP, makeSelectMetaEntities } from './selectors';
28
28
  import { makeSelectLoyaltyPromotionDisplay, setInjectedTags } from '../Cap/selectors';
@@ -33,6 +33,7 @@ import * as globalActions from '../Cap/actions';
33
33
  import { TagList } from '../TagList';
34
34
  import { NO_COMMUNICATION, CREATE, EDIT, PREVIEW } from '../App/constants';
35
35
  import { getTreeStructuredTags } from '../../utils/common';
36
+ import { transformInjectedTags, checkIfSupportedTag, skipTags } from '../../utils/tagValidations';
36
37
  import injectSaga from '../../utils/injectSaga';
37
38
  import injectReducer from '../../utils/injectReducer';
38
39
 
@@ -234,16 +235,63 @@ export class FTP extends React.Component {
234
235
  }));
235
236
  };
236
237
 
238
+ getFlatTags = (tags) => {
239
+ const flatTags = [];
240
+ tags.forEach((tag) => {
241
+ if ((tag.children || []).length) {
242
+ const innerTags = this.getFlatTags(tag.children);
243
+ flatTags.push(innerTags);
244
+ } else {
245
+ flatTags.push(tag.value);
246
+ }
247
+ });
248
+
249
+ return flattenDeep(flatTags);
250
+ };
251
+
252
+ validateTags(content, tagsParam, injectedTagsParams) {
253
+ const tags = tagsParam;
254
+ const injectedTags = transformInjectedTags(injectedTagsParams);
255
+ const response = {};
256
+ response.valid = true;
257
+ response.unsupportedTags = [];
258
+ const flatTags = this.getFlatTags(tags);
259
+ if (flatTags && flatTags.length) {
260
+ const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
261
+ const matchedTags = [...content.matchAll(regex)];
262
+ matchedTags.forEach((tag) => {
263
+ let ifSupported = !!flatTags.find((t) => t === tag[0]);
264
+ const tagValue = tag[0].substring(this.indexOfEnd(tag[0], '{{'), tag[0].indexOf('}}'));
265
+ ifSupported = ifSupported || checkIfSupportedTag(tagValue, injectedTags) || skipTags(tagValue);
266
+ if (!ifSupported) {
267
+ response.unsupportedTags.push(tagValue);
268
+ response.valid = false;
269
+ }
270
+ if (response.unsupportedTags.length === 0) {
271
+ response.valid = true;
272
+ }
273
+ });
274
+ }
275
+ return response;
276
+ }
277
+
237
278
  indexOfEnd(targetString, string) {
238
279
  const io = targetString.indexOf(string);
239
280
  return io == -1 ? -1 : io + string.length;
240
281
  }
241
282
 
242
283
  getMessageContent = () => {
243
- const { messageContent } = this.state;
284
+ const { messageContent, tagsTree = []} = this.state;
244
285
  const { formatMessage } = this.props.intl;
245
286
  const { metaEntities, selectedOfferDetails, injectedTags } = this.props;
246
287
  const tagsRaw = metaEntities && metaEntities.tags ? metaEntities.tags.standard : [];
288
+ const validateTagResponse = !this.props?.isFullMode ? this.validateTags(messageContent, tagsTree, injectedTags) : { valid: true, unsupportedTags: [] };
289
+ let unsupportedTags = null;
290
+ let errorMessageText = '';
291
+ if (!validateTagResponse.valid) {
292
+ unsupportedTags = validateTagResponse.unsupportedTags.join(', ').toString();
293
+ errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags});
294
+ }
247
295
  return (
248
296
  <CapRow>
249
297
  <CapColumn span={11}>
@@ -261,6 +309,7 @@ export class FTP extends React.Component {
261
309
  label={formatMessage(messages.messageHeader)}
262
310
  onChange={this.updateMessageBody}
263
311
  value={messageContent}
312
+ errorMessage={errorMessageText}
264
313
  />
265
314
  </div>
266
315
  </CapColumn>
@@ -21,6 +21,10 @@ export default defineMessages({
21
21
  id: 'creatives.containersV2.FTP.addColumn',
22
22
  defaultMessage: 'Add column',
23
23
  },
24
+ unsupportedTagsValidationError: {
25
+ id: 'creatives.containersV2.FTP.unsupportedTagsValidationError',
26
+ defaultMessage: 'Unsupported tags: {unsupportedTags}. Please remove them from this message.',
27
+ },
24
28
  selectTag: {
25
29
  id: 'creatives.containersV2.FTP.selectTag',
26
30
  defaultMessage: 'Select tag',