@capillarytech/creatives-library 8.0.291 → 8.0.292-alpha.0
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 +3 -1
- package/initialState.js +0 -2
- package/package.json +1 -1
- package/utils/common.js +5 -8
- package/utils/commonUtils.js +4 -85
- 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/index.js +30 -7
- package/v2Containers/Email/index.js +1 -5
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +23 -70
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +29 -137
- package/v2Containers/FTP/index.js +2 -51
- package/v2Containers/FTP/messages.js +0 -4
- package/v2Containers/InApp/index.js +4 -104
- package/v2Containers/InApp/tests/index.test.js +17 -6
- package/v2Containers/InappAdvance/index.js +4 -108
- package/v2Containers/InappAdvance/tests/index.test.js +2 -0
- package/v2Containers/Line/Container/Text/index.js +0 -1
- package/v2Containers/MobilePush/Create/index.js +42 -19
- package/v2Containers/MobilePush/Edit/index.js +42 -19
- 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/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 -2
- package/v2Containers/WebPush/Create/utils/validation.test.js +59 -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,16 +1085,15 @@ 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
|
const { rerender } = renderWithIntl({
|
|
1180
1090
|
isGetFormData: false,
|
|
1181
|
-
isFullMode:
|
|
1091
|
+
isFullMode: false,
|
|
1182
1092
|
metaEntities: {
|
|
1183
1093
|
tags: {
|
|
1184
1094
|
standard: [{ name: 'customer.name' }],
|
|
1185
1095
|
},
|
|
1186
1096
|
},
|
|
1187
|
-
isLiquidEnabled: true,
|
|
1188
1097
|
getLiquidTags,
|
|
1189
1098
|
});
|
|
1190
1099
|
const input = screen.getByTestId('subject-input');
|
|
@@ -1206,7 +1115,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1206
1115
|
<EmailHTMLEditor
|
|
1207
1116
|
{...defaultProps}
|
|
1208
1117
|
isGetFormData
|
|
1209
|
-
isFullMode
|
|
1118
|
+
isFullMode={false}
|
|
1210
1119
|
metaEntities={{
|
|
1211
1120
|
tags: {
|
|
1212
1121
|
standard: [{ name: 'customer.name' }],
|
|
@@ -1230,11 +1139,10 @@ describe('EmailHTMLEditor', () => {
|
|
|
1230
1139
|
// Ensure no HTML/Label/Liquid errors from HtmlEditor
|
|
1231
1140
|
mockGetAllIssues.mockReturnValue([]);
|
|
1232
1141
|
|
|
1233
|
-
//
|
|
1142
|
+
// Liquid validation runs only in library mode
|
|
1234
1143
|
const { rerender } = renderWithIntl({
|
|
1235
1144
|
isGetFormData: false,
|
|
1236
|
-
isFullMode:
|
|
1237
|
-
isLiquidEnabled: true,
|
|
1145
|
+
isFullMode: false,
|
|
1238
1146
|
getLiquidTags,
|
|
1239
1147
|
});
|
|
1240
1148
|
const input = screen.getByTestId('subject-input');
|
|
@@ -1253,7 +1161,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1253
1161
|
await act(async () => {
|
|
1254
1162
|
rerender(
|
|
1255
1163
|
<IntlProvider locale="en" messages={{}}>
|
|
1256
|
-
<EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} />
|
|
1164
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} />
|
|
1257
1165
|
</IntlProvider>
|
|
1258
1166
|
);
|
|
1259
1167
|
});
|
|
@@ -1280,11 +1188,10 @@ describe('EmailHTMLEditor', () => {
|
|
|
1280
1188
|
// Ensure no HTML/Label/Liquid errors from HtmlEditor
|
|
1281
1189
|
mockGetAllIssues.mockReturnValue([]);
|
|
1282
1190
|
|
|
1283
|
-
//
|
|
1191
|
+
// Liquid validation runs only in library mode
|
|
1284
1192
|
const { rerender } = renderWithIntl({
|
|
1285
1193
|
isGetFormData: false,
|
|
1286
|
-
isFullMode:
|
|
1287
|
-
isLiquidEnabled: true,
|
|
1194
|
+
isFullMode: false,
|
|
1288
1195
|
getLiquidTags,
|
|
1289
1196
|
showLiquidErrorInFooter,
|
|
1290
1197
|
onValidationFail,
|
|
@@ -1305,7 +1212,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1305
1212
|
await act(async () => {
|
|
1306
1213
|
rerender(
|
|
1307
1214
|
<IntlProvider locale="en" messages={{}}>
|
|
1308
|
-
<EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} showLiquidErrorInFooter={showLiquidErrorInFooter} onValidationFail={onValidationFail} />
|
|
1215
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} showLiquidErrorInFooter={showLiquidErrorInFooter} onValidationFail={onValidationFail} />
|
|
1309
1216
|
</IntlProvider>
|
|
1310
1217
|
);
|
|
1311
1218
|
});
|
|
@@ -1325,27 +1232,20 @@ describe('EmailHTMLEditor', () => {
|
|
|
1325
1232
|
return Promise.resolve(true);
|
|
1326
1233
|
});
|
|
1327
1234
|
|
|
1328
|
-
const
|
|
1329
|
-
...defaultProps.emailActions,
|
|
1330
|
-
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
1331
|
-
createTemplate: jest.fn((obj, callback) => {
|
|
1332
|
-
callback({ templateId: { _id: '123', versions: {} } });
|
|
1333
|
-
}),
|
|
1334
|
-
};
|
|
1235
|
+
const getFormdata = jest.fn();
|
|
1335
1236
|
const getLiquidTags = jest.fn((content, callback) => {
|
|
1336
1237
|
callback({ askAiraResponse: { data: [] }, isError: false });
|
|
1337
1238
|
});
|
|
1338
1239
|
// Ensure no HTML/Label/Liquid errors from HtmlEditor
|
|
1339
1240
|
mockGetAllIssues.mockReturnValue([]);
|
|
1340
1241
|
|
|
1341
|
-
//
|
|
1242
|
+
// Liquid validation runs only in library mode; then performSave calls getFormdata
|
|
1342
1243
|
const { rerender } = renderWithIntl({
|
|
1343
1244
|
isGetFormData: false,
|
|
1344
|
-
isFullMode:
|
|
1345
|
-
isLiquidEnabled: true,
|
|
1245
|
+
isFullMode: false,
|
|
1346
1246
|
getLiquidTags,
|
|
1347
|
-
|
|
1348
|
-
|
|
1247
|
+
getFormdata,
|
|
1248
|
+
location: { query: { module: 'library' } },
|
|
1349
1249
|
});
|
|
1350
1250
|
const input = screen.getByTestId('subject-input');
|
|
1351
1251
|
await act(async () => {
|
|
@@ -1363,7 +1263,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1363
1263
|
await act(async () => {
|
|
1364
1264
|
rerender(
|
|
1365
1265
|
<IntlProvider locale="en" messages={{}}>
|
|
1366
|
-
<EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags}
|
|
1266
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} getFormdata={getFormdata} location={{ query: { module: 'library' } }} />
|
|
1367
1267
|
</IntlProvider>
|
|
1368
1268
|
);
|
|
1369
1269
|
});
|
|
@@ -1373,12 +1273,11 @@ describe('EmailHTMLEditor', () => {
|
|
|
1373
1273
|
}, { timeout: 5000 });
|
|
1374
1274
|
|
|
1375
1275
|
await waitFor(() => {
|
|
1376
|
-
expect(
|
|
1276
|
+
expect(getFormdata).toHaveBeenCalled();
|
|
1377
1277
|
}, { timeout: 5000 });
|
|
1378
1278
|
});
|
|
1379
1279
|
|
|
1380
1280
|
it('saves in full mode with create template', async () => {
|
|
1381
|
-
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1382
1281
|
const emailActions = {
|
|
1383
1282
|
...defaultProps.emailActions,
|
|
1384
1283
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1412,7 +1311,6 @@ describe('EmailHTMLEditor', () => {
|
|
|
1412
1311
|
});
|
|
1413
1312
|
|
|
1414
1313
|
it('saves in full mode with edit template', async () => {
|
|
1415
|
-
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1416
1314
|
const emailActions = {
|
|
1417
1315
|
...defaultProps.emailActions,
|
|
1418
1316
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1482,7 +1380,6 @@ describe('EmailHTMLEditor', () => {
|
|
|
1482
1380
|
});
|
|
1483
1381
|
|
|
1484
1382
|
it('handles create template error response', async () => {
|
|
1485
|
-
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1486
1383
|
const emailActions = {
|
|
1487
1384
|
...defaultProps.emailActions,
|
|
1488
1385
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1517,7 +1414,6 @@ describe('EmailHTMLEditor', () => {
|
|
|
1517
1414
|
});
|
|
1518
1415
|
|
|
1519
1416
|
it('handles create template success with getFormdata', async () => {
|
|
1520
|
-
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1521
1417
|
const emailActions = {
|
|
1522
1418
|
...defaultProps.emailActions,
|
|
1523
1419
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1554,7 +1450,6 @@ describe('EmailHTMLEditor', () => {
|
|
|
1554
1450
|
});
|
|
1555
1451
|
|
|
1556
1452
|
it('handles create template success without getFormdata', async () => {
|
|
1557
|
-
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1558
1453
|
const emailActions = {
|
|
1559
1454
|
...defaultProps.emailActions,
|
|
1560
1455
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1590,7 +1485,6 @@ describe('EmailHTMLEditor', () => {
|
|
|
1590
1485
|
});
|
|
1591
1486
|
|
|
1592
1487
|
it('saves in library mode with getFormdata', async () => {
|
|
1593
|
-
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1594
1488
|
const getFormdata = jest.fn();
|
|
1595
1489
|
|
|
1596
1490
|
// Set subject and content via component interactions
|
|
@@ -1618,7 +1512,6 @@ describe('EmailHTMLEditor', () => {
|
|
|
1618
1512
|
});
|
|
1619
1513
|
|
|
1620
1514
|
it('saves in library mode without library module', async () => {
|
|
1621
|
-
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1622
1515
|
const getFormdata = jest.fn();
|
|
1623
1516
|
|
|
1624
1517
|
// Set subject and content via component interactions
|
|
@@ -1758,7 +1651,6 @@ describe('EmailHTMLEditor', () => {
|
|
|
1758
1651
|
renderWithIntl({
|
|
1759
1652
|
getLiquidTags: null,
|
|
1760
1653
|
globalActions,
|
|
1761
|
-
isLiquidEnabled: true,
|
|
1762
1654
|
isGetFormData: true,
|
|
1763
1655
|
subject: 'Valid Subject',
|
|
1764
1656
|
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>
|