@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
@@ -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 { hasLiquidSupportFeature, isEmailUnsubscribeTagMandatory } from '../../../utils/common';
18
+ import { isEmailUnsubscribeTagOptional } from '../../../utils/common';
19
19
  import history from '../../../utils/history';
20
20
  import messages from '../messages';
21
21
  import emailMessages from '../../Email/messages';
@@ -108,13 +108,10 @@ const EmailHTMLEditor = (props) => {
108
108
  standardErrors: [],
109
109
  });
110
110
 
111
- // Merge tag validation errors (unsupported/missing) into apiValidationErrors so they show in ValidationErrorDisplay
111
+ // Merge tag validation errors (missing) into apiValidationErrors so they show in ValidationErrorDisplay
112
112
  const mergedApiValidationErrors = useMemo(() => {
113
113
  const tagMessages = [];
114
- if (tagValidationError?.unsupportedTags?.length) {
115
- tagMessages.push(`Unsupported tags are: ${tagValidationError.unsupportedTags.join(', ')}`);
116
- }
117
- if (tagValidationError?.missingTags?.length && !isEmailUnsubscribeTagMandatory()) {
114
+ if (tagValidationError?.missingTags?.length && !isEmailUnsubscribeTagOptional()) {
118
115
  tagMessages.push(`Missing tags are: ${tagValidationError.missingTags.join(', ')}`);
119
116
  }
120
117
  if (tagMessages.length === 0) {
@@ -190,9 +187,6 @@ const EmailHTMLEditor = (props) => {
190
187
  },
191
188
  }), [htmlContent, subject, currentOrgDetails]);
192
189
 
193
- // Check if liquid support is enabled
194
- const isLiquidEnabled = hasLiquidSupportFeature();
195
-
196
190
  // Detect edit mode: when isEditEmail is false (create flow), never treat as edit or fetch template details
197
191
  const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
198
192
  const currentTemplateId = isEditEmail
@@ -488,15 +482,25 @@ const EmailHTMLEditor = (props) => {
488
482
  const handleContentChange = useCallback((content) => {
489
483
  setHtmlContent(content);
490
484
 
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
+
491
497
  // Validate tags
492
498
  if (tags.length > 0 || !isEmpty(injectedTags)) {
493
499
  const validationResult = validateTags({
494
500
  content,
495
501
  tagsParam: tags,
496
- injectedTagsParams: injectedTags,
497
502
  location,
498
503
  tagModule: getDefaultTags,
499
- eventContextTags,
500
504
  isFullMode,
501
505
  });
502
506
 
@@ -506,7 +510,7 @@ const EmailHTMLEditor = (props) => {
506
510
  setTagValidationError(null);
507
511
  }
508
512
  }
509
- }, [tags, injectedTags, location, getDefaultTags, eventContextTags]);
513
+ }, [tags, injectedTags, location, getDefaultTags, eventContextTags, showLiquidErrorInFooter]);
510
514
 
511
515
  // Store the last validation state received from HTMLEditor
512
516
  const lastValidationStateRef = useRef(null);
