@capillarytech/creatives-library 8.0.277 → 8.0.279
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/package.json +1 -1
- package/utils/tagValidations.js +6 -2
- package/utils/tests/tagValidations.test.js +1 -1
- package/v2Components/FormBuilder/index.js +6 -5
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +37 -13
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +43 -32
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +26 -0
- package/v2Containers/InApp/index.js +2 -0
- package/v2Containers/InappAdvance/index.js +2 -0
package/package.json
CHANGED
package/utils/tagValidations.js
CHANGED
|
@@ -201,6 +201,7 @@ export const validateTags = ({
|
|
|
201
201
|
unsupportedTags: [],
|
|
202
202
|
isBraceError: false,
|
|
203
203
|
};
|
|
204
|
+
// Mandatory-tags check: only when we have a tags list and are in library mode
|
|
204
205
|
if (tags && tags.length && !isFullMode) {
|
|
205
206
|
lodashForEach(tags, ({
|
|
206
207
|
definition: {
|
|
@@ -217,6 +218,9 @@ export const validateTags = ({
|
|
|
217
218
|
}
|
|
218
219
|
});
|
|
219
220
|
});
|
|
221
|
+
}
|
|
222
|
+
// In library mode, always scan content for {{...}} and flag unsupported tags (even when tags list is empty)
|
|
223
|
+
if (!isFullMode && content) {
|
|
220
224
|
const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
|
|
221
225
|
let match = regex.exec(content);
|
|
222
226
|
while (match !== null) {
|
|
@@ -224,8 +228,8 @@ export const validateTags = ({
|
|
|
224
228
|
const tagIndex = match?.index;
|
|
225
229
|
match = regex.exec(content);
|
|
226
230
|
let ifSupported = false;
|
|
227
|
-
lodashForEach(tags, (tag) => {
|
|
228
|
-
if (tag
|
|
231
|
+
lodashForEach(tags || [], (tag) => {
|
|
232
|
+
if (tag?.definition?.value === tagValue) {
|
|
229
233
|
ifSupported = true;
|
|
230
234
|
}
|
|
231
235
|
});
|
|
@@ -104,7 +104,7 @@ describe("validateTags", () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
expect(result.valid).toEqual(true);
|
|
107
|
-
expect(result.unsupportedTags).toEqual([]);
|
|
107
|
+
expect(result.unsupportedTags).toEqual(["tag1", "tag2", "tag3"]);
|
|
108
108
|
expect(result.isBraceError).toEqual(false);
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -1247,10 +1247,11 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1247
1247
|
},
|
|
1248
1248
|
}),
|
|
1249
1249
|
() => {
|
|
1250
|
-
// Callback after the state is updated
|
|
1251
1250
|
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
|
|
1252
1251
|
}
|
|
1253
1252
|
);
|
|
1253
|
+
// Show toast for liquid flow too so user sees error (scenario 3)
|
|
1254
|
+
this.openNotificationWithIcon('error', errorString, "email-validation-error");
|
|
1254
1255
|
} else {
|
|
1255
1256
|
this.openNotificationWithIcon('error', errorString, "email-validation-error");
|
|
1256
1257
|
}
|
|
@@ -1518,7 +1519,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1518
1519
|
response.isBraceError = false;
|
|
1519
1520
|
response.isContentEmpty = false;
|
|
1520
1521
|
const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content ;
|
|
1521
|
-
|
|
1522
|
+
// Run tag validation (missing + unsupported) for library mode OR for email channel (scenario 4: full mode email with CK)
|
|
1523
|
+
if(tags && tags.length && (!isFullMode || isEmail)) {
|
|
1522
1524
|
_.forEach(tags, (tag) => {
|
|
1523
1525
|
_.forEach(tag.definition.supportedModules, (module) => {
|
|
1524
1526
|
if (module.mandatory && (currentModule === module.context)) {
|
|
@@ -1581,9 +1583,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1581
1583
|
});
|
|
1582
1584
|
|
|
1583
1585
|
ifSupported = ifSupported || this.checkIfSupportedTag(tagValue, injectedTags);
|
|
1584
|
-
// Only add to unsupportedTags if not inside a {% ... %} block
|
|
1585
|
-
|
|
1586
|
-
if (!ifSupported && !this.liquidFlow() && !isInsideLiquidBlock(content, tagIndex)) {
|
|
1586
|
+
// Only add to unsupportedTags if not inside a {% ... %} block (scenario 3: liquid orgs also get unsupported-tag errors)
|
|
1587
|
+
if (!ifSupported && !isInsideLiquidBlock(content, tagIndex)) {
|
|
1587
1588
|
response.unsupportedTags.push(tagValue);
|
|
1588
1589
|
response.valid = false;
|
|
1589
1590
|
}
|
|
@@ -108,6 +108,24 @@ const EmailHTMLEditor = (props) => {
|
|
|
108
108
|
standardErrors: [],
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
// Merge tag validation errors (unsupported/missing) into apiValidationErrors so they show in ValidationErrorDisplay
|
|
112
|
+
const mergedApiValidationErrors = useMemo(() => {
|
|
113
|
+
const tagMessages = [];
|
|
114
|
+
if (tagValidationError?.unsupportedTags?.length) {
|
|
115
|
+
tagMessages.push(`Unsupported tags are: ${tagValidationError.unsupportedTags.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
if (tagValidationError?.missingTags?.length && !isEmailUnsubscribeTagMandatory()) {
|
|
118
|
+
tagMessages.push(`Missing tags are: ${tagValidationError.missingTags.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
if (tagMessages.length === 0) {
|
|
121
|
+
return apiValidationErrors;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
liquidErrors: apiValidationErrors?.liquidErrors || [],
|
|
125
|
+
standardErrors: [...(apiValidationErrors?.standardErrors || []), ...tagMessages],
|
|
126
|
+
};
|
|
127
|
+
}, [apiValidationErrors, tagValidationError]);
|
|
128
|
+
|
|
111
129
|
// Refs for tracking initialization and previous values
|
|
112
130
|
const contentInitializedRef = useRef(false);
|
|
113
131
|
const subjectInitializedRef = useRef(false);
|
|
@@ -479,6 +497,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
479
497
|
location,
|
|
480
498
|
tagModule: getDefaultTags,
|
|
481
499
|
eventContextTags,
|
|
500
|
+
isFullMode,
|
|
482
501
|
});
|
|
483
502
|
|
|
484
503
|
if (!validationResult.valid) {
|
|
@@ -688,9 +707,10 @@ const EmailHTMLEditor = (props) => {
|
|
|
688
707
|
return;
|
|
689
708
|
}
|
|
690
709
|
|
|
691
|
-
// 2. Validate Unsubscribe Tag (
|
|
692
|
-
//
|
|
693
|
-
|
|
710
|
+
// 2. Validate Unsubscribe Tag when feature is OFF (when flag is false we require unsubscribe)
|
|
711
|
+
// When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is true: do NOT validate for unsubscribe (aligned with FormBuilder).
|
|
712
|
+
// When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is false: validate and require unsubscribe tag.
|
|
713
|
+
if (!isFullMode && !isEmailUnsubscribeTagMandatory() && moduleType === OUTBOUND) {
|
|
694
714
|
// Check if content contains unsubscribe tag (either {{unsubscribe}} or {{unsubscribe(#...)})
|
|
695
715
|
const unsubscribeRegex = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g; // eslint-disable-line no-useless-escape
|
|
696
716
|
const hasUnsubscribeTag = unsubscribeRegex.test(htmlContent);
|
|
@@ -699,17 +719,19 @@ const EmailHTMLEditor = (props) => {
|
|
|
699
719
|
// Show error notification
|
|
700
720
|
const missingTagsMsg = intl.formatMessage(formBuilderMessages.missingTags);
|
|
701
721
|
const errorMessage = `${missingTagsMsg} unsubscribe`;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
722
|
+
setTimeout(() => {
|
|
723
|
+
CapNotification.error({
|
|
724
|
+
message: 'ERROR ! ! !',
|
|
725
|
+
description: errorMessage,
|
|
726
|
+
duration: 5,
|
|
727
|
+
});
|
|
728
|
+
}, 0);
|
|
707
729
|
|
|
708
730
|
// Reset parent state so next click is detected as a change
|
|
709
731
|
if (onValidationFail) {
|
|
710
732
|
onValidationFail();
|
|
711
733
|
}
|
|
712
|
-
// Block save - unsubscribe tag is
|
|
734
|
+
// Block save - unsubscribe tag is required when validation is enabled
|
|
713
735
|
return;
|
|
714
736
|
}
|
|
715
737
|
}
|
|
@@ -717,7 +739,9 @@ const EmailHTMLEditor = (props) => {
|
|
|
717
739
|
// 3. Validate Content Tags
|
|
718
740
|
// For NON-liquid orgs: BLOCKING validation (matches CK/BEE behavior)
|
|
719
741
|
// For liquid orgs: Non-blocking (extractTags API will validate)
|
|
720
|
-
if
|
|
742
|
+
// In library mode, always validate when there is content (even if tags list is empty)
|
|
743
|
+
const shouldValidateTags = (tags.length > 0 || !isEmpty(injectedTags)) || (!isFullMode && !!htmlContent);
|
|
744
|
+
if (shouldValidateTags) {
|
|
721
745
|
const validationResult = validateTags({
|
|
722
746
|
content: htmlContent,
|
|
723
747
|
tagsParam: tags,
|
|
@@ -725,12 +749,12 @@ const EmailHTMLEditor = (props) => {
|
|
|
725
749
|
location,
|
|
726
750
|
tagModule: getDefaultTags,
|
|
727
751
|
eventContextTags,
|
|
752
|
+
isFullMode,
|
|
728
753
|
});
|
|
729
754
|
|
|
730
755
|
const hasUnsupportedTags = validationResult?.unsupportedTags?.length > 0;
|
|
731
|
-
if (!validationResult?.valid || hasUnsupportedTags) {
|
|
756
|
+
if (!validationResult?.valid || (hasUnsupportedTags && !isFullMode)) {
|
|
732
757
|
setTagValidationError(validationResult);
|
|
733
|
-
|
|
734
758
|
// IMPORTANT: For non-liquid orgs, block save (like CK/BEE editor)
|
|
735
759
|
// For liquid orgs, continue (extractTags API will validate)
|
|
736
760
|
if (!isLiquidEnabled) {
|
|
@@ -1156,7 +1180,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
1156
1180
|
isFullMode={isFullMode}
|
|
1157
1181
|
onErrorAcknowledged={handleErrorAcknowledged}
|
|
1158
1182
|
onValidationChange={handleValidationChange}
|
|
1159
|
-
apiValidationErrors={
|
|
1183
|
+
apiValidationErrors={mergedApiValidationErrors}
|
|
1160
1184
|
/>
|
|
1161
1185
|
</CapColumn>
|
|
1162
1186
|
</CapRow>
|
|
@@ -386,6 +386,7 @@ const defaultProps = {
|
|
|
386
386
|
isGetFormData: false,
|
|
387
387
|
getFormdata: jest.fn(),
|
|
388
388
|
templateData: null,
|
|
389
|
+
isEditEmail: true,
|
|
389
390
|
EmailLayout: null,
|
|
390
391
|
getLiquidTags: jest.fn((content, callback) => callback({ askAiraResponse: { data: [] }, isError: false })),
|
|
391
392
|
showLiquidErrorInFooter: jest.fn(),
|
|
@@ -642,6 +643,45 @@ describe('EmailHTMLEditor', () => {
|
|
|
642
643
|
// Should trigger fetch for new template ID
|
|
643
644
|
expect(emailActions.getTemplateDetails).toHaveBeenCalled();
|
|
644
645
|
});
|
|
646
|
+
|
|
647
|
+
it('does not fetch template details when isEditEmail is false even if templateData has _id (create flow)', async () => {
|
|
648
|
+
const emailActions = {
|
|
649
|
+
...defaultProps.emailActions,
|
|
650
|
+
getTemplateDetails: jest.fn(),
|
|
651
|
+
};
|
|
652
|
+
renderWithIntl({
|
|
653
|
+
isEditEmail: false,
|
|
654
|
+
templateData: { _id: 'stale-template-id', name: 'Stale' },
|
|
655
|
+
params: {},
|
|
656
|
+
location: { query: {}, pathname: '/email/create' },
|
|
657
|
+
Email: { templateDetails: null, getTemplateDetailsInProgress: false, fetchingCmsData: false },
|
|
658
|
+
emailActions,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
await waitFor(() => {
|
|
662
|
+
expect(emailActions.getTemplateDetails).not.toHaveBeenCalled();
|
|
663
|
+
}, { timeout: 500 });
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('uses templateData._id for currentTemplateId and fetches when isEditEmail is true', async () => {
|
|
667
|
+
const emailActions = {
|
|
668
|
+
...defaultProps.emailActions,
|
|
669
|
+
getTemplateDetails: jest.fn(),
|
|
670
|
+
};
|
|
671
|
+
const templateId = 'edit-template-id';
|
|
672
|
+
renderWithIntl({
|
|
673
|
+
isEditEmail: true,
|
|
674
|
+
templateData: { _id: templateId, name: 'Edit Template' },
|
|
675
|
+
params: {},
|
|
676
|
+
location: { query: {}, pathname: '/email/create' },
|
|
677
|
+
Email: { templateDetails: null, getTemplateDetailsInProgress: false, fetchingCmsData: false },
|
|
678
|
+
emailActions,
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
await waitFor(() => {
|
|
682
|
+
expect(emailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
683
|
+
}, { timeout: 500 });
|
|
684
|
+
});
|
|
645
685
|
});
|
|
646
686
|
|
|
647
687
|
describe('Subject Handling', () => {
|
|
@@ -976,44 +1016,15 @@ describe('EmailHTMLEditor', () => {
|
|
|
976
1016
|
}, { timeout: 3000 });
|
|
977
1017
|
});
|
|
978
1018
|
|
|
979
|
-
it('
|
|
980
|
-
isEmailUnsubscribeTagMandatory.mockReturnValue(
|
|
981
|
-
const onValidationFail = jest.fn();
|
|
982
|
-
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
983
|
-
|
|
984
|
-
// Set subject via input and content via HTMLEditor mock
|
|
985
|
-
const { rerender } = renderWithIntl({
|
|
986
|
-
onValidationFail,
|
|
987
|
-
isGetFormData: false,
|
|
988
|
-
moduleType: 'OUTBOUND',
|
|
989
|
-
});
|
|
990
|
-
const input = screen.getByTestId('subject-input');
|
|
991
|
-
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
992
|
-
// Trigger content change to set htmlContent
|
|
993
|
-
const changeButton = screen.getByTestId('trigger-content-change');
|
|
994
|
-
fireEvent.click(changeButton);
|
|
995
|
-
// Now trigger save
|
|
996
|
-
rerender(
|
|
997
|
-
<IntlProvider locale="en" messages={{}}>
|
|
998
|
-
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData moduleType="OUTBOUND" />
|
|
999
|
-
</IntlProvider>
|
|
1000
|
-
);
|
|
1001
|
-
|
|
1002
|
-
await waitFor(() => {
|
|
1003
|
-
expect(CapNotification.error).toHaveBeenCalled();
|
|
1004
|
-
expect(onValidationFail).toHaveBeenCalled();
|
|
1005
|
-
}, { timeout: 3000 });
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
it('allows save when unsubscribe tag is present', () => {
|
|
1009
|
-
isEmailUnsubscribeTagMandatory.mockReturnValue(true);
|
|
1019
|
+
it('allows save when unsubscribe validation is on and tag is present', () => {
|
|
1020
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
1010
1021
|
renderWithIntl({
|
|
1011
1022
|
isGetFormData: true,
|
|
1012
1023
|
subject: 'Valid Subject',
|
|
1013
1024
|
htmlContent: '<p>Content {{unsubscribe}}</p>',
|
|
1014
1025
|
moduleType: 'OUTBOUND',
|
|
1015
1026
|
});
|
|
1016
|
-
// Should proceed with save
|
|
1027
|
+
// Should proceed with save (validation passes)
|
|
1017
1028
|
});
|
|
1018
1029
|
|
|
1019
1030
|
it('blocks save for non-liquid orgs when tag validation fails', async () => {
|
|
@@ -1025,6 +1025,32 @@ describe('useEmailWrapper', () => {
|
|
|
1025
1025
|
expect(mockEmailActions.getTemplateDetails).not.toHaveBeenCalled();
|
|
1026
1026
|
}, { timeout: 1000 });
|
|
1027
1027
|
});
|
|
1028
|
+
|
|
1029
|
+
it('should NOT call getTemplateDetails when isEditEmail is false (create flow)', async () => {
|
|
1030
|
+
const templateId = 'create-flow-id';
|
|
1031
|
+
const createFlowProps = {
|
|
1032
|
+
...newFlowMockProps,
|
|
1033
|
+
isEditEmail: false,
|
|
1034
|
+
params: { id: templateId },
|
|
1035
|
+
location: {
|
|
1036
|
+
pathname: `/email/edit/${templateId}`,
|
|
1037
|
+
query: { id: templateId },
|
|
1038
|
+
},
|
|
1039
|
+
Email: {
|
|
1040
|
+
...newFlowMockProps.Email,
|
|
1041
|
+
templateDetails: null,
|
|
1042
|
+
getTemplateDetailsInProgress: false,
|
|
1043
|
+
},
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
1047
|
+
initialProps: createFlowProps,
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
await waitFor(() => {
|
|
1051
|
+
expect(mockEmailActions.getTemplateDetails).not.toHaveBeenCalled();
|
|
1052
|
+
}, { timeout: 1000 });
|
|
1053
|
+
});
|
|
1028
1054
|
});
|
|
1029
1055
|
});
|
|
1030
1056
|
|
|
@@ -1155,6 +1155,7 @@ export const InApp = (props) => {
|
|
|
1155
1155
|
location,
|
|
1156
1156
|
tagModule: getDefaultTags,
|
|
1157
1157
|
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1158
|
+
isFullMode,
|
|
1158
1159
|
}) || {};
|
|
1159
1160
|
|
|
1160
1161
|
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
@@ -1182,6 +1183,7 @@ export const InApp = (props) => {
|
|
|
1182
1183
|
location,
|
|
1183
1184
|
tagModule: getDefaultTags,
|
|
1184
1185
|
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1186
|
+
isFullMode,
|
|
1185
1187
|
}) || {};
|
|
1186
1188
|
|
|
1187
1189
|
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
@@ -878,6 +878,7 @@ export const InappAdvanced = (props) => {
|
|
|
878
878
|
location,
|
|
879
879
|
tagModule: getDefaultTags,
|
|
880
880
|
eventContextTags: metaEntities?.eventContextTags || [],
|
|
881
|
+
isFullMode,
|
|
881
882
|
}) || {};
|
|
882
883
|
|
|
883
884
|
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
@@ -905,6 +906,7 @@ export const InappAdvanced = (props) => {
|
|
|
905
906
|
location,
|
|
906
907
|
tagModule: getDefaultTags,
|
|
907
908
|
eventContextTags: metaEntities?.eventContextTags || [],
|
|
909
|
+
isFullMode,
|
|
908
910
|
}) || {};
|
|
909
911
|
|
|
910
912
|
if (validationResponse?.unsupportedTags?.length > 0) {
|