@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 +1 -1
- package/v2Containers/CreativesContainer/index.js +19 -2
- package/v2Containers/InApp/index.js +29 -3
- package/v2Containers/InappAdvance/index.js +2 -2
- package/v2Containers/MobilePush/Create/index.js +18 -9
- package/v2Containers/MobilePush/Edit/index.js +12 -7
- package/v2Containers/WebPush/Create/utils/validation.js +0 -6
- package/v2Containers/WebPush/Create/utils/validation.test.js +0 -15
package/package.json
CHANGED
|
@@ -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 =
|
|
1783
|
-
const hasStandard =
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
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
|
|
137
|
-
nextProps.onValidationFail();
|
|
137
|
+
if (!this.state.isFormValid) {
|
|
138
|
+
nextProps.onValidationFail?.();
|
|
138
139
|
return;
|
|
139
140
|
}
|
|
140
|
-
if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter
|
|
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
|
-
|
|
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.
|
|
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);
|