@capillarytech/creatives-library 8.0.278 → 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.278",
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) {
@@ -691,7 +710,7 @@ const EmailHTMLEditor = (props) => {
691
710
  // 2. Validate Unsubscribe Tag when feature is OFF (when flag is false we require unsubscribe)
692
711
  // When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is true: do NOT validate for unsubscribe (aligned with FormBuilder).
693
712
  // When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is false: validate and require unsubscribe tag.
694
- if (!isEmailUnsubscribeTagMandatory() && moduleType === OUTBOUND) {
713
+ if (!isFullMode && !isEmailUnsubscribeTagMandatory() && moduleType === OUTBOUND) {
695
714
  // Check if content contains unsubscribe tag (either {{unsubscribe}} or {{unsubscribe(#...)})
696
715
  const unsubscribeRegex = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g; // eslint-disable-line no-useless-escape
697
716
  const hasUnsubscribeTag = unsubscribeRegex.test(htmlContent);
@@ -700,11 +719,13 @@ const EmailHTMLEditor = (props) => {
700
719
  // Show error notification
701
720
  const missingTagsMsg = intl.formatMessage(formBuilderMessages.missingTags);
702
721
  const errorMessage = `${missingTagsMsg} unsubscribe`;
703
- CapNotification.error({
704
- message: 'ERROR ! ! !',
705
- description: errorMessage,
706
- duration: 5,
707
- });
722
+ setTimeout(() => {
723
+ CapNotification.error({
724
+ message: 'ERROR ! ! !',
725
+ description: errorMessage,
726
+ duration: 5,
727
+ });
728
+ }, 0);
708
729
 
709
730
  // Reset parent state so next click is detected as a change
710
731
  if (onValidationFail) {
@@ -718,7 +739,9 @@ const EmailHTMLEditor = (props) => {
718
739
  // 3. Validate Content Tags
719
740
  // For NON-liquid orgs: BLOCKING validation (matches CK/BEE behavior)
720
741
  // For liquid orgs: Non-blocking (extractTags API will validate)
721
- 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) {
722
745
  const validationResult = validateTags({
723
746
  content: htmlContent,
724
747
  tagsParam: tags,
@@ -726,12 +749,12 @@ const EmailHTMLEditor = (props) => {
726
749
  location,
727
750
  tagModule: getDefaultTags,
728
751
  eventContextTags,
752
+ isFullMode,
729
753
  });
730
754
 
731
755
  const hasUnsupportedTags = validationResult?.unsupportedTags?.length > 0;
732
- if (!validationResult?.valid || hasUnsupportedTags) {
756
+ if (!validationResult?.valid || (hasUnsupportedTags && !isFullMode)) {
733
757
  setTagValidationError(validationResult);
734
-
735
758
  // IMPORTANT: For non-liquid orgs, block save (like CK/BEE editor)
736
759
  // For liquid orgs, continue (extractTags API will validate)
737
760
  if (!isLiquidEnabled) {
@@ -1157,7 +1180,7 @@ const EmailHTMLEditor = (props) => {
1157
1180
  isFullMode={isFullMode}
1158
1181
  onErrorAcknowledged={handleErrorAcknowledged}
1159
1182
  onValidationChange={handleValidationChange}
1160
- apiValidationErrors={apiValidationErrors}
1183
+ apiValidationErrors={mergedApiValidationErrors}
1161
1184
  />
1162
1185
  </CapColumn>
1163
1186
  </CapRow>
@@ -1016,36 +1016,6 @@ describe('EmailHTMLEditor', () => {
1016
1016
  }, { timeout: 3000 });
1017
1017
  });
1018
1018
 
1019
- it('blocks save when unsubscribe validation is on (flag false) and tag is missing', async () => {
1020
- // When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is false we validate and require unsubscribe
1021
- isEmailUnsubscribeTagMandatory.mockReturnValue(false);
1022
- const onValidationFail = jest.fn();
1023
- const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
1024
-
1025
- // Set subject via input and content via HTMLEditor mock
1026
- const { rerender } = renderWithIntl({
1027
- onValidationFail,
1028
- isGetFormData: false,
1029
- moduleType: 'OUTBOUND',
1030
- });
1031
- const input = screen.getByTestId('subject-input');
1032
- fireEvent.change(input, { target: { value: 'Valid Subject' } });
1033
- // Trigger content change to set htmlContent
1034
- const changeButton = screen.getByTestId('trigger-content-change');
1035
- fireEvent.click(changeButton);
1036
- // Now trigger save
1037
- rerender(
1038
- <IntlProvider locale="en" messages={{}}>
1039
- <EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData moduleType="OUTBOUND" />
1040
- </IntlProvider>
1041
- );
1042
-
1043
- await waitFor(() => {
1044
- expect(CapNotification.error).toHaveBeenCalled();
1045
- expect(onValidationFail).toHaveBeenCalled();
1046
- }, { timeout: 3000 });
1047
- });
1048
-
1049
1019
  it('allows save when unsubscribe validation is on and tag is present', () => {
1050
1020
  isEmailUnsubscribeTagMandatory.mockReturnValue(false);
1051
1021
  renderWithIntl({
@@ -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) {