@@ -650,7 +654,7 @@ const EmailHTMLEditor = (props) => {
650
654
  // IMPORTANT: Clear API validation errors FIRST before checking for validation errors
651
655
  // This ensures that old API errors don't block the save when user fixes content and clicks Update again
652
656
  // We'll re-validate with fresh API call anyway
653
- if (isLiquidEnabled && getLiquidTags) {
657
+ if (getLiquidTags) {
654
658
  setApiValidationErrors({
655
659
  liquidErrors: [],
656
660
  standardErrors: [],
@@ -712,7 +716,7 @@ const EmailHTMLEditor = (props) => {
712
716
  // When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is false: validate and require unsubscribe tag.
713
717
  // Run for both library and full mode so liquid-enabled orgs also get the error (notification + ValidationErrorDisplay).
714
718
  const isModuleTypeOutbound = (moduleType || '').toUpperCase() === OUTBOUND;
715
- if (!isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
719
+ if (!isEmailUnsubscribeTagOptional() && isModuleTypeOutbound) {
716
720
  const unsubscribeRegex = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g; // eslint-disable-line no-useless-escape
717
721
  const hasUnsubscribeTag = unsubscribeRegex.test(htmlContent);
718
722
 
@@ -744,52 +748,17 @@ const EmailHTMLEditor = (props) => {
744
748
  const validationResult = validateTags({
745
749
  content: htmlContent,
746
750
  tagsParam: tags,
747
- injectedTagsParams: injectedTags,
748
751
  location,
749
752
  tagModule: getDefaultTags,
750
- eventContextTags,
751
753
  isFullMode,
752
754
  });
753
755
 
754
- const hasUnsupportedTags = validationResult?.unsupportedTags?.length > 0;
755
- if (!validationResult?.valid || (hasUnsupportedTags && !isFullMode)) {
756
+ if (!validationResult?.valid) {
756
757
  setTagValidationError(validationResult);
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
758
+ // For liquid orgs, show warning and continue (extractTags API will validate)
790
759
  }
791
760
  // Clear tag errors if valid
792
- if (tagValidationError && validationResult?.valid && !hasUnsupportedTags) {
761
+ if (tagValidationError && validationResult?.valid) {
793
762
  setTagValidationError(null);
794
763
  }
795
764
  }
@@ -956,13 +925,9 @@ const EmailHTMLEditor = (props) => {
956
925
  }
957
926
  };
958
927
 
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
-
928
+ // Liquid validation (extractTags) only in library mode
929
+ if (getLiquidTags && !isFullMode) {
964
930
  const onError = ({ standardErrors, liquidErrors }) => {
965
- // Store API validation errors in state so they can be displayed in UI
966
931
  setApiValidationErrors({
967
932
  liquidErrors: liquidErrors || [],
968
933
  standardErrors: standardErrors || [],
@@ -974,15 +939,12 @@ const EmailHTMLEditor = (props) => {
974
939
  LIQUID_ERROR_MSG: liquidErrors || [],
975
940
  });
976
941
  }
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
979
942
  if (onValidationFail) {
980
943
  onValidationFail();
981
944
  }
982
945
  };
983
946
 
984
947
  const onSuccess = () => {
985
- // Clear API validation errors on success
986
948
  setApiValidationErrors({
987
949
  liquidErrors: [],
988
950
  standardErrors: [],
@@ -998,10 +960,6 @@ const EmailHTMLEditor = (props) => {
998
960
  messages: formBuilderMessages,
999
961
  onError,
1000
962
  onSuccess,
1001
- tagLookupMap: metaEntities?.tagLookupMap,
1002
- eventContextTags,
1003
- isLiquidFlow: true,
1004
- forwardedTags: forwardedTags || {},
1005
963
  });
1006
964
  } else {
1007
965
  performSave();
@@ -1013,7 +971,6 @@ const EmailHTMLEditor = (props) => {
1013
971
  injectedTags,
1014
972
  location,
1015
973
  getDefaultTags,
1016
- eventContextTags,
1017
974
  formatMessage,
1018
975
  subjectError,
1019
976
  isFullMode,
@@ -1025,11 +982,8 @@ const EmailHTMLEditor = (props) => {
1025
982
  emailActions,
1026
983
  getFormdata,
1027
984
  isGetFormData,
1028
- isLiquidEnabled,
1029
985
  getLiquidTags,
1030
986
  showLiquidErrorInFooter,
1031
- metaEntities,
1032
- forwardedTags,
1033
987
  globalActions,
1034
988
  intl,
1035
989
  extractedTemplateName,
@@ -1175,7 +1129,6 @@ const EmailHTMLEditor = (props) => {
1175
1129
  userLocale={intl.locale || 'en'}
1176
1130
  moduleFilterEnabled={location?.query?.type !== EMBEDDED}
1177
1131
  onTagContextChange={handleOnTagsContextChange}
1178
- isLiquidEnabled={isLiquidEnabled}
1179
1132
  isFullMode={isFullMode}
1180
1133
  onErrorAcknowledged={handleErrorAcknowledged}
1181
1134
  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 { isEmailUnsubscribeTagMandatory } from '../../../../utils/common';
16
+ import { isEmailUnsubscribeTagOptional } from '../../../../utils/common';
17
17
 
18
18
  // Mock dependencies
19
19
  jest.mock('../../../../utils/commonUtils', () => ({
@@ -24,11 +24,8 @@ 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);
29
27
  jest.mock('../../../../utils/common', () => ({
30
- hasLiquidSupportFeature: (...args) => mockHasLiquidSupportFeature(...args),
31
- isEmailUnsubscribeTagMandatory: jest.fn(() => false),
28
+ isEmailUnsubscribeTagOptional: jest.fn(() => false),
32
29
  }));
33
30
 
34
31
  jest.mock('../../../../utils/history', () => ({
@@ -420,7 +417,7 @@ describe('EmailHTMLEditor', () => {
420
417
  jest.clearAllMocks();
421
418
  validateLiquidTemplateContent.mockResolvedValue(true);
422
419
  validateTags.mockReturnValue({ valid: true });
423
- isEmailUnsubscribeTagMandatory.mockReturnValue(false);
420
+ isEmailUnsubscribeTagOptional.mockReturnValue(false);
424
421
  // Reset mock functions
425
422
  mockGetAllIssues.mockReturnValue([]);
426
423
  mockGetValidationState.mockReturnValue({
@@ -428,88 +425,6 @@ describe('EmailHTMLEditor', () => {
428
425
  hasErrors: false,
429
426
  issueCounts: { errors: 0, warnings: 0, total: 0 },
430
427
  });
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
- });
513
428
  });
514
429
 
515
430
  describe('Default Parameter Values (lines 60-63)', () => {
@@ -1106,7 +1021,7 @@ describe('EmailHTMLEditor', () => {
1106
1021
  });
1107
1022
 
1108
1023
  it('allows save when unsubscribe validation is on and tag is present', () => {
1109
- isEmailUnsubscribeTagMandatory.mockReturnValue(false);
1024
+ isEmailUnsubscribeTagOptional.mockReturnValue(false);
1110
1025
  renderWithIntl({
1111
1026
  isGetFormData: true,
1112
1027
  subject: 'Valid Subject',
@@ -1116,49 +1031,44 @@ describe('EmailHTMLEditor', () => {
1116
1031
  // Should proceed with save (validation passes)
1117
1032
  });
1118
1033
 
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'],
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);
1125
1039
  });
1126
1040
  const onValidationFail = jest.fn();
1127
- const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
1128
-
1129
- // Set subject and content via component interactions
1130
1041
  const { rerender } = renderWithIntl({
1131
1042
  onValidationFail,
1132
1043
  isGetFormData: false,
1044
+ isFullMode: false,
1133
1045
  metaEntities: {
1134
1046
  tags: {
1135
1047
  standard: [{ name: 'customer.name' }],
1136
1048
  },
1137
1049
  },
1138
- getLiquidTags: null, // No liquid tags for non-liquid org
1050
+ getLiquidTags: jest.fn((content, cb) => cb({ askAiraResponse: { data: [] }, isError: false })),
1139
1051
  });
1140
1052
  const input = screen.getByTestId('subject-input');
1141
1053
  fireEvent.change(input, { target: { value: 'Valid Subject' } });
1142
1054
  const changeButton = screen.getByTestId('trigger-content-change');
1143
1055
  fireEvent.click(changeButton);
1144
- // Now trigger save
1145
1056
  rerender(
1146
1057
  <IntlProvider locale="en" messages={{}}>
1147
1058
  <EmailHTMLEditor
1148
1059
  {...defaultProps}
1149
1060
  onValidationFail={onValidationFail}
1150
1061
  isGetFormData
1062
+ isFullMode={false}
1151
1063
  metaEntities={{
1152
1064
  tags: {
1153
1065
  standard: [{ name: 'customer.name' }],
1154
1066
  },
1155
1067
  }}
1156
- getLiquidTags={null} />
1068
+ getLiquidTags={jest.fn((content, cb) => cb({ askAiraResponse: { data: [] }, isError: false }))} />
1157
1069
  </IntlProvider>
1158
1070
  );
1159
-
1160
1071
  await waitFor(() => {
1161
- expect(CapNotification.error).toHaveBeenCalled();
1162
1072
  expect(onValidationFail).toHaveBeenCalled();
1163
1073
  }, { timeout: 3000 });
1164
1074
  });
@@ -1175,7 +1085,7 @@ describe('EmailHTMLEditor', () => {
1175
1085
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1176
1086
  mockGetAllIssues.mockReturnValue([]);
1177
1087
 
1178
- // Set subject and content via component interactions
1088
+ // Liquid validation runs only in library mode
1179
1089
  // Use isFullMode: false (library mode) to test liquid validation path
1180
1090
  const { rerender } = renderWithIntl({
1181
1091
  isGetFormData: false,
@@ -1185,7 +1095,6 @@ describe('EmailHTMLEditor', () => {
1185
1095
  standard: [{ name: 'customer.name' }],
1186
1096
  },
1187
1097
  },
1188
- isLiquidEnabled: true,
1189
1098
  getLiquidTags,
1190
1099
  });
1191
1100
  const input = screen.getByTestId('subject-input');
@@ -1231,12 +1140,11 @@ describe('EmailHTMLEditor', () => {
1231
1140
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1232
1141
  mockGetAllIssues.mockReturnValue([]);
1233
1142
 
1234
- // Set subject and content via component interactions
1143
+ // Liquid validation runs only in library mode
1235
1144
  // Use isFullMode: false (library mode) to test liquid validation path
1236
1145
  const { rerender } = renderWithIntl({
1237
1146
  isGetFormData: false,
1238
1147
  isFullMode: false,
1239
- isLiquidEnabled: true,
1240
1148
  getLiquidTags,
1241
1149
  });
1242
1150
  const input = screen.getByTestId('subject-input');
@@ -1282,12 +1190,11 @@ describe('EmailHTMLEditor', () => {
1282
1190
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1283
1191
  mockGetAllIssues.mockReturnValue([]);
1284
1192
 
1285
- // Set subject and content via component interactions
1193
+ // Liquid validation runs only in library mode
1286
1194
  // Use isFullMode: false (library mode) to test liquid validation path
1287
1195
  const { rerender } = renderWithIntl({
1288
1196
  isGetFormData: false,
1289
1197
  isFullMode: false,
1290
- isLiquidEnabled: true,
1291
1198
  getLiquidTags,
1292
1199
  showLiquidErrorInFooter,
1293
1200
  onValidationFail,
@@ -1335,14 +1242,15 @@ describe('EmailHTMLEditor', () => {
1335
1242
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1336
1243
  mockGetAllIssues.mockReturnValue([]);
1337
1244
 
1338
- // Set subject and content via component interactions
1245
+ // Liquid validation runs only in library mode; then performSave calls getFormdata
1339
1246
  // Use isFullMode: false (library mode) to test liquid validation path
1340
1247
  const { rerender } = renderWithIntl({
1341
1248
  isGetFormData: false,
1342
1249
  isFullMode: false,
1250
+ isFullMode: false,
1343
1251
  getLiquidTags,
1344
1252
  getFormdata,
1345
- templateName: 'New Template',
1253
+ location: { query: { module: 'library' } },
1346
1254
  });
1347
1255
  const input = screen.getByTestId('subject-input');
1348
1256
  await act(async () => {
@@ -1360,7 +1268,7 @@ describe('EmailHTMLEditor', () => {
1360
1268
  await act(async () => {
1361
1269
  rerender(
1362
1270
  <IntlProvider locale="en" messages={{}}>
1363
- <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} getFormdata={getFormdata} templateName="New Template" />
1271
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} getFormdata={getFormdata} location={{ query: { module: 'library' } }} />
1364
1272
  </IntlProvider>
1365
1273
  );
1366
1274
  });
@@ -1376,7 +1284,6 @@ describe('EmailHTMLEditor', () => {
1376
1284
  });
1377
1285
 
1378
1286
  it('saves in full mode with create template', async () => {
1379
- mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1380
1287
  const emailActions = {
1381
1288
  ...defaultProps.emailActions,
1382
1289
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1410,7 +1317,6 @@ describe('EmailHTMLEditor', () => {
1410
1317
  });
1411
1318
 
1412
1319
  it('saves in full mode with edit template', async () => {
1413
- mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1414
1320
  const emailActions = {
1415
1321
  ...defaultProps.emailActions,
1416
1322
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1480,7 +1386,6 @@ describe('EmailHTMLEditor', () => {
1480
1386
  });
1481
1387
 
1482
1388
  it('handles create template error response', async () => {
1483
- mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1484
1389
  const emailActions = {
1485
1390
  ...defaultProps.emailActions,
1486
1391
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1515,7 +1420,6 @@ describe('EmailHTMLEditor', () => {
1515
1420
  });
1516
1421
 
1517
1422
  it('handles create template success with getFormdata', async () => {
1518
- mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1519
1423
  const emailActions = {
1520
1424
  ...defaultProps.emailActions,
1521
1425
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1552,7 +1456,6 @@ describe('EmailHTMLEditor', () => {
1552
1456
  });
1553
1457
 
1554
1458
  it('handles create template success without getFormdata', async () => {
1555
- mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1556
1459
  const emailActions = {
1557
1460
  ...defaultProps.emailActions,
1558
1461
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1588,7 +1491,6 @@ describe('EmailHTMLEditor', () => {
1588
1491
  });
1589
1492
 
1590
1493
  it('saves in library mode with getFormdata', async () => {
1591
- mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1592
1494
  const getFormdata = jest.fn();
1593
1495
 
1594
1496
  // Set subject and content via component interactions
@@ -1616,7 +1518,6 @@ describe('EmailHTMLEditor', () => {
1616
1518
  });
1617
1519
 
1618
1520
  it('saves in library mode without library module', async () => {
1619
- mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1620
1521
  const getFormdata = jest.fn();
1621
1522
 
1622
1523
  // Set subject and content via component interactions
@@ -1756,7 +1657,6 @@ describe('EmailHTMLEditor', () => {
1756
1657
  renderWithIntl({
1757
1658
  getLiquidTags: null,
1758
1659
  globalActions,
1759
- isLiquidEnabled: true,
1760
1660
  isGetFormData: true,
1761
1661
  subject: 'Valid Subject',
1762
1662
  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, flattenDeep, replace } from 'lodash';
25
+ import _, { find, cloneDeep, findIndex, isEmpty, isEqual, filter, 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,7 +33,6 @@ 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';
37
36
  import injectSaga from '../../utils/injectSaga';
38
37
  import injectReducer from '../../utils/injectReducer';
39
38
 
@@ -235,63 +234,16 @@ export class FTP extends React.Component {
235
234
  }));
236
235
  };
237
236
 
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
-
278
237
  indexOfEnd(targetString, string) {
279
238
  const io = targetString.indexOf(string);
280
239
  return io == -1 ? -1 : io + string.length;
281
240
  }
282
241
 
283
242
  getMessageContent = () => {
284
- const { messageContent, tagsTree = []} = this.state;
243
+ const { messageContent } = this.state;
285
244
  const { formatMessage } = this.props.intl;
286
245
  const { metaEntities, selectedOfferDetails, injectedTags } = this.props;
287
246
  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
- }
295
247
  return (
296
248
  <CapRow>
297
249
  <CapColumn span={11}>
@@ -309,7 +261,6 @@ export class FTP extends React.Component {
309
261
  label={formatMessage(messages.messageHeader)}
310
262
  onChange={this.updateMessageBody}
311
263
  value={messageContent}
312
- errorMessage={errorMessageText}
313
264
  />
314
265
  </div>
315
266
  </CapColumn>
@@ -21,10 +21,6 @@ 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
- },
28
24
  selectTag: {
29
25
  id: 'creatives.containersV2.FTP.selectTag',
30
26
  defaultMessage: 'Select tag',