@capillarytech/creatives-library 8.0.292-alpha.0 → 8.0.292-alpha.2

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.292-alpha.0",
4
+ "version": "8.0.292-alpha.2",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -69,6 +69,23 @@ import {
69
69
  import { MANUAL_CAROUSEL } from '../MobilePushNew/constants';
70
70
  import { BIG_HTML } from '../InApp/constants';
71
71
 
72
+ /**
73
+ * Returns true if value is "deep empty": no errors present.
74
+ * - null/undefined: empty
75
+ * - string: empty if length === 0
76
+ * - array: empty if length === 0
77
+ * - plain object (e.g. { android: [], ios: [], generic: [] }): empty only if every value is deep-empty
78
+ */
79
+ function isDeepEmpty(value) {
80
+ if (value == null) return true;
81
+ if (typeof value === 'string') return value.length === 0;
82
+ if (Array.isArray(value)) return value.length === 0;
83
+ if (typeof value === 'object') {
84
+ return Object.values(value).every(isDeepEmpty);
85
+ }
86
+ return false;
87
+ }
88
+
72
89
  const classPrefix = 'add-creatives-section';
73
90
  const CREATIVES_CONTAINER = 'creativesContainer';
74
91
 
@@ -1779,8 +1796,8 @@ export class Creatives extends React.Component {
1779
1796
  showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
1780
1797
  const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
1781
1798
  const standardMsgs = get(errorMessagesFromFormBuilder, constants.STANDARD_ERROR_MSG, []);
1782
- const hasLiquid = Array.isArray(liquidMsgs) ? liquidMsgs.length > 0 : !isEmpty(liquidMsgs);
1783
- const hasStandard = Array.isArray(standardMsgs) ? standardMsgs.length > 0 : !isEmpty(standardMsgs);
1799
+ const hasLiquid = !isDeepEmpty(liquidMsgs);
1800
+ const hasStandard = !isDeepEmpty(standardMsgs);
1784
1801
  const isLiquidValidationError = hasLiquid || hasStandard;
1785
1802
  // Don't overwrite existing liquid error with empty only for Mobile Push OLD (FormBuilder/clear calls empty there); SMS/others clear on input change
1786
1803
  const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
@@ -50,7 +50,7 @@ import {
50
50
  LAYOUT_RADIO_OPTIONS,
51
51
  IOS_CAPITAL,
52
52
  } from "./constants";
53
- import { INAPP, SMS } from "../CreativesContainer/constants";
53
+ import { GENERIC, INAPP, SMS } from "../CreativesContainer/constants";
54
54
  import {
55
55
  ALL, TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
56
56
  } from "../Whatsapp/constants";
@@ -1079,10 +1079,36 @@ export const InApp = (props) => {
1079
1079
  // Validation middleware for tag validation (both liquid and non-liquid flow)
1080
1080
  // Liquid validation (extractTags) runs only in library mode
1081
1081
  const validationMiddleWare = async () => {
1082
+ // Normalize validator bucket keys to component state keys (ANDROID, IOS_CAPITAL, GENERIC)
1083
+ // so we don't merge e.g. 'android'/'ios'/'generic' with ANDROID/IOS/GENERIC and get duplicate/stale keys
1084
+ const normalizeErrorBuckets = (errors) => {
1085
+ const normalized = {
1086
+ [ANDROID]: [],
1087
+ [IOS_CAPITAL]: [],
1088
+ [GENERIC]: [],
1089
+ };
1090
+ if (!errors || typeof errors !== 'object') return normalized;
1091
+ const keyMap = {
1092
+ ANDROID,
1093
+ android: ANDROID,
1094
+ [IOS_CAPITAL]: IOS_CAPITAL,
1095
+ [IOS]: IOS_CAPITAL,
1096
+ ios: IOS_CAPITAL,
1097
+ GENERIC,
1098
+ generic: GENERIC,
1099
+ };
1100
+ for (const [key, value] of Object.entries(errors)) {
1101
+ const targetKey = keyMap[key];
1102
+ if (targetKey != null && Array.isArray(value)) {
1103
+ normalized[targetKey] = value;
1104
+ }
1105
+ }
1106
+ return normalized;
1107
+ };
1082
1108
  const onError = ({ standardErrors, liquidErrors }) => {
1083
1109
  setErrorMessage((prev) => ({
1084
- STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
1085
- LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
1110
+ STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...normalizeErrorBuckets(standardErrors) },
1111
+ LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...normalizeErrorBuckets(liquidErrors) },
1086
1112
  }));
1087
1113
  };
1088
1114
  const onSuccess = () => {
@@ -817,7 +817,7 @@ export const InappAdvanced = (props) => {
817
817
 
818
818
  // Liquid validation (extractTags) only in library mode
819
819
  if (!isFullMode) {
820
- validateInAppContent(payload, {
820
+ await validateInAppContent(payload, {
821
821
  currentTab: panes === ANDROID ? 1 : 2,
822
822
  onError,
823
823
  onSuccess,
@@ -837,7 +837,7 @@ export const InappAdvanced = (props) => {
837
837
  singleTab: getSingleTab(accountData),
838
838
  });
839
839
  } else {
840
- onSuccess();
840
+ await onSuccess();
841
841
  }
842
842
  };
843
843
 
@@ -98,11 +98,11 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
98
98
  // Library mode: on Done click (transition to isGetFormData), run extractTags only when no existing validation errors (braces, personalization, etc.)
99
99
  if (nextProps.isGetFormData && !this.props.isGetFormData && !nextProps.isFullMode) {
100
100
  // If form already has validation errors (braces, personalization tags, etc.), do not call extractTags; just fail
101
- if (!this.state.isFormValid && nextProps.onValidationFail) {
101
+ if (!this.state.isFormValid && typeof nextProps.onValidationFail === 'function') {
102
102
  nextProps.onValidationFail();
103
103
  return;
104
104
  }
105
- if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter && nextProps.onValidationFail) {
105
+ if (typeof nextProps.getLiquidTags === 'function' && typeof nextProps.showLiquidErrorInFooter === 'function' && typeof nextProps.onValidationFail === 'function') {
106
106
  const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
107
107
  validateMobilePushContent(formDataArr, {
108
108
  currentTab: this.state.currentTab,
@@ -115,18 +115,27 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
115
115
  const toArray = (v) => (Array.isArray(v) ? v : (v && typeof v === 'object' ? [].concat(...Object.values(v)) : []));
116
116
  const STANDARD_ERROR_MSG = toArray(standardErrors);
117
117
  const LIQUID_ERROR_MSG = toArray(liquidErrors);
118
- nextProps.showLiquidErrorInFooter(
119
- { STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
120
- this.state.currentTab
121
- );
122
- nextProps.onValidationFail();
118
+ if (typeof nextProps.showLiquidErrorInFooter === 'function') {
119
+ nextProps.showLiquidErrorInFooter(
120
+ { STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
121
+ this.state.currentTab
122
+ );
123
+ }
124
+ // Only trigger onValidationFail when there are actual errors; skip when helper called onError with empty arrays (reset case)
125
+ if ((STANDARD_ERROR_MSG.length > 0 || LIQUID_ERROR_MSG.length > 0) && typeof nextProps.onValidationFail === 'function') {
126
+ nextProps.onValidationFail();
127
+ }
123
128
  },
124
129
  onSuccess: () => {
125
- nextProps.getFormLibraryData(this.getFormData());
130
+ if (typeof nextProps.getFormLibraryData === 'function') {
131
+ nextProps.getFormLibraryData(this.getFormData());
132
+ }
126
133
  },
127
134
  });
128
- } else {
135
+ } else if (typeof nextProps.getFormLibraryData === 'function') {
129
136
  nextProps.getFormLibraryData(this.getFormData());
137
+ } else if (typeof nextProps.onValidationFail === 'function') {
138
+ nextProps.onValidationFail();
130
139
  }
131
140
  } else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
132
141
  this.startValidation();
@@ -69,6 +69,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
69
69
  schema: {},
70
70
  currentTab: 1,
71
71
  editData: {},
72
+ errorData: [],
72
73
  loading: false,
73
74
  isFormValid: true,
74
75
  injectedTags: {},
@@ -133,11 +134,11 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
133
134
  // Library mode: on Done click (transition to isGetFormData), run extractTags only when no existing validation errors (braces, personalization, etc.)
134
135
  if (nextProps.isGetFormData && !this.props.isGetFormData && !nextProps.isFullMode) {
135
136
  // If form already has validation errors (braces, personalization tags, etc.), do not call extractTags; just fail
136
- if (!this.state.isFormValid && nextProps.onValidationFail) {
137
- nextProps.onValidationFail();
137
+ if (!this.state.isFormValid) {
138
+ nextProps.onValidationFail?.();
138
139
  return;
139
140
  }
140
- if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter && nextProps.onValidationFail) {
141
+ if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter) {
141
142
  const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
142
143
  validateMobilePushContent(formDataArr, {
143
144
  currentTab: this.state.currentTab,
@@ -154,7 +155,11 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
154
155
  { STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
155
156
  this.state.currentTab
156
157
  );
157
- nextProps.onValidationFail();
158
+ // Only treat as validation failure when there are actual errors; skip when helper called onError with empty reset payload
159
+ const hasErrors = STANDARD_ERROR_MSG.length > 0 || LIQUID_ERROR_MSG.length > 0;
160
+ if (hasErrors) {
161
+ nextProps.onValidationFail?.();
162
+ }
158
163
  },
159
164
  onSuccess: () => {
160
165
  nextProps.getFormLibraryData(this.getFormData());
@@ -741,8 +746,8 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
741
746
  const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
742
747
  if (!_.isEmpty(this.state.formData) && !this.state.isFormValid) {
743
748
  let tab = this.state.currentTab;
744
- const isAndroidInvalid = Object.values(errorData[0] || {}).includes(true);
745
- const isIosInvalid = Object.values(errorData[1] || {}).includes(true);
749
+ const isAndroidInvalid = Object.values(errorData?.[0] || {}).includes(true);
750
+ const isIosInvalid = Object.values(errorData?.[1] || {}).includes(true);
746
751
  const isIosTabVisible = get(schema, 'containers[0].panes[1].isSupported', true) !== false;
747
752
  if (isAndroidInvalid) {
748
753
  tab = 1;
@@ -2329,7 +2334,7 @@ Edit.propTypes = {
2329
2334
  getFormLibraryData: PropTypes.func,
2330
2335
  isGetFormData: PropTypes.bool,
2331
2336
  Create: PropTypes.object,
2332
- onValidationFail: PropTypes.bool,
2337
+ onValidationFail: PropTypes.func,
2333
2338
  onPreviewContentClicked: PropTypes.func,
2334
2339
  onTestContentClicked: PropTypes.func,
2335
2340
  creativesMode: PropTypes.string,
@@ -80,12 +80,6 @@ export const validateMessageContent = (value, formatMessage, messages, validatio
80
80
  isFullMode,
81
81
  }) || {};
82
82
 
83
- if (validationResponse?.unsupportedTags?.length) {
84
- return formatMessage(globalMessages.unsupportedTagsValidationError, {
85
- unsupportedTags: validationResponse.unsupportedTags.join(', '),
86
- });
87
- }
88
-
89
83
  if (validationResponse?.isBraceError) {
90
84
  return formatMessage(globalMessages.unbalanacedCurlyBraces);
91
85
  }
@@ -317,21 +317,6 @@ describe('validation', () => {
317
317
  expect(mockFormatMessage).toHaveBeenCalledWith(mockMessages.personalizationTokensErrorMessage);
318
318
  });
319
319
 
320
- it('should return unsupported tags error when validateTags returns unsupportedTags', () => {
321
- validateTags.mockReturnValue({ unsupportedTags: ['invalidTag', 'otherTag'] });
322
- const result = validateMessageContent(
323
- 'Hello {{invalidTag}}',
324
- mockFormatMessage,
325
- mockMessages,
326
- mockValidationConfig
327
- );
328
- expect(result).toBe('Unsupported tags: invalidTag, otherTag');
329
- expect(mockFormatMessage).toHaveBeenCalledWith(
330
- globalMessages.unsupportedTagsValidationError,
331
- { unsupportedTags: 'invalidTag, otherTag' }
332
- );
333
- });
334
-
335
320
  it('should pass isFullMode to validateTags when provided', () => {
336
321
  validateTags.mockReturnValue({});
337
322
  validateMessageContent('Valid message', mockFormatMessage, mockMessages, mockValidationConfig, true);