@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.
- package/constants/unified.js +5 -1
- package/initialState.js +0 -2
- package/package.json +1 -1
- package/utils/common.js +5 -8
- package/utils/commonUtils.js +36 -93
- package/utils/tagValidations.js +83 -223
- package/utils/tests/commonUtil.test.js +147 -124
- package/utils/tests/tagValidations.test.js +441 -358
- package/v2Components/ErrorInfoNote/index.js +2 -5
- package/v2Components/FormBuilder/index.js +137 -203
- package/v2Components/FormBuilder/messages.js +0 -8
- package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
- package/v2Containers/Cap/mockData.js +0 -14
- package/v2Containers/Cap/reducer.js +3 -55
- package/v2Containers/Cap/tests/reducer.test.js +0 -102
- package/v2Containers/CreativesContainer/SlideBoxContent.js +5 -1
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -5
- package/v2Containers/CreativesContainer/constants.js +6 -0
- package/v2Containers/CreativesContainer/index.js +47 -7
- package/v2Containers/Email/index.js +1 -5
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +23 -70
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -120
- package/v2Containers/FTP/index.js +2 -51
- package/v2Containers/FTP/messages.js +0 -4
- package/v2Containers/InApp/index.js +35 -107
- package/v2Containers/InApp/tests/index.test.js +17 -6
- package/v2Containers/InappAdvance/index.js +4 -112
- package/v2Containers/InappAdvance/tests/index.test.js +2 -0
- package/v2Containers/Line/Container/Text/index.js +0 -1
- package/v2Containers/MobilePush/Create/index.js +59 -19
- package/v2Containers/MobilePush/Edit/index.js +48 -20
- package/v2Containers/MobilePushNew/index.js +12 -32
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/index.js +12 -37
- package/v2Containers/Sms/Create/index.js +39 -3
- package/v2Containers/Sms/Create/messages.js +4 -0
- package/v2Containers/Sms/Edit/index.js +35 -3
- package/v2Containers/Sms/commonMethods.js +3 -6
- package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
- package/v2Containers/SmsTrai/Edit/index.js +11 -47
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/TemplatesV2/index.js +28 -13
- package/v2Containers/Viber/index.js +0 -1
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
- package/v2Containers/WebPush/Create/index.js +2 -2
- package/v2Containers/WebPush/Create/utils/validation.js +17 -8
- package/v2Containers/WebPush/Create/utils/validation.test.js +44 -24
- package/v2Containers/Whatsapp/index.js +9 -17
- package/v2Containers/Zalo/index.js +3 -11
|
@@ -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 {
|
|
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 (
|
|
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?.
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
755
|
-
if (!validationResult?.valid || (hasUnsupportedTags && !isFullMode)) {
|
|
756
|
+
if (!validationResult?.valid) {
|
|
756
757
|
setTagValidationError(validationResult);
|
|
757
|
-
//
|
|
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
|
|
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
|
-
//
|
|
960
|
-
if (
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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:
|
|
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={
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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}
|
|
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,
|
|
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
|
|
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',
|