@capillarytech/creatives-library 8.0.287-alpha.3 → 8.0.288
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 +1 -0
- package/initialState.js +2 -0
- package/package.json +1 -1
- package/utils/common.js +8 -5
- package/utils/commonUtils.js +111 -2
- package/utils/tagValidations.js +222 -84
- package/utils/tests/commonUtil.test.js +118 -147
- package/utils/tests/tagValidations.test.js +358 -280
- package/v2Components/CapTagList/index.js +7 -2
- package/v2Components/CapTagListWithInput/index.js +4 -0
- package/v2Components/ErrorInfoNote/index.js +5 -2
- package/v2Components/FormBuilder/index.js +187 -74
- package/v2Components/FormBuilder/messages.js +12 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +5 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +2 -1
- package/v2Containers/Cap/mockData.js +14 -0
- package/v2Containers/Cap/reducer.js +55 -3
- package/v2Containers/Cap/tests/reducer.test.js +102 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +20 -0
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +40 -6
- package/v2Containers/CreativesContainer/constants.js +6 -0
- package/v2Containers/CreativesContainer/index.js +36 -5
- package/v2Containers/CreativesContainer/messages.js +12 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +339 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +18 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +37 -0
- package/v2Containers/Email/index.js +5 -1
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +62 -10
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +115 -12
- package/v2Containers/FTP/index.js +51 -2
- package/v2Containers/FTP/messages.js +4 -0
- package/v2Containers/InApp/index.js +96 -1
- package/v2Containers/InApp/tests/index.test.js +6 -17
- package/v2Containers/InappAdvance/index.js +103 -2
- package/v2Containers/Line/Container/Text/index.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +37 -1
- package/v2Containers/MobilePush/Create/messages.js +4 -0
- package/v2Containers/MobilePush/Edit/index.js +37 -2
- package/v2Containers/MobilePush/Edit/messages.js +4 -0
- package/v2Containers/MobilePushNew/components/PlatformContentFields.js +36 -12
- package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +68 -27
- package/v2Containers/MobilePushNew/index.js +92 -5
- package/v2Containers/MobilePushNew/messages.js +8 -0
- package/v2Containers/MobilepushWrapper/index.js +7 -1
- package/v2Containers/Rcs/index.js +37 -12
- package/v2Containers/Sms/Create/index.js +3 -31
- package/v2Containers/Sms/Create/messages.js +0 -4
- package/v2Containers/Sms/Edit/index.js +3 -29
- package/v2Containers/Sms/commonMethods.js +6 -6
- package/v2Containers/SmsTrai/Edit/index.js +47 -6
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/TagList/index.js +17 -1
- package/v2Containers/TagList/messages.js +4 -0
- package/v2Containers/TemplatesV2/index.js +43 -23
- package/v2Containers/Viber/index.js +1 -0
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
- package/v2Containers/WebPush/Create/index.js +25 -6
- package/v2Containers/WebPush/Create/messages.js +8 -1
- package/v2Containers/WebPush/Create/utils/validation.js +20 -22
- package/v2Containers/WebPush/Create/utils/validation.test.js +52 -0
- package/v2Containers/Whatsapp/index.js +17 -9
- package/v2Containers/Zalo/index.js +11 -3
|
@@ -13,7 +13,7 @@ import { IntlProvider } from 'react-intl';
|
|
|
13
13
|
import EmailHTMLEditor from '../EmailHTMLEditor';
|
|
14
14
|
import { validateLiquidTemplateContent } from '../../../../utils/commonUtils';
|
|
15
15
|
import { validateTags } from '../../../../utils/tagValidations';
|
|
16
|
-
import {
|
|
16
|
+
import { isEmailUnsubscribeTagMandatory } from '../../../../utils/common';
|
|
17
17
|
|
|
18
18
|
// Mock dependencies
|
|
19
19
|
jest.mock('../../../../utils/commonUtils', () => ({
|
|
@@ -24,8 +24,11 @@ jest.mock('../../../../utils/tagValidations', () => ({
|
|
|
24
24
|
validateTags: jest.fn(),
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
|
+
// Create mutable mock for hasLiquidSupportFeature
|
|
28
|
+
const mockHasLiquidSupportFeature = jest.fn(() => true);
|
|
27
29
|
jest.mock('../../../../utils/common', () => ({
|
|
28
|
-
|
|
30
|
+
hasLiquidSupportFeature: (...args) => mockHasLiquidSupportFeature(...args),
|
|
31
|
+
isEmailUnsubscribeTagMandatory: jest.fn(() => false),
|
|
29
32
|
}));
|
|
30
33
|
|
|
31
34
|
jest.mock('../../../../utils/history', () => ({
|
|
@@ -417,7 +420,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
417
420
|
jest.clearAllMocks();
|
|
418
421
|
validateLiquidTemplateContent.mockResolvedValue(true);
|
|
419
422
|
validateTags.mockReturnValue({ valid: true });
|
|
420
|
-
|
|
423
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
421
424
|
// Reset mock functions
|
|
422
425
|
mockGetAllIssues.mockReturnValue([]);
|
|
423
426
|
mockGetValidationState.mockReturnValue({
|
|
@@ -425,6 +428,88 @@ describe('EmailHTMLEditor', () => {
|
|
|
425
428
|
hasErrors: false,
|
|
426
429
|
issueCounts: { errors: 0, warnings: 0, total: 0 },
|
|
427
430
|
});
|
|
431
|
+
// Reset hasLiquidSupportFeature mock to return true by default
|
|
432
|
+
mockHasLiquidSupportFeature.mockReturnValue(true);
|
|
433
|
+
capturedApiValidationErrorsRef.current = null;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('mergedApiValidationErrors (lines 124-125)', () => {
|
|
437
|
+
beforeEach(() => {
|
|
438
|
+
global.__captureApiValidationErrorsRef = capturedApiValidationErrorsRef;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
afterEach(() => {
|
|
442
|
+
delete global.__captureApiValidationErrorsRef;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('merges tag unsupported errors into standardErrors when tagValidationError has unsupportedTags', async () => {
|
|
446
|
+
validateTags.mockReturnValue({
|
|
447
|
+
valid: false,
|
|
448
|
+
unsupportedTags: ['tagA', 'tagB'],
|
|
449
|
+
});
|
|
450
|
+
renderWithIntl({
|
|
451
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
452
|
+
tags: [{ name: 'customer.name' }],
|
|
453
|
+
supportedTags: [],
|
|
454
|
+
});
|
|
455
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
456
|
+
await act(async () => {
|
|
457
|
+
fireEvent.click(changeButton);
|
|
458
|
+
});
|
|
459
|
+
await waitFor(() => {
|
|
460
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
461
|
+
expect(capturedApiValidationErrorsRef.current.liquidErrors).toEqual([]);
|
|
462
|
+
expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
|
|
463
|
+
'Unsupported tags are: tagA, tagB',
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('merges tag missing errors into standardErrors when tagValidationError has missingTags and unsubscribe not mandatory', async () => {
|
|
469
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
470
|
+
validateTags.mockReturnValue({
|
|
471
|
+
valid: false,
|
|
472
|
+
missingTags: ['unsubscribe'],
|
|
473
|
+
});
|
|
474
|
+
renderWithIntl({
|
|
475
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
476
|
+
tags: [{ name: 'customer.name' }],
|
|
477
|
+
supportedTags: [],
|
|
478
|
+
});
|
|
479
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
480
|
+
await act(async () => {
|
|
481
|
+
fireEvent.click(changeButton);
|
|
482
|
+
});
|
|
483
|
+
await waitFor(() => {
|
|
484
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
485
|
+
expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
|
|
486
|
+
'Missing tags are: unsubscribe',
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('uses apiValidationErrors.liquidErrors and concatenates apiValidationErrors.standardErrors with tag messages (merge shape)', async () => {
|
|
492
|
+
// When tag messages exist, mergedApiValidationErrors returns liquidErrors from apiValidationErrors
|
|
493
|
+
// and standardErrors = [...(apiValidationErrors?.standardErrors || []), ...tagMessages] (lines 124-125)
|
|
494
|
+
validateTags.mockReturnValue({
|
|
495
|
+
valid: false,
|
|
496
|
+
unsupportedTags: ['customTag'],
|
|
497
|
+
});
|
|
498
|
+
renderWithIntl({
|
|
499
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
500
|
+
tags: [{ name: 'customer.name' }],
|
|
501
|
+
});
|
|
502
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
503
|
+
await act(async () => {
|
|
504
|
+
fireEvent.click(changeButton);
|
|
505
|
+
});
|
|
506
|
+
await waitFor(() => {
|
|
507
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
508
|
+
const { liquidErrors, standardErrors } = capturedApiValidationErrorsRef.current;
|
|
509
|
+
expect(liquidErrors).toEqual([]);
|
|
510
|
+
expect(standardErrors).toContain('Unsupported tags are: customTag');
|
|
511
|
+
});
|
|
512
|
+
});
|
|
428
513
|
});
|
|
429
514
|
|
|
430
515
|
describe('Default Parameter Values (lines 60-63)', () => {
|
|
@@ -1021,7 +1106,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1021
1106
|
});
|
|
1022
1107
|
|
|
1023
1108
|
it('allows save when unsubscribe validation is on and tag is present', () => {
|
|
1024
|
-
|
|
1109
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
1025
1110
|
renderWithIntl({
|
|
1026
1111
|
isGetFormData: true,
|
|
1027
1112
|
subject: 'Valid Subject',
|
|
@@ -1031,14 +1116,17 @@ describe('EmailHTMLEditor', () => {
|
|
|
1031
1116
|
// Should proceed with save (validation passes)
|
|
1032
1117
|
});
|
|
1033
1118
|
|
|
1034
|
-
it('blocks save
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1119
|
+
it('blocks save for non-liquid orgs when tag validation fails', async () => {
|
|
1120
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1121
|
+
validateTags.mockReturnValue({
|
|
1122
|
+
valid: false,
|
|
1123
|
+
unsupportedTags: ['tag1'],
|
|
1124
|
+
missingTags: ['tag2'],
|
|
1040
1125
|
});
|
|
1041
1126
|
const onValidationFail = jest.fn();
|
|
1127
|
+
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
1128
|
+
|
|
1129
|
+
// Set subject and content via component interactions
|
|
1042
1130
|
const { rerender } = renderWithIntl({
|
|
1043
1131
|
onValidationFail,
|
|
1044
1132
|
isGetFormData: false,
|
|
@@ -1047,12 +1135,13 @@ describe('EmailHTMLEditor', () => {
|
|
|
1047
1135
|
standard: [{ name: 'customer.name' }],
|
|
1048
1136
|
},
|
|
1049
1137
|
},
|
|
1050
|
-
getLiquidTags:
|
|
1138
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
1051
1139
|
});
|
|
1052
1140
|
const input = screen.getByTestId('subject-input');
|
|
1053
1141
|
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1054
1142
|
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1055
1143
|
fireEvent.click(changeButton);
|
|
1144
|
+
// Now trigger save
|
|
1056
1145
|
rerender(
|
|
1057
1146
|
<IntlProvider locale="en" messages={{}}>
|
|
1058
1147
|
<EmailHTMLEditor
|
|
@@ -1064,10 +1153,12 @@ describe('EmailHTMLEditor', () => {
|
|
|
1064
1153
|
standard: [{ name: 'customer.name' }],
|
|
1065
1154
|
},
|
|
1066
1155
|
}}
|
|
1067
|
-
getLiquidTags={
|
|
1156
|
+
getLiquidTags={null} />
|
|
1068
1157
|
</IntlProvider>
|
|
1069
1158
|
);
|
|
1159
|
+
|
|
1070
1160
|
await waitFor(() => {
|
|
1161
|
+
expect(CapNotification.error).toHaveBeenCalled();
|
|
1071
1162
|
expect(onValidationFail).toHaveBeenCalled();
|
|
1072
1163
|
}, { timeout: 3000 });
|
|
1073
1164
|
});
|
|
@@ -1093,6 +1184,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1093
1184
|
standard: [{ name: 'customer.name' }],
|
|
1094
1185
|
},
|
|
1095
1186
|
},
|
|
1187
|
+
isLiquidEnabled: true,
|
|
1096
1188
|
getLiquidTags,
|
|
1097
1189
|
});
|
|
1098
1190
|
const input = screen.getByTestId('subject-input');
|
|
@@ -1142,6 +1234,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1142
1234
|
const { rerender } = renderWithIntl({
|
|
1143
1235
|
isGetFormData: false,
|
|
1144
1236
|
isFullMode: true,
|
|
1237
|
+
isLiquidEnabled: true,
|
|
1145
1238
|
getLiquidTags,
|
|
1146
1239
|
});
|
|
1147
1240
|
const input = screen.getByTestId('subject-input');
|
|
@@ -1191,6 +1284,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1191
1284
|
const { rerender } = renderWithIntl({
|
|
1192
1285
|
isGetFormData: false,
|
|
1193
1286
|
isFullMode: true,
|
|
1287
|
+
isLiquidEnabled: true,
|
|
1194
1288
|
getLiquidTags,
|
|
1195
1289
|
showLiquidErrorInFooter,
|
|
1196
1290
|
onValidationFail,
|
|
@@ -1248,6 +1342,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1248
1342
|
const { rerender } = renderWithIntl({
|
|
1249
1343
|
isGetFormData: false,
|
|
1250
1344
|
isFullMode: true,
|
|
1345
|
+
isLiquidEnabled: true,
|
|
1251
1346
|
getLiquidTags,
|
|
1252
1347
|
emailActions,
|
|
1253
1348
|
templateName: 'New Template',
|
|
@@ -1283,6 +1378,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1283
1378
|
});
|
|
1284
1379
|
|
|
1285
1380
|
it('saves in full mode with create template', async () => {
|
|
1381
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1286
1382
|
const emailActions = {
|
|
1287
1383
|
...defaultProps.emailActions,
|
|
1288
1384
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1316,6 +1412,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1316
1412
|
});
|
|
1317
1413
|
|
|
1318
1414
|
it('saves in full mode with edit template', async () => {
|
|
1415
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1319
1416
|
const emailActions = {
|
|
1320
1417
|
...defaultProps.emailActions,
|
|
1321
1418
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1385,6 +1482,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1385
1482
|
});
|
|
1386
1483
|
|
|
1387
1484
|
it('handles create template error response', async () => {
|
|
1485
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1388
1486
|
const emailActions = {
|
|
1389
1487
|
...defaultProps.emailActions,
|
|
1390
1488
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1419,6 +1517,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1419
1517
|
});
|
|
1420
1518
|
|
|
1421
1519
|
it('handles create template success with getFormdata', async () => {
|
|
1520
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1422
1521
|
const emailActions = {
|
|
1423
1522
|
...defaultProps.emailActions,
|
|
1424
1523
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1455,6 +1554,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1455
1554
|
});
|
|
1456
1555
|
|
|
1457
1556
|
it('handles create template success without getFormdata', async () => {
|
|
1557
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1458
1558
|
const emailActions = {
|
|
1459
1559
|
...defaultProps.emailActions,
|
|
1460
1560
|
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
@@ -1490,6 +1590,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1490
1590
|
});
|
|
1491
1591
|
|
|
1492
1592
|
it('saves in library mode with getFormdata', async () => {
|
|
1593
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1493
1594
|
const getFormdata = jest.fn();
|
|
1494
1595
|
|
|
1495
1596
|
// Set subject and content via component interactions
|
|
@@ -1517,6 +1618,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1517
1618
|
});
|
|
1518
1619
|
|
|
1519
1620
|
it('saves in library mode without library module', async () => {
|
|
1621
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1520
1622
|
const getFormdata = jest.fn();
|
|
1521
1623
|
|
|
1522
1624
|
// Set subject and content via component interactions
|
|
@@ -1656,6 +1758,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
1656
1758
|
renderWithIntl({
|
|
1657
1759
|
getLiquidTags: null,
|
|
1658
1760
|
globalActions,
|
|
1761
|
+
isLiquidEnabled: true,
|
|
1659
1762
|
isGetFormData: true,
|
|
1660
1763
|
subject: 'Valid Subject',
|
|
1661
1764
|
htmlContent: '<p>Content</p>',
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
CapTooltip,
|
|
23
23
|
} from '@capillarytech/cap-ui-library';
|
|
24
24
|
import { FONT_SIZE_L } from '@capillarytech/cap-ui-library/styled/variables';
|
|
25
|
-
import _, { find, cloneDeep, findIndex, isEmpty, isEqual, filter, replace } from 'lodash';
|
|
25
|
+
import _, { find, cloneDeep, findIndex, isEmpty, isEqual, filter, flattenDeep, replace } from 'lodash';
|
|
26
26
|
import * as actions from './actions';
|
|
27
27
|
import { makeSelectFTP, makeSelectMetaEntities } from './selectors';
|
|
28
28
|
import { makeSelectLoyaltyPromotionDisplay, setInjectedTags } from '../Cap/selectors';
|
|
@@ -33,6 +33,7 @@ import * as globalActions from '../Cap/actions';
|
|
|
33
33
|
import { TagList } from '../TagList';
|
|
34
34
|
import { NO_COMMUNICATION, CREATE, EDIT, PREVIEW } from '../App/constants';
|
|
35
35
|
import { getTreeStructuredTags } from '../../utils/common';
|
|
36
|
+
import { transformInjectedTags, checkIfSupportedTag, skipTags } from '../../utils/tagValidations';
|
|
36
37
|
import injectSaga from '../../utils/injectSaga';
|
|
37
38
|
import injectReducer from '../../utils/injectReducer';
|
|
38
39
|
|
|
@@ -234,16 +235,63 @@ export class FTP extends React.Component {
|
|
|
234
235
|
}));
|
|
235
236
|
};
|
|
236
237
|
|
|
238
|
+
getFlatTags = (tags) => {
|
|
239
|
+
const flatTags = [];
|
|
240
|
+
tags.forEach((tag) => {
|
|
241
|
+
if ((tag.children || []).length) {
|
|
242
|
+
const innerTags = this.getFlatTags(tag.children);
|
|
243
|
+
flatTags.push(innerTags);
|
|
244
|
+
} else {
|
|
245
|
+
flatTags.push(tag.value);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return flattenDeep(flatTags);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
validateTags(content, tagsParam, injectedTagsParams) {
|
|
253
|
+
const tags = tagsParam;
|
|
254
|
+
const injectedTags = transformInjectedTags(injectedTagsParams);
|
|
255
|
+
const response = {};
|
|
256
|
+
response.valid = true;
|
|
257
|
+
response.unsupportedTags = [];
|
|
258
|
+
const flatTags = this.getFlatTags(tags);
|
|
259
|
+
if (flatTags && flatTags.length) {
|
|
260
|
+
const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
|
|
261
|
+
const matchedTags = [...content.matchAll(regex)];
|
|
262
|
+
matchedTags.forEach((tag) => {
|
|
263
|
+
let ifSupported = !!flatTags.find((t) => t === tag[0]);
|
|
264
|
+
const tagValue = tag[0].substring(this.indexOfEnd(tag[0], '{{'), tag[0].indexOf('}}'));
|
|
265
|
+
ifSupported = ifSupported || checkIfSupportedTag(tagValue, injectedTags) || skipTags(tagValue);
|
|
266
|
+
if (!ifSupported) {
|
|
267
|
+
response.unsupportedTags.push(tagValue);
|
|
268
|
+
response.valid = false;
|
|
269
|
+
}
|
|
270
|
+
if (response.unsupportedTags.length === 0) {
|
|
271
|
+
response.valid = true;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return response;
|
|
276
|
+
}
|
|
277
|
+
|
|
237
278
|
indexOfEnd(targetString, string) {
|
|
238
279
|
const io = targetString.indexOf(string);
|
|
239
280
|
return io == -1 ? -1 : io + string.length;
|
|
240
281
|
}
|
|
241
282
|
|
|
242
283
|
getMessageContent = () => {
|
|
243
|
-
const { messageContent } = this.state;
|
|
284
|
+
const { messageContent, tagsTree = []} = this.state;
|
|
244
285
|
const { formatMessage } = this.props.intl;
|
|
245
286
|
const { metaEntities, selectedOfferDetails, injectedTags } = this.props;
|
|
246
287
|
const tagsRaw = metaEntities && metaEntities.tags ? metaEntities.tags.standard : [];
|
|
288
|
+
const validateTagResponse = !this.props?.isFullMode ? this.validateTags(messageContent, tagsTree, injectedTags) : { valid: true, unsupportedTags: [] };
|
|
289
|
+
let unsupportedTags = null;
|
|
290
|
+
let errorMessageText = '';
|
|
291
|
+
if (!validateTagResponse.valid) {
|
|
292
|
+
unsupportedTags = validateTagResponse.unsupportedTags.join(', ').toString();
|
|
293
|
+
errorMessageText = formatMessage(messages.unsupportedTagsValidationError, {unsupportedTags});
|
|
294
|
+
}
|
|
247
295
|
return (
|
|
248
296
|
<CapRow>
|
|
249
297
|
<CapColumn span={11}>
|
|
@@ -261,6 +309,7 @@ export class FTP extends React.Component {
|
|
|
261
309
|
label={formatMessage(messages.messageHeader)}
|
|
262
310
|
onChange={this.updateMessageBody}
|
|
263
311
|
value={messageContent}
|
|
312
|
+
errorMessage={errorMessageText}
|
|
264
313
|
/>
|
|
265
314
|
</div>
|
|
266
315
|
</CapColumn>
|
|
@@ -21,6 +21,10 @@ export default defineMessages({
|
|
|
21
21
|
id: 'creatives.containersV2.FTP.addColumn',
|
|
22
22
|
defaultMessage: 'Add column',
|
|
23
23
|
},
|
|
24
|
+
unsupportedTagsValidationError: {
|
|
25
|
+
id: 'creatives.containersV2.FTP.unsupportedTagsValidationError',
|
|
26
|
+
defaultMessage: 'Unsupported tags: {unsupportedTags}. Please remove them from this message.',
|
|
27
|
+
},
|
|
24
28
|
selectTag: {
|
|
25
29
|
id: 'creatives.containersV2.FTP.selectTag',
|
|
26
30
|
defaultMessage: 'Select tag',
|
|
@@ -31,6 +31,7 @@ import creativesMessages from '../CreativesContainer/messages';
|
|
|
31
31
|
import withCreatives from "../../hoc/withCreatives";
|
|
32
32
|
import UnifiedPreview from "../../v2Components/CommonTestAndPreview/UnifiedPreview";
|
|
33
33
|
import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
|
|
34
|
+
import { validateTags } from "../../utils/tagValidations";
|
|
34
35
|
import injectReducer from '../../utils/injectReducer';
|
|
35
36
|
import v2InAppReducer from './reducer';
|
|
36
37
|
import { v2InAppSagas } from './sagas';
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
import { getCdnUrl } from "../../utils/cdnTransformation";
|
|
58
59
|
import { getCtaObject, hasAnyErrors, getSingleTab } from "./utils";
|
|
59
60
|
import { validateInAppContent } from "../../utils/commonUtils";
|
|
61
|
+
import { hasLiquidSupportFeature } from "../../utils/common";
|
|
60
62
|
import formBuilderMessages from "../../v2Components/FormBuilder/messages";
|
|
61
63
|
import HTMLEditor from "../../v2Components/HtmlEditor";
|
|
62
64
|
import { HTML_EDITOR_VARIANTS } from "../../v2Components/HtmlEditor/constants";
|
|
@@ -1093,7 +1095,9 @@ export const InApp = (props) => {
|
|
|
1093
1095
|
// Skip validation if no tags are available (e.g., in tests or when tags haven't loaded)
|
|
1094
1096
|
const hasTags = tags && tags.length > 0;
|
|
1095
1097
|
|
|
1096
|
-
|
|
1098
|
+
// For liquid flow, use validateInAppContent
|
|
1099
|
+
if (isLiquidFlow && hasTags) {
|
|
1100
|
+
// Validate the INAPP content
|
|
1097
1101
|
const payload = createPayload();
|
|
1098
1102
|
validateInAppContent(payload, {
|
|
1099
1103
|
currentTab: panes === ANDROID ? 1 : 2, // Convert ANDROID/IOS to tab numbers
|
|
@@ -1102,6 +1106,10 @@ export const InApp = (props) => {
|
|
|
1102
1106
|
getLiquidTags: (content, callback) => globalActions.getLiquidTags(content, callback),
|
|
1103
1107
|
formatMessage,
|
|
1104
1108
|
messages: formBuilderMessages,
|
|
1109
|
+
tagLookupMap: metaEntities?.tagLookupMap || {},
|
|
1110
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1111
|
+
isLiquidFlow,
|
|
1112
|
+
forwardedTags: {},
|
|
1105
1113
|
skipTags: (tag) => {
|
|
1106
1114
|
// Skip certain tags if needed
|
|
1107
1115
|
const skipRegexes = [
|
|
@@ -1116,11 +1124,98 @@ export const InApp = (props) => {
|
|
|
1116
1124
|
},
|
|
1117
1125
|
singleTab: getSingleTab(accountData),
|
|
1118
1126
|
});
|
|
1127
|
+
} else if (hasTags) {
|
|
1128
|
+
// For non-liquid flow, validate tags using validateTags (only if tags are available)
|
|
1129
|
+
const androidContent = htmlContentAndroid || '';
|
|
1130
|
+
const iosContent = htmlContentIos || '';
|
|
1131
|
+
|
|
1132
|
+
const androidSupported = get(accountData, 'selectedWeChatAccount.configs.android') === DEVICE_SUPPORTED || get(editContent, 'ANDROID.deviceType') === ANDROID;
|
|
1133
|
+
const iosSupported = get(accountData, 'selectedWeChatAccount.configs.ios') === DEVICE_SUPPORTED || get(editContent, 'IOS.deviceType') === IOS_CAPITAL;
|
|
1134
|
+
|
|
1135
|
+
let hasErrors = false;
|
|
1136
|
+
const newErrors = {
|
|
1137
|
+
STANDARD_ERROR_MSG: {
|
|
1138
|
+
ANDROID: [],
|
|
1139
|
+
IOS: [],
|
|
1140
|
+
GENERIC: [],
|
|
1141
|
+
},
|
|
1142
|
+
LIQUID_ERROR_MSG: {
|
|
1143
|
+
ANDROID: [],
|
|
1144
|
+
IOS: [],
|
|
1145
|
+
GENERIC: [],
|
|
1146
|
+
},
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
// Validate Android content
|
|
1150
|
+
if (androidSupported && androidContent && androidContent?.trim() !== '') {
|
|
1151
|
+
const validationResponse = validateTags({
|
|
1152
|
+
content: androidContent,
|
|
1153
|
+
tagsParam: tags,
|
|
1154
|
+
injectedTagsParams: injectedTags || {},
|
|
1155
|
+
location,
|
|
1156
|
+
tagModule: getDefaultTags,
|
|
1157
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1158
|
+
isFullMode,
|
|
1159
|
+
}) || {};
|
|
1160
|
+
|
|
1161
|
+
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
1162
|
+
hasErrors = true;
|
|
1163
|
+
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
1164
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1165
|
+
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
1166
|
+
})
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
if (validationResponse?.isBraceError) {
|
|
1170
|
+
hasErrors = true;
|
|
1171
|
+
newErrors.LIQUID_ERROR_MSG.ANDROID.push(
|
|
1172
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Validate iOS content
|
|
1178
|
+
if (iosSupported && iosContent && iosContent?.trim() !== '') {
|
|
1179
|
+
const validationResponse = validateTags({
|
|
1180
|
+
content: iosContent,
|
|
1181
|
+
tagsParam: tags,
|
|
1182
|
+
injectedTagsParams: injectedTags || {},
|
|
1183
|
+
location,
|
|
1184
|
+
tagModule: getDefaultTags,
|
|
1185
|
+
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1186
|
+
isFullMode,
|
|
1187
|
+
}) || {};
|
|
1188
|
+
|
|
1189
|
+
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
1190
|
+
hasErrors = true;
|
|
1191
|
+
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
1192
|
+
formatMessage(globalMessages.unsupportedTagsValidationError, {
|
|
1193
|
+
unsupportedTags: validationResponse.unsupportedTags.join(", "),
|
|
1194
|
+
})
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
if (validationResponse?.isBraceError) {
|
|
1198
|
+
hasErrors = true;
|
|
1199
|
+
newErrors.LIQUID_ERROR_MSG.IOS.push(
|
|
1200
|
+
formatMessage(globalMessages.unbalanacedCurlyBraces)
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (hasErrors) {
|
|
1206
|
+
setErrorMessage(newErrors);
|
|
1207
|
+
} else {
|
|
1208
|
+
// No errors, proceed with submission
|
|
1209
|
+
onSuccess();
|
|
1210
|
+
}
|
|
1119
1211
|
} else {
|
|
1212
|
+
// No tags available, skip validation and proceed directly
|
|
1120
1213
|
onSuccess();
|
|
1121
1214
|
}
|
|
1122
1215
|
};
|
|
1123
1216
|
|
|
1217
|
+
const isLiquidFlow = hasLiquidSupportFeature();
|
|
1218
|
+
|
|
1124
1219
|
// Check template data to determine editor type (for render decision)
|
|
1125
1220
|
const templateDetails = isFullMode ? editData?.templateDetails : templateData;
|
|
1126
1221
|
const versions = templateDetails?.versions || {};
|
|
@@ -21,16 +21,18 @@ 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';
|
|
25
24
|
|
|
26
25
|
const mockActions = {
|
|
27
26
|
getTemplateInfoById: jest.fn(),
|
|
28
27
|
resetEditTemplate: jest.fn(),
|
|
29
|
-
getTemplateDetails: jest.fn((id,
|
|
28
|
+
getTemplateDetails: jest.fn((id, callback) => {
|
|
30
29
|
// Simulate successful template details fetch to prevent loading state
|
|
31
30
|
// The callback is setSpin function, call it with false to stop spinner
|
|
32
|
-
if (
|
|
33
|
-
setTimeout
|
|
31
|
+
if (callback && typeof callback === 'function') {
|
|
32
|
+
// Use setTimeout to ensure it's called after render
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
callback(false);
|
|
35
|
+
}, 0);
|
|
34
36
|
}
|
|
35
37
|
}),
|
|
36
38
|
editTemplate: jest.fn(),
|
|
@@ -39,9 +41,6 @@ const mockActions = {
|
|
|
39
41
|
};
|
|
40
42
|
const mockGlobalActions = {
|
|
41
43
|
fetchSchemaForEntity: jest.fn(),
|
|
42
|
-
getLiquidTags: jest.fn((content, callback) =>
|
|
43
|
-
callback({ askAiraResponse: { data: [], errors: [] }, isError: false }),
|
|
44
|
-
),
|
|
45
44
|
};
|
|
46
45
|
|
|
47
46
|
jest.mock('../../../v2Containers/TagList/index.js', () => ({
|
|
@@ -67,17 +66,7 @@ const renderComponent = (props) =>
|
|
|
67
66
|
);
|
|
68
67
|
|
|
69
68
|
describe('Test activity inApp container', () => {
|
|
70
|
-
afterEach(() => {
|
|
71
|
-
jest.restoreAllMocks();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
69
|
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
|
-
});
|
|
81
70
|
renderComponent({
|
|
82
71
|
actions: mockActions,
|
|
83
72
|
globalActions: mockGlobalActions,
|