@capillarytech/creatives-library 8.0.308 → 8.0.309
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/constants/unified.js +5 -1
- package/initialState.js +0 -2
- package/package.json +1 -1
- package/utils/common.js +5 -8
- package/utils/commonUtils.js +36 -93
- package/utils/tagValidations.js +83 -223
- package/utils/tests/commonUtil.test.js +147 -124
- package/utils/tests/tagValidations.test.js +441 -358
- package/v2Components/ErrorInfoNote/index.js +2 -5
- package/v2Components/FormBuilder/index.js +137 -203
- package/v2Components/FormBuilder/messages.js +0 -8
- package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
- package/v2Containers/Cap/mockData.js +0 -14
- package/v2Containers/Cap/reducer.js +3 -55
- package/v2Containers/Cap/tests/reducer.test.js +0 -102
- package/v2Containers/CreativesContainer/SlideBoxContent.js +5 -1
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -5
- package/v2Containers/CreativesContainer/constants.js +6 -0
- package/v2Containers/CreativesContainer/index.js +47 -7
- package/v2Containers/Email/index.js +1 -5
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +23 -70
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -120
- package/v2Containers/FTP/index.js +2 -51
- package/v2Containers/FTP/messages.js +0 -4
- package/v2Containers/InApp/index.js +35 -107
- package/v2Containers/InApp/tests/index.test.js +17 -6
- package/v2Containers/InappAdvance/index.js +4 -112
- package/v2Containers/InappAdvance/tests/index.test.js +2 -0
- package/v2Containers/Line/Container/Text/index.js +0 -1
- package/v2Containers/MobilePush/Create/index.js +59 -19
- package/v2Containers/MobilePush/Edit/index.js +48 -20
- package/v2Containers/MobilePushNew/index.js +12 -32
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/index.js +12 -37
- package/v2Containers/Sms/Create/index.js +39 -3
- package/v2Containers/Sms/Create/messages.js +4 -0
- package/v2Containers/Sms/Edit/index.js +35 -3
- package/v2Containers/Sms/commonMethods.js +3 -6
- package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
- package/v2Containers/SmsTrai/Edit/index.js +11 -47
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/TemplatesV2/index.js +28 -13
- package/v2Containers/Viber/index.js +0 -1
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
- package/v2Containers/WebPush/Create/index.js +2 -2
- package/v2Containers/WebPush/Create/utils/validation.js +17 -8
- package/v2Containers/WebPush/Create/utils/validation.test.js +44 -24
- package/v2Containers/Whatsapp/index.js +9 -17
- package/v2Containers/Zalo/index.js +3 -11
|
@@ -33,7 +33,6 @@ import creativesMessages from '../CreativesContainer/messages';
|
|
|
33
33
|
import withCreatives from "../../hoc/withCreatives";
|
|
34
34
|
import UnifiedPreview from "../../v2Components/CommonTestAndPreview/UnifiedPreview";
|
|
35
35
|
import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
|
|
36
|
-
import { validateTags } from "../../utils/tagValidations";
|
|
37
36
|
import injectReducer from '../../utils/injectReducer';
|
|
38
37
|
import v2InAppReducer from './reducer';
|
|
39
38
|
import { v2InAppSagas } from './sagas';
|
|
@@ -53,7 +52,7 @@ import {
|
|
|
53
52
|
LAYOUT_RADIO_OPTIONS,
|
|
54
53
|
IOS_CAPITAL,
|
|
55
54
|
} from "./constants";
|
|
56
|
-
import { INAPP, SMS } from "../CreativesContainer/constants";
|
|
55
|
+
import { GENERIC, INAPP, SMS } from "../CreativesContainer/constants";
|
|
57
56
|
import { AI_CONTENT_BOT_DISABLED } from "../../constants/unified";
|
|
58
57
|
import {
|
|
59
58
|
ALL, TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
|
|
@@ -61,7 +60,8 @@ import {
|
|
|
61
60
|
import { getCdnUrl } from "../../utils/cdnTransformation";
|
|
62
61
|
import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
|
|
63
62
|
import { validateInAppContent } from "../../utils/commonUtils";
|
|
64
|
-
import {
|
|
63
|
+
import { validateTags } from "../../utils/tagValidations";
|
|
64
|
+
import { hasNewEditorFlowInAppEnabled } from "../../utils/common";
|
|
65
65
|
import formBuilderMessages from "../../v2Components/FormBuilder/messages";
|
|
66
66
|
import HTMLEditor from "../../v2Components/HtmlEditor";
|
|
67
67
|
import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
|
|
@@ -1084,39 +1084,55 @@ export const InApp = (props) => {
|
|
|
1084
1084
|
};
|
|
1085
1085
|
|
|
1086
1086
|
// Validation middleware for tag validation (both liquid and non-liquid flow)
|
|
1087
|
+
// Liquid validation (extractTags) runs only in library mode
|
|
1087
1088
|
const validationMiddleWare = async () => {
|
|
1088
|
-
//
|
|
1089
|
+
// Normalize validator bucket keys to component state keys (ANDROID, IOS_CAPITAL, GENERIC)
|
|
1090
|
+
// so we don't merge e.g. 'android'/'ios'/'generic' with ANDROID/IOS/GENERIC and get duplicate/stale keys
|
|
1091
|
+
const normalizeErrorBuckets = (errors) => {
|
|
1092
|
+
const normalized = {
|
|
1093
|
+
[ANDROID]: [],
|
|
1094
|
+
[IOS_CAPITAL]: [],
|
|
1095
|
+
[GENERIC]: [],
|
|
1096
|
+
};
|
|
1097
|
+
if (!errors || typeof errors !== 'object') return normalized;
|
|
1098
|
+
const keyMap = {
|
|
1099
|
+
ANDROID,
|
|
1100
|
+
android: ANDROID,
|
|
1101
|
+
[IOS_CAPITAL]: IOS_CAPITAL,
|
|
1102
|
+
[IOS]: IOS_CAPITAL,
|
|
1103
|
+
ios: IOS_CAPITAL,
|
|
1104
|
+
GENERIC,
|
|
1105
|
+
generic: GENERIC,
|
|
1106
|
+
};
|
|
1107
|
+
for (const [key, value] of Object.entries(errors)) {
|
|
1108
|
+
const targetKey = keyMap[key];
|
|
1109
|
+
if (targetKey != null && Array.isArray(value)) {
|
|
1110
|
+
normalized[targetKey] = value;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return normalized;
|
|
1114
|
+
};
|
|
1089
1115
|
const onError = ({ standardErrors, liquidErrors }) => {
|
|
1090
1116
|
setErrorMessage((prev) => ({
|
|
1091
|
-
STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...standardErrors },
|
|
1092
|
-
LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...liquidErrors },
|
|
1117
|
+
STANDARD_ERROR_MSG: { ...prev.STANDARD_ERROR_MSG, ...normalizeErrorBuckets(standardErrors) },
|
|
1118
|
+
LIQUID_ERROR_MSG: { ...prev.LIQUID_ERROR_MSG, ...normalizeErrorBuckets(liquidErrors) },
|
|
1093
1119
|
}));
|
|
1094
1120
|
};
|
|
1095
1121
|
const onSuccess = () => {
|
|
1096
|
-
// Proceed with submission when validation is successful
|
|
1097
1122
|
onDoneCallback();
|
|
1098
1123
|
};
|
|
1099
1124
|
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
// For liquid flow, use validateInAppContent
|
|
1104
|
-
if (isLiquidFlow && hasTags) {
|
|
1105
|
-
// Validate the INAPP content
|
|
1125
|
+
// Library mode: run extractTags validation (always, so we catch liquid errors even when tags not loaded)
|
|
1126
|
+
if (!isFullMode) {
|
|
1106
1127
|
const payload = createPayload();
|
|
1107
1128
|
validateInAppContent(payload, {
|
|
1108
|
-
currentTab: panes === ANDROID ? 1 : 2,
|
|
1129
|
+
currentTab: panes === ANDROID ? 1 : 2,
|
|
1109
1130
|
onError,
|
|
1110
1131
|
onSuccess,
|
|
1111
1132
|
getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
|
|
1112
1133
|
formatMessage,
|
|
1113
1134
|
messages: formBuilderMessages,
|
|
1114
|
-
tagLookupMap: metaEntities?.tagLookupMap || {},
|
|
1115
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1116
|
-
isLiquidFlow,
|
|
1117
|
-
forwardedTags: {},
|
|
1118
1135
|
skipTags: (tag) => {
|
|
1119
|
-
// Skip certain tags if needed
|
|
1120
1136
|
const skipRegexes = [
|
|
1121
1137
|
/dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
|
|
1122
1138
|
/unsubscribe\(#[a-zA-Z\d]{6}\)/,
|
|
@@ -1124,103 +1140,15 @@ export const InApp = (props) => {
|
|
|
1124
1140
|
/SURVEY.*\.TOKEN/,
|
|
1125
1141
|
/^[A-Za-z].*\([a-zA-Z\d]*\)/,
|
|
1126
1142
|
];
|
|
1127
|
-
|
|
1128
1143
|
return skipRegexes.some((regex) => regex.test(tag));
|
|
1129
1144
|
},
|
|
1130
1145
|
singleTab: getSingleTab(accountData),
|
|
1131
1146
|
});
|
|
1132
|
-
} else if (hasTags) {
|
|
1133
|
-
// For non-liquid flow, validate tags using validateTags (only if tags are available)
|
|
1134
|
-
const androidContent = htmlContentAndroid || '';
|
|
1135
|
-
const iosContent = htmlContentIos || '';
|
|
1136
|
-
|
|
1137
|
-
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
1138
|
-
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
1139
|
-
|
|
1140
|
-
let hasErrors = false;
|
|
1141
|
-
const newErrors = {
|
|
1142
|
-
STANDARD_ERROR_MSG: {
|
|
1143
|
-
ANDROID: [],
|
|
1144
|
-
IOS: [],
|
|
1145
|
-
GENERIC: [],
|
|
1146
|
-
},
|
|
1147
|
-
LIQUID_ERROR_MSG: {
|
|
1148
|
-
ANDROID: [],
|
|
1149
|
-
IOS: [],
|
|
1150
|
-
GENERIC: [],
|
|
1151
|
-
},
|
|
1152
|
-
};
|
|
1153
|
-
|
|
1154
|
-
// Validate Android content
|
|
1155
|
-
if (androidSupported && androidContent && androidContent?.trim() !== '') {
|
|
1156
|
-
const validationResponse = validateTags({
|
|
1157
|
-
content: androidContent,
|
|
1158
|
-
tagsParam: tags,
|
|
1159
|
-
injectedTagsParams: injectedTags || {},
|
|
1160
|
-
location,
|
|
1161
|
-
tagModule: getDefaultTags,
|
|
1162
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1163
|
-
isFullMode,
|
|
1164
|
-
}) || {};
|
|
1165
|
-
|
|
1166
|
-
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
1167
|
-
hasErrors = true;
|
|
1168
|
-
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
1169
|
-
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1170
|
-
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
1171
|
-
})
|
|
1172
|
-
);
|
|
1173
|
-
}
|
|
1174
|
-
if (validationResponse?.isBraceError) {
|
|
1175
|
-
hasErrors = true;
|
|
1176
|
-
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
1177
|
-
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// Validate iOS content
|
|
1183
|
-
if (iosSupported && iosContent && iosContent?.trim() !== '') {
|
|
1184
|
-
const validationResponse = validateTags({
|
|
1185
|
-
content: iosContent,
|
|
1186
|
-
tagsParam: tags,
|
|
1187
|
-
injectedTagsParams: injectedTags || {},
|
|
1188
|
-
location,
|
|
1189
|
-
tagModule: getDefaultTags,
|
|
1190
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1191
|
-
isFullMode,
|
|
1192
|
-
}) || {};
|
|
1193
|
-
|
|
1194
|
-
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
1195
|
-
hasErrors = true;
|
|
1196
|
-
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
1197
|
-
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1198
|
-
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
1199
|
-
})
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
if (validationResponse?.isBraceError) {
|
|
1203
|
-
hasErrors = true;
|
|
1204
|
-
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
1205
|
-
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
if (hasErrors) {
|
|
1211
|
-
setErrorMessage(newErrors);
|
|
1212
|
-
} else {
|
|
1213
|
-
// No errors, proceed with submission
|
|
1214
|
-
onSuccess();
|
|
1215
|
-
}
|
|
1216
1147
|
} else {
|
|
1217
|
-
// No tags available, skip validation and proceed directly
|
|
1218
1148
|
onSuccess();
|
|
1219
1149
|
}
|
|
1220
1150
|
};
|
|
1221
1151
|
|
|
1222
|
-
const isLiquidFlow = hasLiquidSupportFeature();
|
|
1223
|
-
|
|
1224
1152
|
// Check template data to determine editor type (for render decision)
|
|
1225
1153
|
const templateDetails = isFullMode ? editData?.templateDetails : templateData;
|
|
1226
1154
|
const versions = templateDetails?.versions || {};
|
|
@@ -21,18 +21,16 @@ import { getCtaObject } from '../utils';
|
|
|
21
21
|
jest.mock('redux-auth-wrapper/history4/redirect', () => ({
|
|
22
22
|
connectedRouterRedirect: jest.fn((config) => (Component) => Component),
|
|
23
23
|
}));
|
|
24
|
+
import * as commonUtils from '../../../utils/commonUtils';
|
|
24
25
|
|
|
25
26
|
const mockActions = {
|
|
26
27
|
getTemplateInfoById: jest.fn(),
|
|
27
28
|
resetEditTemplate: jest.fn(),
|
|
28
|
-
getTemplateDetails: jest.fn((id,
|
|
29
|
+
getTemplateDetails: jest.fn((id, setSpin) => {
|
|
29
30
|
// Simulate successful template details fetch to prevent loading state
|
|
30
31
|
// The callback is setSpin function, call it with false to stop spinner
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
setTimeout(() => {
|
|
34
|
-
callback(false);
|
|
35
|
-
}, 0);
|
|
32
|
+
if (setSpin && typeof setSpin === 'function') {
|
|
33
|
+
setTimeout(() => setSpin(false), 0);
|
|
36
34
|
}
|
|
37
35
|
}),
|
|
38
36
|
editTemplate: jest.fn(),
|
|
@@ -41,6 +39,9 @@ const mockActions = {
|
|
|
41
39
|
};
|
|
42
40
|
const mockGlobalActions = {
|
|
43
41
|
fetchSchemaForEntity: jest.fn(),
|
|
42
|
+
getLiquidTags: jest.fn((content, callback) =>
|
|
43
|
+
callback({ askAiraResponse: { data: [], errors: [] }, isError: false }),
|
|
44
|
+
),
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
jest.mock('../../../v2Containers/TagList/index.js', () => ({
|
|
@@ -66,7 +67,17 @@ const renderComponent = (props) =>
|
|
|
66
67
|
);
|
|
67
68
|
|
|
68
69
|
describe('Test activity inApp container', () => {
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
jest.restoreAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
69
74
|
it('test case for inApp template update flow', async () => {
|
|
75
|
+
jest
|
|
76
|
+
.spyOn(commonUtils, 'validateInAppContent')
|
|
77
|
+
.mockImplementation((payload, options) => {
|
|
78
|
+
options.onSuccess();
|
|
79
|
+
return Promise.resolve(true);
|
|
80
|
+
});
|
|
70
81
|
renderComponent({
|
|
71
82
|
actions: mockActions,
|
|
72
83
|
globalActions: mockGlobalActions,
|
|
@@ -50,9 +50,7 @@ import injectReducer from '../../utils/injectReducer';
|
|
|
50
50
|
import v2InAppReducer from '../InApp/reducer';
|
|
51
51
|
import { v2InAppSagas } from '../InApp/sagas';
|
|
52
52
|
import injectSaga from '../../utils/injectSaga';
|
|
53
|
-
import { validateTags } from "../../utils/tagValidations";
|
|
54
53
|
import { validateInAppContent } from "../../utils/commonUtils";
|
|
55
|
-
import { hasLiquidSupportFeature } from "../../utils/common";
|
|
56
54
|
import formBuilderMessages from "../../v2Components/FormBuilder/messages";
|
|
57
55
|
import { getSingleTab, hasAnyErrors } from "../InApp/utils";
|
|
58
56
|
import ErrorInfoNote from "../../v2Components/ErrorInfoNote";
|
|
@@ -818,30 +816,18 @@ export const InappAdvanced = (props) => {
|
|
|
818
816
|
const payload = createPayload(latestHtmlValues);
|
|
819
817
|
|
|
820
818
|
// Validate the INAPP content
|
|
821
|
-
const isLiquidFlow = hasLiquidSupportFeature();
|
|
822
819
|
// Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
|
|
823
820
|
const hasTags = tags && tags.length > 0;
|
|
824
821
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
onSuccess();
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
if (isLiquidFlow && hasTags) {
|
|
832
|
-
validateInAppContent(payload, {
|
|
822
|
+
if (!isFullMode || hasTags) {
|
|
823
|
+
await validateInAppContent(payload, {
|
|
833
824
|
currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
|
|
834
825
|
onError,
|
|
835
826
|
onSuccess,
|
|
836
827
|
getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
|
|
837
828
|
formatMessage,
|
|
838
829
|
messages: formBuilderMessages,
|
|
839
|
-
tagLookupMap: metaEntities?.tagLookupMap || {},
|
|
840
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
841
|
-
isLiquidFlow,
|
|
842
|
-
forwardedTags: {},
|
|
843
830
|
skipTags: (tag) => {
|
|
844
|
-
// Skip certain tags if needed
|
|
845
831
|
const skipRegexes = [
|
|
846
832
|
/dynamic_expiry_date_after_\d+_days\.FORMAT_\d/,
|
|
847
833
|
/unsubscribe\(#[a-zA-Z\d]{6}\)/,
|
|
@@ -849,98 +835,12 @@ export const InappAdvanced = (props) => {
|
|
|
849
835
|
/SURVEY.*\.TOKEN/,
|
|
850
836
|
/^[A-Za-z].*\([a-zA-Z\d]*\)/,
|
|
851
837
|
];
|
|
852
|
-
|
|
853
838
|
return skipRegexes.some((regex) => regex.test(tag));
|
|
854
839
|
},
|
|
855
840
|
singleTab: getSingleTab(accountData),
|
|
856
841
|
});
|
|
857
|
-
} else if (hasTags) {
|
|
858
|
-
// For non-liquid flow, validate tags using validateTags (only if tags are available)
|
|
859
|
-
const androidContent = latestHtmlValues?.android || (androidBeeHtml?.value || (typeof androidBeeHtml === 'string' ? androidBeeHtml : ''));
|
|
860
|
-
const iosContent = latestHtmlValues?.ios || (iosBeeHtml?.value || (typeof iosBeeHtml === 'string' ? iosBeeHtml : ''));
|
|
861
|
-
|
|
862
|
-
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
863
|
-
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
864
|
-
|
|
865
|
-
let hasErrors = false;
|
|
866
|
-
const newErrors = {
|
|
867
|
-
STANDARD_ERROR_MSG: {
|
|
868
|
-
ANDROID: [],
|
|
869
|
-
IOS: [],
|
|
870
|
-
GENERIC: [],
|
|
871
|
-
},
|
|
872
|
-
LIQUID_ERROR_MSG: {
|
|
873
|
-
ANDROID: [],
|
|
874
|
-
IOS: [],
|
|
875
|
-
GENERIC: [],
|
|
876
|
-
},
|
|
877
|
-
};
|
|
878
|
-
|
|
879
|
-
// Validate Android content
|
|
880
|
-
if (androidSupported && androidContent && androidContent?.trim() !== '') {
|
|
881
|
-
const validationResponse = validateTags({
|
|
882
|
-
content: androidContent,
|
|
883
|
-
tagsParam: tags,
|
|
884
|
-
injectedTagsParams: injectedTags || {},
|
|
885
|
-
location,
|
|
886
|
-
tagModule: getDefaultTags,
|
|
887
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
888
|
-
isFullMode,
|
|
889
|
-
}) || {};
|
|
890
|
-
|
|
891
|
-
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
892
|
-
hasErrors = true;
|
|
893
|
-
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
894
|
-
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
895
|
-
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
896
|
-
})
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
if (validationResponse?.isBraceError) {
|
|
900
|
-
hasErrors = true;
|
|
901
|
-
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
902
|
-
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Validate iOS content
|
|
908
|
-
if (iosSupported && iosContent && iosContent?.trim() !== '') {
|
|
909
|
-
const validationResponse = validateTags({
|
|
910
|
-
content: iosContent,
|
|
911
|
-
tagsParam: tags,
|
|
912
|
-
injectedTagsParams: injectedTags || {},
|
|
913
|
-
location,
|
|
914
|
-
tagModule: getDefaultTags,
|
|
915
|
-
eventContextTags: metaEntities?.eventContextTags || [],
|
|
916
|
-
isFullMode,
|
|
917
|
-
}) || {};
|
|
918
|
-
|
|
919
|
-
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
920
|
-
hasErrors = true;
|
|
921
|
-
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
922
|
-
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
923
|
-
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
924
|
-
})
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
if (validationResponse?.isBraceError) {
|
|
928
|
-
hasErrors = true;
|
|
929
|
-
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
930
|
-
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
if (hasErrors) {
|
|
936
|
-
setErrorMessage(newErrors);
|
|
937
|
-
} else {
|
|
938
|
-
// No errors, proceed with submission
|
|
939
|
-
onSuccess();
|
|
940
|
-
}
|
|
941
842
|
} else {
|
|
942
|
-
|
|
943
|
-
onSuccess();
|
|
843
|
+
await onSuccess();
|
|
944
844
|
}
|
|
945
845
|
};
|
|
946
846
|
|
|
@@ -1045,15 +945,7 @@ export const InappAdvanced = (props) => {
|
|
|
1045
945
|
)}
|
|
1046
946
|
<CapButton
|
|
1047
947
|
onClick={async () => {
|
|
1048
|
-
|
|
1049
|
-
const hasTags = tags && tags?.length > 0;
|
|
1050
|
-
if (isLiquidFlow || hasTags) {
|
|
1051
|
-
// Use validation middleware for tag validation
|
|
1052
|
-
await liquidMiddleWare();
|
|
1053
|
-
} else {
|
|
1054
|
-
// No validation needed, proceed directly
|
|
1055
|
-
await onDoneCallback();
|
|
1056
|
-
}
|
|
948
|
+
await liquidMiddleWare();
|
|
1057
949
|
}}
|
|
1058
950
|
disabled={isDisableDone()}
|
|
1059
951
|
className="inapp-create-btn"
|
|
@@ -61,6 +61,7 @@ describe('InappAdvanced Component', () => {
|
|
|
61
61
|
|
|
62
62
|
const mockGlobalActions = {
|
|
63
63
|
fetchSchemaForEntity: jest.fn(),
|
|
64
|
+
getLiquidTags: jest.fn((content, callback) => callback({ askAiraResponse: { data: [] }, isError: false })),
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
defaultProps = {
|
|
@@ -409,6 +410,7 @@ describe('InappAdvanced Component', () => {
|
|
|
409
410
|
actions: mockActions,
|
|
410
411
|
globalActions: {
|
|
411
412
|
fetchSchemaForEntity: jest.fn(),
|
|
413
|
+
getLiquidTags: jest.fn((content, callback) => callback({ askAiraResponse: { data: [] }, isError: false })),
|
|
412
414
|
},
|
|
413
415
|
location: {
|
|
414
416
|
pathname: '/inapp/create',
|
|
@@ -38,7 +38,8 @@ import v2MobilePushCreateReducer from './reducer';
|
|
|
38
38
|
import { v2MobilePushWatchDuplicateTemplateSaga } from './sagas';
|
|
39
39
|
import { EXTERNAL_LINK_LOWERCASE } from './constants';
|
|
40
40
|
import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
|
|
41
|
-
import { checkForPersonalizationTokens } from '../../../utils/commonUtils';
|
|
41
|
+
import { checkForPersonalizationTokens, validateMobilePushContent } from '../../../utils/commonUtils';
|
|
42
|
+
import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
|
|
42
43
|
|
|
43
44
|
const PrefixWrapper = styled.div`
|
|
44
45
|
margin-right: 16px;
|
|
@@ -94,8 +95,56 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
94
95
|
this.props.globalActions.fetchSchemaForEntity(query);
|
|
95
96
|
};
|
|
96
97
|
componentWillReceiveProps = (nextProps) => {
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
// Library mode: on Done click (transition to isGetFormData), run extractTags only when no existing validation errors (braces, personalization, etc.)
|
|
99
|
+
if (nextProps.isGetFormData && !this.props.isGetFormData && !nextProps.isFullMode) {
|
|
100
|
+
// If form already has validation errors (braces, personalization tags, etc.), do not call extractTags or return payload; always return early
|
|
101
|
+
if (!this.state.isFormValid) {
|
|
102
|
+
if (typeof nextProps.onValidationFail === 'function') {
|
|
103
|
+
nextProps.onValidationFail();
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const hasAllValidationCallbacks =
|
|
108
|
+
typeof nextProps.getLiquidTags === 'function' &&
|
|
109
|
+
typeof nextProps.showLiquidErrorInFooter === 'function' &&
|
|
110
|
+
typeof nextProps.onValidationFail === 'function';
|
|
111
|
+
|
|
112
|
+
if (hasAllValidationCallbacks) {
|
|
113
|
+
const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
|
|
114
|
+
validateMobilePushContent(formDataArr, {
|
|
115
|
+
currentTab: this.state.currentTab,
|
|
116
|
+
getLiquidTags: nextProps.getLiquidTags,
|
|
117
|
+
formatMessage: this.props.intl.formatMessage,
|
|
118
|
+
messages: formBuilderMessages,
|
|
119
|
+
onError: (err) => {
|
|
120
|
+
const { standardErrors = [], liquidErrors = [] } = err;
|
|
121
|
+
// _validatePlatformSpecificContent passes { standardErrors: { ANDROID, IOS, generic }, liquidErrors: { ... } }; footer expects arrays
|
|
122
|
+
const toArray = (v) => (Array.isArray(v) ? v : (v && typeof v === 'object' ? [].concat(...Object.values(v)) : []));
|
|
123
|
+
const STANDARD_ERROR_MSG = toArray(standardErrors);
|
|
124
|
+
const LIQUID_ERROR_MSG = toArray(liquidErrors);
|
|
125
|
+
if (typeof nextProps.showLiquidErrorInFooter === 'function') {
|
|
126
|
+
nextProps.showLiquidErrorInFooter(
|
|
127
|
+
{ STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
|
|
128
|
+
this.state.currentTab
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
// Only trigger onValidationFail when there are actual errors; skip when helper called onError with empty arrays (reset case)
|
|
132
|
+
if ((STANDARD_ERROR_MSG.length > 0 || LIQUID_ERROR_MSG.length > 0) && typeof nextProps.onValidationFail === 'function') {
|
|
133
|
+
nextProps.onValidationFail();
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
onSuccess: () => {
|
|
137
|
+
if (this.state.isFormValid && typeof nextProps.getFormLibraryData === 'function') {
|
|
138
|
+
nextProps.getFormLibraryData(this.getFormData());
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
// Fail closed: require full validation callback set; treat any missing callback as validation failure
|
|
144
|
+
if (typeof nextProps.onValidationFail === 'function') {
|
|
145
|
+
nextProps.onValidationFail();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
99
148
|
} else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
|
|
100
149
|
this.startValidation();
|
|
101
150
|
}
|
|
@@ -678,31 +727,21 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
678
727
|
}
|
|
679
728
|
showError = () => {
|
|
680
729
|
const {intl} = this.props;
|
|
681
|
-
const {errorData} = this.state;
|
|
730
|
+
const {errorData, schema} = this.state;
|
|
682
731
|
const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
|
|
683
732
|
if (!isEmpty(this.state.formData) && !this.state.isFormValid) {
|
|
684
733
|
let tab = this.state.currentTab;
|
|
685
|
-
const isAndroidInvalid = Object.values(errorData[0]).includes(true);
|
|
686
|
-
const isIosInvalid = Object.values(errorData[1]).includes(true);
|
|
687
|
-
|
|
734
|
+
const isAndroidInvalid = Object.values(errorData[0] || {}).includes(true);
|
|
735
|
+
const isIosInvalid = Object.values(errorData[1] || {}).includes(true);
|
|
736
|
+
const isIosTabVisible = get(schema, 'containers[0].panes[1].isSupported', true) !== false;
|
|
688
737
|
if (isAndroidInvalid) {
|
|
689
738
|
tab = 1;
|
|
690
739
|
errorMessage.description = intl.formatMessage(messages.invalidAndroidMessage);
|
|
691
|
-
|
|
692
|
-
if (!isEmpty(invalidTags)) {
|
|
693
|
-
isTagErrorExist = true;
|
|
694
|
-
errorMessage.description = `${intl.formatMessage(messages.invalidAndroidMessage)} ${intl.formatMessage(messages.invalidTags)}: ${invalidTags.join(',')} `;
|
|
695
|
-
}
|
|
696
|
-
} else if (isIosInvalid) {
|
|
740
|
+
} else if (isIosInvalid && isIosTabVisible) {
|
|
697
741
|
tab = 2;
|
|
698
742
|
errorMessage.description = intl.formatMessage(messages.invalidIosMessage);
|
|
699
|
-
const invalidTags = errorData[1]['invalid-tags'];
|
|
700
|
-
if (!isEmpty(invalidTags)) {
|
|
701
|
-
isTagErrorExist = true;
|
|
702
|
-
errorMessage.description = `${intl.formatMessage(messages.invalidIosMessage)} ${intl.formatMessage(messages.invalidTags)}: ${invalidTags.join(',')} `;
|
|
703
|
-
}
|
|
704
743
|
}
|
|
705
|
-
if (tab !== this.state.currentTab
|
|
744
|
+
if (tab !== this.state.currentTab) {
|
|
706
745
|
CapNotification.error(errorMessage);
|
|
707
746
|
}
|
|
708
747
|
}
|
|
@@ -2029,6 +2068,7 @@ Create.propTypes = {
|
|
|
2029
2068
|
onPreviewContentClicked: PropTypes.func,
|
|
2030
2069
|
onTestContentClicked: PropTypes.func,
|
|
2031
2070
|
eventContextTags: PropTypes.array,
|
|
2071
|
+
getLiquidTags: PropTypes.func,
|
|
2032
2072
|
showLiquidErrorInFooter: PropTypes.func,
|
|
2033
2073
|
showTestAndPreviewSlidebox: PropTypes.bool,
|
|
2034
2074
|
handleTestAndPreview: PropTypes.func,
|
|
@@ -39,7 +39,8 @@ import { v2MobilePushEditSagas } from './sagas';
|
|
|
39
39
|
import v2MobilePushEditReducer from './reducer';
|
|
40
40
|
import * as globalActions from '../../Cap/actions';
|
|
41
41
|
import { MAPP_SDK } from './constants';
|
|
42
|
-
import { checkForPersonalizationTokens, isEmbeddedEditOrPreview } from '../../../utils/commonUtils';
|
|
42
|
+
import { checkForPersonalizationTokens, isEmbeddedEditOrPreview, validateMobilePushContent } from '../../../utils/commonUtils';
|
|
43
|
+
import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
|
|
43
44
|
import { EMBEDDED } from '../../Whatsapp/constants';
|
|
44
45
|
import { OUTBOUND } from '../../../v2Components/FormBuilder/constants';
|
|
45
46
|
import TestAndPreviewSlidebox from '../../../v2Components/TestAndPreviewSlidebox';
|
|
@@ -68,6 +69,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
68
69
|
schema: {},
|
|
69
70
|
currentTab: 1,
|
|
70
71
|
editData: {},
|
|
72
|
+
errorData: [],
|
|
71
73
|
loading: false,
|
|
72
74
|
isFormValid: true,
|
|
73
75
|
injectedTags: {},
|
|
@@ -129,8 +131,43 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
129
131
|
this.hasFetchedInitialTagsRef = false;
|
|
130
132
|
this.lastFetchedTagContextRef = null;
|
|
131
133
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
// Library mode: on Done click (transition to isGetFormData), run extractTags only when no existing validation errors (braces, personalization, etc.)
|
|
135
|
+
if (nextProps.isGetFormData && !this.props.isGetFormData && !nextProps.isFullMode) {
|
|
136
|
+
// If form already has validation errors (braces, personalization tags, etc.), do not call extractTags; just fail
|
|
137
|
+
if (!this.state.isFormValid) {
|
|
138
|
+
nextProps.onValidationFail?.();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (nextProps.getLiquidTags && nextProps.showLiquidErrorInFooter) {
|
|
142
|
+
const formDataArr = [this.state.formData?.[0], this.state.formData?.[1]];
|
|
143
|
+
validateMobilePushContent(formDataArr, {
|
|
144
|
+
currentTab: this.state.currentTab,
|
|
145
|
+
getLiquidTags: nextProps.getLiquidTags,
|
|
146
|
+
formatMessage: this.props.intl.formatMessage,
|
|
147
|
+
messages: formBuilderMessages,
|
|
148
|
+
onError: (err) => {
|
|
149
|
+
const { standardErrors = [], liquidErrors = [] } = err;
|
|
150
|
+
// _validatePlatformSpecificContent passes { standardErrors: { ANDROID, IOS, generic }, liquidErrors: { ... } }; footer expects arrays
|
|
151
|
+
const toArray = (v) => (Array.isArray(v) ? v : (v && typeof v === 'object' ? [].concat(...Object.values(v)) : []));
|
|
152
|
+
const STANDARD_ERROR_MSG = toArray(standardErrors);
|
|
153
|
+
const LIQUID_ERROR_MSG = toArray(liquidErrors);
|
|
154
|
+
nextProps.showLiquidErrorInFooter(
|
|
155
|
+
{ STANDARD_ERROR_MSG, LIQUID_ERROR_MSG },
|
|
156
|
+
this.state.currentTab
|
|
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
|
+
}
|
|
163
|
+
},
|
|
164
|
+
onSuccess: () => {
|
|
165
|
+
nextProps.getFormLibraryData(this.getFormData());
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
nextProps.getFormLibraryData(this.getFormData());
|
|
170
|
+
}
|
|
134
171
|
} else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
|
|
135
172
|
this.startValidation();
|
|
136
173
|
}
|
|
@@ -705,32 +742,22 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
705
742
|
// eslint-disable-next-line react/sort-comp
|
|
706
743
|
showError = () => {
|
|
707
744
|
const {intl} = this.props;
|
|
708
|
-
const {errorData} = this.state;
|
|
745
|
+
const {errorData, schema} = this.state;
|
|
709
746
|
const errorMessage = {key: 'validation-error', message: intl.formatMessage(messages.validationError)};
|
|
710
|
-
let isTagErrorExist = false;
|
|
711
747
|
if (!_.isEmpty(this.state.formData) && !this.state.isFormValid) {
|
|
712
748
|
let tab = this.state.currentTab;
|
|
713
|
-
const isAndroidInvalid = Object.values(errorData[0]).includes(true);
|
|
714
|
-
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);
|
|
751
|
+
const isIosTabVisible = get(schema, 'containers[0].panes[1].isSupported', true) !== false;
|
|
715
752
|
if (isAndroidInvalid) {
|
|
716
753
|
tab = 1;
|
|
717
754
|
errorMessage.description = intl.formatMessage(messages.invalidAndroidMessage);
|
|
718
|
-
|
|
719
|
-
if (!_.isEmpty(invalidTags)) {
|
|
720
|
-
isTagErrorExist = true;
|
|
721
|
-
errorMessage.description = `${intl.formatMessage(messages.invalidAndroidMessage)} ${intl.formatMessage(messages.invalidTags)}: ${invalidTags.join(',')} `;
|
|
722
|
-
}
|
|
723
|
-
} else if (isIosInvalid) {
|
|
755
|
+
} else if (isIosInvalid && isIosTabVisible) {
|
|
724
756
|
tab = 2;
|
|
725
757
|
errorMessage.description = intl.formatMessage(messages.invalidIosMessage);
|
|
726
|
-
const invalidTags = errorData[1]['invalid-tags'];
|
|
727
|
-
if (!_.isEmpty(invalidTags)) {
|
|
728
|
-
isTagErrorExist = true;
|
|
729
|
-
errorMessage.description = `${intl.formatMessage(messages.invalidIosMessage)} ${intl.formatMessage(messages.invalidTags)}: ${invalidTags.join(',')} `;
|
|
730
|
-
}
|
|
731
758
|
}
|
|
732
759
|
|
|
733
|
-
if (tab !== this.state.currentTab
|
|
760
|
+
if (tab !== this.state.currentTab) {
|
|
734
761
|
CapNotification.error(errorMessage);
|
|
735
762
|
}
|
|
736
763
|
}
|
|
@@ -2307,11 +2334,12 @@ Edit.propTypes = {
|
|
|
2307
2334
|
getFormLibraryData: PropTypes.func,
|
|
2308
2335
|
isGetFormData: PropTypes.bool,
|
|
2309
2336
|
Create: PropTypes.object,
|
|
2310
|
-
onValidationFail: PropTypes.
|
|
2337
|
+
onValidationFail: PropTypes.func,
|
|
2311
2338
|
onPreviewContentClicked: PropTypes.func,
|
|
2312
2339
|
onTestContentClicked: PropTypes.func,
|
|
2313
2340
|
creativesMode: PropTypes.string,
|
|
2314
2341
|
eventContextTags: PropTypes.array,
|
|
2342
|
+
getLiquidTags: PropTypes.func,
|
|
2315
2343
|
showLiquidErrorInFooter: PropTypes.func,
|
|
2316
2344
|
showTestAndPreviewSlidebox: PropTypes.bool,
|
|
2317
2345
|
handleTestAndPreview: PropTypes.func,
|