@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.277",
4
+ "version": "8.0.279",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -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.definition.value === tagValue) {
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
- if(tags && tags.length && !isFullMode) {
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 and not in liquid flow
1585
- // Tags inside {% %} blocks can contain any dynamic tags and should not be validated
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 (if mandatory)
692
- // Check if unsubscribe tag is mandatory and if it exists in content
693
- if (isEmailUnsubscribeTagMandatory() && moduleType === OUTBOUND) {
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
- CapNotification.error({
703
- message: 'ERROR ! ! !',
704
- description: errorMessage,
705
- duration: 5,
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 mandatory
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 (tags.length > 0 || !isEmpty(injectedTags)) {
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={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('blocks save when unsubscribe tag is mandatory and missing', async () => {
980
- isEmailUnsubscribeTagMandatory.mockReturnValue(true);
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) {