@capillarytech/creatives-library 8.0.290-alpha.3 → 8.0.290-alpha.4
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 +3 -0
- package/package.json +1 -1
- package/utils/tagValidations.js +4 -6
- package/utils/tests/tagValidations.test.js +161 -0
- package/v2Components/FormBuilder/index.js +49 -40
- package/v2Containers/CreativesContainer/SlideBoxContent.js +5 -1
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +13 -7
- package/v2Containers/CreativesContainer/index.js +11 -1
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +15 -10
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +19 -24
- package/v2Containers/InApp/index.js +4 -9
- package/v2Containers/InappAdvance/index.js +3 -6
- package/v2Containers/InappAdvance/tests/index.test.js +2 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +24 -3
- package/v2Containers/MobilePush/Create/index.js +36 -3
- package/v2Containers/MobilePush/Edit/index.js +36 -3
- package/v2Containers/MobilePushNew/index.js +15 -4
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +18 -4
- package/v2Containers/Sms/Create/index.js +22 -18
- package/v2Containers/Sms/Edit/index.js +18 -16
- package/v2Containers/Sms/commonMethods.js +0 -3
- package/v2Containers/Sms/tests/commonMethods.test.js +122 -0
- package/v2Containers/SmsTrai/Edit/index.js +5 -0
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/WebPush/Create/utils/validation.test.js +59 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +624 -248
package/constants/unified.js
CHANGED
|
@@ -149,6 +149,9 @@ export const BADGES_ENROLL = 'BADGES_ENROLL';
|
|
|
149
149
|
export const BADGES_ISSUE = 'BADGES_ISSUE';
|
|
150
150
|
export const CUSTOMER_BARCODE_TAG = 'customer_barcode';
|
|
151
151
|
export const COPY_OF = 'Copy of';
|
|
152
|
+
export const UNSUBSCRIBE_TAG = 'unsubscribe';
|
|
153
|
+
/** Whitespace-tolerant check for {{ unsubscribe }}-style tag in content. */
|
|
154
|
+
export const UNSUBSCRIBE_TAG_REGEX = new RegExp(`\\{\\{\\s*${UNSUBSCRIBE_TAG}\\s*\\}\\}`);
|
|
152
155
|
export const ENTRY_TRIGGER_TAG_REGEX = /\bentryTrigger\.\w+(?:\.\w+)?(?:\(\w+\))?/g;
|
|
153
156
|
export const SKIP_TAGS_REGEX_GROUPS = ["dynamic_expiry_date_after_\\d+_days.FORMAT_\\d", "unsubscribe\\(#[a-zA-Z\\d]{6}\\)", "Link_to_[a-zA-Z]", "SURVEY.*.TOKEN", "^[A-Za-z].*\\([a-zA-Z\\d]*\\)", "referral_unique_(code|url).*userid"];
|
|
154
157
|
|
package/package.json
CHANGED
package/utils/tagValidations.js
CHANGED
|
@@ -8,13 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import lodashForEach from 'lodash/forEach';
|
|
10
10
|
import lodashCloneDeep from 'lodash/cloneDeep';
|
|
11
|
-
import { ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS } from '../constants/unified';
|
|
11
|
+
import { ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS, UNSUBSCRIBE_TAG, UNSUBSCRIBE_TAG_REGEX } from '../constants/unified';
|
|
12
12
|
|
|
13
13
|
const DEFAULT = 'default';
|
|
14
14
|
const SUBTAGS = 'subtags';
|
|
15
15
|
|
|
16
|
-
/** Whitespace-tolerant check for {{ unsubscribe }}-style tag in content. */
|
|
17
|
-
const UNSUBSCRIBE_TAG_REGEX = /\{\{\s*unsubscribe\s*\}\}/;
|
|
18
16
|
export const hasUnsubscribeTag = (content) =>
|
|
19
17
|
typeof content === 'string' && UNSUBSCRIBE_TAG_REGEX.test(content);
|
|
20
18
|
|
|
@@ -57,7 +55,7 @@ export const validateTagsCore = ({
|
|
|
57
55
|
value,
|
|
58
56
|
},
|
|
59
57
|
}) => {
|
|
60
|
-
if (value ===
|
|
58
|
+
if (value === UNSUBSCRIBE_TAG) {
|
|
61
59
|
lodashForEach(supportedModules, (module) => {
|
|
62
60
|
if (module.mandatory && (currentModule === module.context)) {
|
|
63
61
|
if (!hasUnsubscribeTag(contentForUnsubscribeScan)) {
|
|
@@ -76,8 +74,8 @@ export const validateTagsCore = ({
|
|
|
76
74
|
while (match !== null) {
|
|
77
75
|
const tagValue = match[1].trim();
|
|
78
76
|
const ifSkipped = skipTagsFn(tagValue);
|
|
79
|
-
if (ifSkipped && tagValue.indexOf(
|
|
80
|
-
const missingTagIndex = response.missingTags.indexOf(
|
|
77
|
+
if (ifSkipped && tagValue.indexOf(UNSUBSCRIBE_TAG) !== -1) {
|
|
78
|
+
const missingTagIndex = response.missingTags.indexOf(UNSUBSCRIBE_TAG);
|
|
81
79
|
if (missingTagIndex !== -1) {
|
|
82
80
|
response.missingTags.splice(missingTagIndex, 1);
|
|
83
81
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import '@testing-library/jest-dom';
|
|
2
2
|
import {
|
|
3
|
+
hasUnsubscribeTag,
|
|
4
|
+
validateTagsCore,
|
|
3
5
|
getTagMapValue,
|
|
4
6
|
getLoyaltyTagsMapValue,
|
|
5
7
|
getForwardedMapValues,
|
|
@@ -7,9 +9,161 @@ import {
|
|
|
7
9
|
validateIfTagClosed,
|
|
8
10
|
validateTags,
|
|
9
11
|
skipTags,
|
|
12
|
+
checkIfSupportedTag,
|
|
13
|
+
transformInjectedTags,
|
|
10
14
|
} from '../tagValidations';
|
|
11
15
|
import { containsBase64Images } from '../content';
|
|
12
16
|
|
|
17
|
+
describe('hasUnsubscribeTag', () => {
|
|
18
|
+
it('should return false when content is not a string', () => {
|
|
19
|
+
expect(hasUnsubscribeTag(null)).toBe(false);
|
|
20
|
+
expect(hasUnsubscribeTag(undefined)).toBe(false);
|
|
21
|
+
expect(hasUnsubscribeTag(123)).toBe(false);
|
|
22
|
+
expect(hasUnsubscribeTag({})).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return false when content has no unsubscribe tag', () => {
|
|
26
|
+
expect(hasUnsubscribeTag('Hello world')).toBe(false);
|
|
27
|
+
expect(hasUnsubscribeTag('{{ name }}')).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return true when content has {{ unsubscribe }}', () => {
|
|
31
|
+
expect(hasUnsubscribeTag('Click {{ unsubscribe }} here')).toBe(true);
|
|
32
|
+
expect(hasUnsubscribeTag('{{ unsubscribe }}')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('validateTagsCore', () => {
|
|
37
|
+
it('should include isContentEmpty: false when includeIsContentEmpty is true', () => {
|
|
38
|
+
const result = validateTagsCore({
|
|
39
|
+
contentForBraceCheck: '{{a}}',
|
|
40
|
+
contentForUnsubscribeScan: '{{a}}',
|
|
41
|
+
tags: null,
|
|
42
|
+
currentModule: 'default',
|
|
43
|
+
isFullMode: true,
|
|
44
|
+
includeIsContentEmpty: true,
|
|
45
|
+
});
|
|
46
|
+
expect(result.isContentEmpty).toBe(false);
|
|
47
|
+
expect(result.valid).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should use initialMissingTags when provided', () => {
|
|
51
|
+
const result = validateTagsCore({
|
|
52
|
+
contentForBraceCheck: '{{a}}',
|
|
53
|
+
contentForUnsubscribeScan: '{{a}}',
|
|
54
|
+
tags: [{ definition: { supportedModules: [], value: 'x' } }],
|
|
55
|
+
currentModule: 'default',
|
|
56
|
+
isFullMode: false,
|
|
57
|
+
initialMissingTags: ['requiredTag'],
|
|
58
|
+
});
|
|
59
|
+
expect(result.missingTags).toEqual(['requiredTag']);
|
|
60
|
+
expect(result.valid).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should use custom skipTagsFn when provided', () => {
|
|
64
|
+
const customSkip = jest.fn(() => true);
|
|
65
|
+
const result = validateTagsCore({
|
|
66
|
+
contentForBraceCheck: '{{ unsubscribe }}',
|
|
67
|
+
contentForUnsubscribeScan: '{{ unsubscribe }}',
|
|
68
|
+
tags: [
|
|
69
|
+
{
|
|
70
|
+
definition: {
|
|
71
|
+
supportedModules: [{ context: 'DEFAULT', mandatory: true }],
|
|
72
|
+
value: 'unsubscribe',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
currentModule: 'DEFAULT',
|
|
77
|
+
isFullMode: false,
|
|
78
|
+
skipTagsFn: customSkip,
|
|
79
|
+
});
|
|
80
|
+
expect(customSkip).toHaveBeenCalled();
|
|
81
|
+
expect(result.valid).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('checkIfSupportedTag', () => {
|
|
86
|
+
it('should return false for empty or no injected tags', () => {
|
|
87
|
+
expect(checkIfSupportedTag('someTag', {})).toBe(false);
|
|
88
|
+
expect(checkIfSupportedTag('someTag', undefined)).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return true when tag matches a top-level key', () => {
|
|
92
|
+
const injectedTags = { name: { name: 'Name' }, unsubscribe: { name: 'Unsubscribe' } };
|
|
93
|
+
expect(checkIfSupportedTag('name', injectedTags)).toBe(true);
|
|
94
|
+
expect(checkIfSupportedTag('unsubscribe', injectedTags)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return false when tag does not match any key', () => {
|
|
98
|
+
const injectedTags = { name: { name: 'Name' } };
|
|
99
|
+
expect(checkIfSupportedTag('other', injectedTags)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return true when tag matches a nested subtag', () => {
|
|
103
|
+
const injectedTags = {
|
|
104
|
+
customer: {
|
|
105
|
+
name: 'Customer',
|
|
106
|
+
subtags: {
|
|
107
|
+
first_name: { name: 'First Name' },
|
|
108
|
+
last_name: { name: 'Last Name' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
expect(checkIfSupportedTag('first_name', injectedTags)).toBe(true);
|
|
113
|
+
expect(checkIfSupportedTag('last_name', injectedTags)).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return false when tag is in neither top-level nor subtags', () => {
|
|
117
|
+
const injectedTags = {
|
|
118
|
+
customer: {
|
|
119
|
+
name: 'Customer',
|
|
120
|
+
subtags: { first_name: { name: 'First Name' } },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
expect(checkIfSupportedTag('unknown', injectedTags)).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('transformInjectedTags', () => {
|
|
128
|
+
it('should add tag-header and normalize subtags when key contains "subtags"', () => {
|
|
129
|
+
const tags = [
|
|
130
|
+
{
|
|
131
|
+
name: 'Customer',
|
|
132
|
+
subtags: {
|
|
133
|
+
first_name: { name: 'First Name' },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
const result = transformInjectedTags(tags);
|
|
138
|
+
expect(result).toBe(tags);
|
|
139
|
+
expect(tags[0]['tag-header']).toBe(true);
|
|
140
|
+
expect(tags[0].subtags).toEqual({ first_name: { name: 'First Name' } });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should recursively transform nested subtags', () => {
|
|
144
|
+
const tags = [
|
|
145
|
+
{
|
|
146
|
+
name: 'Parent',
|
|
147
|
+
subtags: {
|
|
148
|
+
child: {
|
|
149
|
+
name: 'Child',
|
|
150
|
+
subtags: { grandchild: { name: 'Grandchild' } },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
transformInjectedTags(tags);
|
|
156
|
+
expect(tags[0].subtags.child.subtags).toEqual({ grandchild: { name: 'Grandchild' } });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should return tags unchanged when no subtag keys exist', () => {
|
|
160
|
+
const tags = [{ name: 'Simple', desc: 'No subtags' }];
|
|
161
|
+
const result = transformInjectedTags(tags);
|
|
162
|
+
expect(result).toBe(tags);
|
|
163
|
+
expect(tags[0]).toEqual({ name: 'Simple', desc: 'No subtags' });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
13
167
|
describe("check if curly brackets are balanced", () => {
|
|
14
168
|
it("test for balanced curly brackets", () => {
|
|
15
169
|
let value = "hello {{optout"
|
|
@@ -18,6 +172,9 @@ describe("check if curly brackets are balanced", () => {
|
|
|
18
172
|
value += "}}"
|
|
19
173
|
let result = validateIfTagClosed(value);
|
|
20
174
|
expect(result).toEqual(true);
|
|
175
|
+
// no braces or empty string: match returns null, l1/l2/l3 undefined -> true
|
|
176
|
+
expect(validateIfTagClosed("")).toEqual(true);
|
|
177
|
+
expect(validateIfTagClosed("plain text no braces")).toEqual(true);
|
|
21
178
|
//valid cases
|
|
22
179
|
expect(validateIfTagClosed("{{{Hello}}}")).toEqual(true);
|
|
23
180
|
expect(validateIfTagClosed("{{{Hello}}")).toEqual(true);
|
|
@@ -1048,6 +1205,10 @@ describe('getForwardedMapValues', () => {
|
|
|
1048
1205
|
expect(getForwardedMapValues(input)).toEqual(expected);
|
|
1049
1206
|
});
|
|
1050
1207
|
|
|
1208
|
+
test('should return empty object when called with no argument (default param)', () => {
|
|
1209
|
+
expect(getForwardedMapValues()).toEqual({});
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1051
1212
|
test('should correctly process objects with subtags', () => {
|
|
1052
1213
|
const input = {
|
|
1053
1214
|
customer: {
|
|
@@ -1328,51 +1328,59 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1328
1328
|
}
|
|
1329
1329
|
onSubmitWrapper = (args) => {
|
|
1330
1330
|
const {singleTab = null} = args || {};
|
|
1331
|
-
|
|
1331
|
+
// Liquid validation (extractTags + Aira) only in library mode
|
|
1332
|
+
const runLiquidValidation = this.isLiquidFlowSupportedByChannel() && !this.props.isFullMode;
|
|
1333
|
+
if (runLiquidValidation) {
|
|
1332
1334
|
// For MPUSH, we need to validate both Android and iOS content separately
|
|
1333
1335
|
if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
|
|
1334
1336
|
this.validateFormBuilderMPush(this.state.formData, singleTab);
|
|
1335
1337
|
return;
|
|
1336
1338
|
}
|
|
1337
|
-
|
|
1338
|
-
// For other channels (EMAIL, SMS, INAPP)
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
(prevState) => ({
|
|
1345
|
-
liquidErrorMessage: {
|
|
1346
|
-
...prevState.liquidErrorMessage,
|
|
1347
|
-
STANDARD_ERROR_MSG: standardErrors,
|
|
1348
|
-
LIQUID_ERROR_MSG: liquidErrors,
|
|
1349
|
-
},
|
|
1350
|
-
}),
|
|
1351
|
-
() => {
|
|
1352
|
-
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage);
|
|
1353
|
-
this.props.stopValidation();
|
|
1354
|
-
}
|
|
1355
|
-
);
|
|
1356
|
-
};
|
|
1357
|
-
|
|
1358
|
-
const onSuccess = (contentToSubmit) => {
|
|
1359
|
-
const channel = this.props.channel || this.props?.schema?.channel?.toUpperCase();
|
|
1360
|
-
if(channel === EMAIL) {
|
|
1361
|
-
const content = this.state.formData?.base?.[this.props.baseLanguage]?.["template-content"] || "";
|
|
1362
|
-
this.handleLiquidTemplateSubmit(content);
|
|
1363
|
-
} else {
|
|
1364
|
-
this.handleLiquidTemplateSubmit(contentToSubmit);
|
|
1339
|
+
|
|
1340
|
+
// For other channels (EMAIL, SMS, INAPP): only call extractTags if there are no brace/empty errors already.
|
|
1341
|
+
// Run sync validation first; if it fails, block and show errors without calling the API.
|
|
1342
|
+
this.validateForm(null, null, true, false, () => {
|
|
1343
|
+
if (!this.state.isFormValid) {
|
|
1344
|
+
this.props.stopValidation();
|
|
1345
|
+
return;
|
|
1365
1346
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1347
|
+
const content = getChannelData(this.props.schema.channel || this.props.channel, this.state.formData, this.props.baseLanguage);
|
|
1348
|
+
|
|
1349
|
+
const onError = ({ standardErrors, liquidErrors }) => {
|
|
1350
|
+
this.setState(
|
|
1351
|
+
(prevState) => ({
|
|
1352
|
+
liquidErrorMessage: {
|
|
1353
|
+
...prevState.liquidErrorMessage,
|
|
1354
|
+
STANDARD_ERROR_MSG: standardErrors,
|
|
1355
|
+
LIQUID_ERROR_MSG: liquidErrors,
|
|
1356
|
+
},
|
|
1357
|
+
}),
|
|
1358
|
+
() => {
|
|
1359
|
+
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage);
|
|
1360
|
+
this.props.stopValidation();
|
|
1361
|
+
this.props.onFormValidityChange(false, this.state.errorData);
|
|
1362
|
+
}
|
|
1363
|
+
);
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
const onSuccess = (contentToSubmit) => {
|
|
1367
|
+
const channel = this.props.channel || this.props?.schema?.channel?.toUpperCase();
|
|
1368
|
+
if(channel === EMAIL) {
|
|
1369
|
+
const content = this.state.formData?.base?.[this.props.baseLanguage]?.["template-content"] || "";
|
|
1370
|
+
this.handleLiquidTemplateSubmit(content);
|
|
1371
|
+
} else {
|
|
1372
|
+
this.handleLiquidTemplateSubmit(contentToSubmit);
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
validateLiquidTemplateContent(content, {
|
|
1377
|
+
getLiquidTags: this.props.actions.getLiquidTags,
|
|
1378
|
+
formatMessage: this.props.intl.formatMessage,
|
|
1379
|
+
messages,
|
|
1380
|
+
onError,
|
|
1381
|
+
onSuccess,
|
|
1382
|
+
skipTags: this.skipTags.bind(this)
|
|
1383
|
+
});
|
|
1376
1384
|
});
|
|
1377
1385
|
} else {
|
|
1378
1386
|
this.props.onSubmit(this.state.formData);
|
|
@@ -1399,7 +1407,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1399
1407
|
|
|
1400
1408
|
// Set up callbacks for error and success handling
|
|
1401
1409
|
const onLiquidError = ({ standardErrors, liquidErrors }) => {
|
|
1402
|
-
|
|
1403
1410
|
this.setState(
|
|
1404
1411
|
(prevState) => ({
|
|
1405
1412
|
liquidErrorMessage: {
|
|
@@ -1411,6 +1418,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1411
1418
|
() => {
|
|
1412
1419
|
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.state.currentTab);
|
|
1413
1420
|
this.props.stopValidation();
|
|
1421
|
+
// Block save: tell parent form is invalid so Done/submit is blocked
|
|
1422
|
+
this.props.onFormValidityChange(false, this.state.errorData);
|
|
1414
1423
|
}
|
|
1415
1424
|
);
|
|
1416
1425
|
};
|
|
@@ -566,6 +566,7 @@ export function SlideBoxContent(props) {
|
|
|
566
566
|
handleTestAndPreview={handleTestAndPreview}
|
|
567
567
|
handleCloseTestAndPreview={handleCloseTestAndPreview}
|
|
568
568
|
isTestAndPreviewMode={isTestAndPreviewMode}
|
|
569
|
+
onValidationFail={onValidationFail}
|
|
569
570
|
/>
|
|
570
571
|
)}
|
|
571
572
|
{isEditFTP && (
|
|
@@ -632,6 +633,7 @@ export function SlideBoxContent(props) {
|
|
|
632
633
|
getLiquidTags={getLiquidTags}
|
|
633
634
|
getDefaultTags={type}
|
|
634
635
|
isFullMode={isFullMode}
|
|
636
|
+
onValidationFail={onValidationFail}
|
|
635
637
|
forwardedTags={forwardedTags}
|
|
636
638
|
selectedOfferDetails={selectedOfferDetails}
|
|
637
639
|
onPreviewContentClicked={onPreviewContentClicked}
|
|
@@ -780,6 +782,7 @@ export function SlideBoxContent(props) {
|
|
|
780
782
|
<MobliPushEdit
|
|
781
783
|
getFormLibraryData={getFormData}
|
|
782
784
|
setIsLoadingContent={setIsLoadingContent}
|
|
785
|
+
getLiquidTags={getLiquidTags}
|
|
783
786
|
location={{
|
|
784
787
|
pathname: `/mobilepush/edit/`,
|
|
785
788
|
query,
|
|
@@ -852,6 +855,7 @@ export function SlideBoxContent(props) {
|
|
|
852
855
|
mobilePushCreateMode={mobilePushCreateMode}
|
|
853
856
|
isGetFormData={isGetFormData}
|
|
854
857
|
getFormData={getFormData}
|
|
858
|
+
getLiquidTags={getLiquidTags}
|
|
855
859
|
templateData={templateData}
|
|
856
860
|
type={type}
|
|
857
861
|
step={templateStep}
|
|
@@ -880,7 +884,7 @@ export function SlideBoxContent(props) {
|
|
|
880
884
|
/>
|
|
881
885
|
) : (
|
|
882
886
|
<MobilePushNew
|
|
883
|
-
key="creatives-mobilepush-
|
|
887
|
+
key="creatives-mobilepush-create-new"
|
|
884
888
|
date={new Date().getMilliseconds()}
|
|
885
889
|
setIsLoadingContent={setIsLoadingContent}
|
|
886
890
|
onMobilepushModeChange={onMobilepushModeChange}
|
|
@@ -6,7 +6,7 @@ import CapError from '@capillarytech/cap-ui-library/CapError';
|
|
|
6
6
|
import PropTypes from 'prop-types';
|
|
7
7
|
import messages from './messages';
|
|
8
8
|
import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
|
|
9
|
-
import { PREVIEW } from './constants';
|
|
9
|
+
import { PREVIEW, EMAIL, SMS, EDIT_TEMPLATE, MOBILE_PUSH } from './constants';
|
|
10
10
|
import { EMAIL_CREATE_MODES } from '../EmailWrapper/constants';
|
|
11
11
|
import { hasSupportCKEditor } from '../../utils/common';
|
|
12
12
|
import { getMessageForDevice, getTitleForDevice } from '../../utils/commonUtils';
|
|
@@ -15,7 +15,7 @@ function getFullModeSaveBtn(slidBoxContent, isCreatingTemplate) {
|
|
|
15
15
|
if (isCreatingTemplate) {
|
|
16
16
|
return <FormattedMessage {...messages.creativesTemplatesDone} />;
|
|
17
17
|
}
|
|
18
|
-
return slidBoxContent ===
|
|
18
|
+
return slidBoxContent === EDIT_TEMPLATE
|
|
19
19
|
? <FormattedMessage {...messages.creativesTemplatesUpdate} />
|
|
20
20
|
: <FormattedMessage {...messages.creativesTemplatesSaveFullMode} />;
|
|
21
21
|
}
|
|
@@ -52,9 +52,13 @@ function SlideBoxFooter(props) {
|
|
|
52
52
|
// Calculate if buttons should be disabled
|
|
53
53
|
// Only apply validation state checks for EMAIL channel in HTML Editor mode (not BEE/DragDrop)
|
|
54
54
|
// For other channels, BEE editor, or when htmlEditorValidationState is not provided, don't disable based on validation
|
|
55
|
-
const isEmailChannel = currentChannel?.toUpperCase() ===
|
|
56
|
-
const isSmsChannel = currentChannel?.toUpperCase() ===
|
|
57
|
-
|
|
55
|
+
const isEmailChannel = currentChannel?.toUpperCase() === EMAIL;
|
|
56
|
+
const isSmsChannel = currentChannel?.toUpperCase() === SMS;
|
|
57
|
+
// Use templateData.type in library/edit so footer shows when editing a mobile push template (currentChannel may not be set to template channel)
|
|
58
|
+
const isMobilePushChannel =
|
|
59
|
+
currentChannel?.toUpperCase() === MOBILE_PUSH ||
|
|
60
|
+
(templateData?.type && templateData.type.toUpperCase() === MOBILE_PUSH);
|
|
61
|
+
const isEditMode = slidBoxContent === EDIT_TEMPLATE;
|
|
58
62
|
|
|
59
63
|
// Use selectedEmailCreateMode for accurate mode detection in create mode (emailCreateMode is mapped for backwards compatibility)
|
|
60
64
|
// In edit mode: htmlEditorValidationState is initialized as {} but only updated by HTML Editor
|
|
@@ -130,9 +134,11 @@ function SlideBoxFooter(props) {
|
|
|
130
134
|
const isBEEEditorModeInCreate = !isHTMLEditorMode && !isEditMode;
|
|
131
135
|
const isBEEEditorMode = isBEEEditorModeInEdit || isBEEEditorModeInCreate;
|
|
132
136
|
const hasBEEEditorErrors = isEmailChannel && isBEEEditorMode && (hasStandardErrors || hasLiquidErrors) && (!htmlEditorValidationState || !htmlEditorHasErrors);
|
|
133
|
-
const hasSmsValidationErrors = isSmsChannel && hasStandardErrors;
|
|
137
|
+
const hasSmsValidationErrors = isSmsChannel && (hasStandardErrors || hasLiquidErrors);
|
|
138
|
+
// Mobile Push OLD: footer only for extractTags/Aira liquid errors, not standard tag errors
|
|
139
|
+
const hasMobilePushValidationErrors = isMobilePushChannel && hasLiquidErrors;
|
|
134
140
|
|
|
135
|
-
const shouldShowErrorInfoNote = hasBEEEditorErrors || hasSmsValidationErrors || isSupportCKEditor;
|
|
141
|
+
const shouldShowErrorInfoNote = hasBEEEditorErrors || hasSmsValidationErrors || hasMobilePushValidationErrors || isSupportCKEditor;
|
|
136
142
|
|
|
137
143
|
// Check for personalization tokens in title/message when anonymous user tries to save
|
|
138
144
|
const hasPersonalizationTokens = () => {
|
|
@@ -1777,8 +1777,18 @@ export class Creatives extends React.Component {
|
|
|
1777
1777
|
}
|
|
1778
1778
|
|
|
1779
1779
|
showLiquidErrorInFooter = (errorMessagesFromFormBuilder, currentFormBuilderTab) => {
|
|
1780
|
+
const liquidMsgs = get(errorMessagesFromFormBuilder, constants.LIQUID_ERROR_MSG, []);
|
|
1781
|
+
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);
|
|
1784
|
+
const isLiquidValidationError = hasLiquid || hasStandard;
|
|
1785
|
+
// 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
|
+
const isMobilePush = this.state.currentChannel?.toUpperCase() === constants.MOBILE_PUSH;
|
|
1787
|
+
if (!hasLiquid && !hasStandard && this.state.isLiquidValidationError && isMobilePush) {
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1780
1790
|
this.setState({
|
|
1781
|
-
isLiquidValidationError
|
|
1791
|
+
isLiquidValidationError,
|
|
1782
1792
|
liquidErrorMessage: errorMessagesFromFormBuilder,
|
|
1783
1793
|
activeFormBuilderTab: currentFormBuilderTab === 1 ? constants.ANDROID : (currentFormBuilderTab === 2 ? constants.IOS : null), // Update activeFormBuilderTab, default to 1 if undefined
|
|
1784
1794
|
});
|
|
@@ -482,6 +482,18 @@ const EmailHTMLEditor = (props) => {
|
|
|
482
482
|
const handleContentChange = useCallback((content) => {
|
|
483
483
|
setHtmlContent(content);
|
|
484
484
|
|
|
485
|
+
// Clear previous liquid/API validation errors so Done button can be enabled after user fixes content
|
|
486
|
+
setApiValidationErrors({
|
|
487
|
+
liquidErrors: [],
|
|
488
|
+
standardErrors: [],
|
|
489
|
+
});
|
|
490
|
+
if (showLiquidErrorInFooter) {
|
|
491
|
+
showLiquidErrorInFooter({
|
|
492
|
+
STANDARD_ERROR_MSG: [],
|
|
493
|
+
LIQUID_ERROR_MSG: [],
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
485
497
|
// Validate tags
|
|
486
498
|
if (tags.length > 0 || !isEmpty(injectedTags)) {
|
|
487
499
|
const validationResult = validateTags({
|
|
@@ -498,7 +510,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
498
510
|
setTagValidationError(null);
|
|
499
511
|
}
|
|
500
512
|
}
|
|
501
|
-
}, [tags, injectedTags, location, getDefaultTags, eventContextTags]);
|
|
513
|
+
}, [tags, injectedTags, location, getDefaultTags, eventContextTags, showLiquidErrorInFooter]);
|
|
502
514
|
|
|
503
515
|
// Store the last validation state received from HTMLEditor
|
|
504
516
|
const lastValidationStateRef = useRef(null);
|
|
@@ -913,13 +925,9 @@ const EmailHTMLEditor = (props) => {
|
|
|
913
925
|
}
|
|
914
926
|
};
|
|
915
927
|
|
|
916
|
-
//
|
|
917
|
-
if (getLiquidTags) {
|
|
918
|
-
// Note: API validation errors are already cleared at the start of handleSave
|
|
919
|
-
// This ensures fresh validation on every save attempt
|
|
920
|
-
|
|
928
|
+
// Liquid validation (extractTags) only in library mode
|
|
929
|
+
if (getLiquidTags && !isFullMode) {
|
|
921
930
|
const onError = ({ standardErrors, liquidErrors }) => {
|
|
922
|
-
// Store API validation errors in state so they can be displayed in UI
|
|
923
931
|
setApiValidationErrors({
|
|
924
932
|
liquidErrors: liquidErrors || [],
|
|
925
933
|
standardErrors: standardErrors || [],
|
|
@@ -931,15 +939,12 @@ const EmailHTMLEditor = (props) => {
|
|
|
931
939
|
LIQUID_ERROR_MSG: liquidErrors || [],
|
|
932
940
|
});
|
|
933
941
|
}
|
|
934
|
-
// Don't reset ref here - liquid validation is async and resetting causes infinite loop
|
|
935
|
-
// The parent's isGetFormData will be reset by onValidationFail, and the next click will be detected
|
|
936
942
|
if (onValidationFail) {
|
|
937
943
|
onValidationFail();
|
|
938
944
|
}
|
|
939
945
|
};
|
|
940
946
|
|
|
941
947
|
const onSuccess = () => {
|
|
942
|
-
// Clear API validation errors on success
|
|
943
948
|
setApiValidationErrors({
|
|
944
949
|
liquidErrors: [],
|
|
945
950
|
standardErrors: [],
|