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