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

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.
Files changed (55) hide show
  1. package/constants/unified.js +1 -3
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +85 -4
  6. package/utils/tagValidations.js +223 -83
  7. package/utils/tests/commonUtil.test.js +124 -147
  8. package/utils/tests/tagValidations.test.js +358 -441
  9. package/v2Components/ErrorInfoNote/index.js +5 -2
  10. package/v2Components/FormBuilder/index.js +203 -137
  11. package/v2Components/FormBuilder/messages.js +8 -0
  12. package/v2Components/HtmlEditor/HTMLEditor.js +11 -2
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  15. package/v2Components/HtmlEditor/_htmlEditor.scss +6 -1
  16. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +10 -10
  17. package/v2Components/HtmlEditor/components/DeviceToggle/FLOW_AND_CLICK_BEHAVIOUR.md +70 -0
  18. package/v2Components/HtmlEditor/hooks/useInAppContent.js +15 -8
  19. package/v2Containers/Cap/mockData.js +14 -0
  20. package/v2Containers/Cap/reducer.js +55 -3
  21. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  22. package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -5
  23. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  24. package/v2Containers/CreativesContainer/index.js +10 -30
  25. package/v2Containers/Email/index.js +5 -1
  26. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
  27. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +137 -29
  28. package/v2Containers/FTP/index.js +51 -2
  29. package/v2Containers/FTP/messages.js +4 -0
  30. package/v2Containers/InApp/index.js +139 -22
  31. package/v2Containers/InApp/tests/index.test.js +6 -17
  32. package/v2Containers/InappAdvance/index.js +118 -8
  33. package/v2Containers/InappAdvance/tests/index.test.js +2 -3
  34. package/v2Containers/Line/Container/Text/index.js +1 -0
  35. package/v2Containers/MobilePush/Create/index.js +19 -42
  36. package/v2Containers/MobilePush/Edit/index.js +19 -42
  37. package/v2Containers/MobilePushNew/index.js +32 -12
  38. package/v2Containers/MobilepushWrapper/index.js +1 -3
  39. package/v2Containers/Rcs/index.js +37 -12
  40. package/v2Containers/Sms/Create/index.js +3 -39
  41. package/v2Containers/Sms/Create/messages.js +0 -4
  42. package/v2Containers/Sms/Edit/index.js +3 -35
  43. package/v2Containers/Sms/commonMethods.js +6 -3
  44. package/v2Containers/SmsTrai/Edit/index.js +47 -11
  45. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  46. package/v2Containers/SmsWrapper/index.js +0 -2
  47. package/v2Containers/Viber/index.js +1 -0
  48. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  49. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  50. package/v2Containers/WebPush/Create/index.js +2 -2
  51. package/v2Containers/WebPush/Create/utils/validation.js +2 -17
  52. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -59
  53. package/v2Containers/Whatsapp/index.js +17 -9
  54. package/v2Containers/Zalo/index.js +11 -3
  55. package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
@@ -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 { isEmailUnsubscribeTagOptional } from '../../../../utils/common';
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
- isEmailUnsubscribeTagOptional: jest.fn(() => false),
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
- isEmailUnsubscribeTagOptional.mockReturnValue(false);
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
- isEmailUnsubscribeTagOptional.mockReturnValue(false);
1109
+ isEmailUnsubscribeTagMandatory.mockReturnValue(false);
1025
1110
  renderWithIntl({
1026
1111
  isGetFormData: true,
1027
1112
  subject: 'Valid Subject',
@@ -1031,44 +1116,49 @@ describe('EmailHTMLEditor', () => {
1031
1116
  // Should proceed with save (validation passes)
1032
1117
  });
1033
1118
 
1034
- it('blocks save when liquid API validation fails for Email', async () => {
1035
- // Liquid validation runs in library mode (!isFullMode). Simulate API validation failure via mock onError.
1036
- validateLiquidTemplateContent.mockImplementation((content, options) => {
1037
- options.onError({ standardErrors: [], liquidErrors: ['Validation failed'] });
1038
- return Promise.resolve(false);
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'],
1039
1125
  });
1040
1126
  const onValidationFail = jest.fn();
1127
+ const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
1128
+
1129
+ // Set subject and content via component interactions
1041
1130
  const { rerender } = renderWithIntl({
1042
1131
  onValidationFail,
1043
1132
  isGetFormData: false,
1044
- isFullMode: false,
1045
1133
  metaEntities: {
1046
1134
  tags: {
1047
1135
  standard: [{ name: 'customer.name' }],
1048
1136
  },
1049
1137
  },
1050
- getLiquidTags: jest.fn((content, cb) => cb({ askAiraResponse: { data: [] }, isError: false })),
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
1059
1148
  {...defaultProps}
1060
1149
  onValidationFail={onValidationFail}
1061
1150
  isGetFormData
1062
- isFullMode={false}
1063
1151
  metaEntities={{
1064
1152
  tags: {
1065
1153
  standard: [{ name: 'customer.name' }],
1066
1154
  },
1067
1155
  }}
1068
- getLiquidTags={jest.fn((content, cb) => cb({ askAiraResponse: { data: [] }, isError: false }))} />
1156
+ getLiquidTags={null} />
1069
1157
  </IntlProvider>
1070
1158
  );
1159
+
1071
1160
  await waitFor(() => {
1161
+ expect(CapNotification.error).toHaveBeenCalled();
1072
1162
  expect(onValidationFail).toHaveBeenCalled();
1073
1163
  }, { timeout: 3000 });
1074
1164
  });
@@ -1085,15 +1175,16 @@ describe('EmailHTMLEditor', () => {
1085
1175
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1086
1176
  mockGetAllIssues.mockReturnValue([]);
1087
1177
 
1088
- // Liquid validation runs only in library mode
1178
+ // Set subject and content via component interactions
1089
1179
  const { rerender } = renderWithIntl({
1090
1180
  isGetFormData: false,
1091
- isFullMode: false,
1181
+ isFullMode: true,
1092
1182
  metaEntities: {
1093
1183
  tags: {
1094
1184
  standard: [{ name: 'customer.name' }],
1095
1185
  },
1096
1186
  },
1187
+ isLiquidEnabled: true,
1097
1188
  getLiquidTags,
1098
1189
  });
1099
1190
  const input = screen.getByTestId('subject-input');
@@ -1115,7 +1206,7 @@ describe('EmailHTMLEditor', () => {
1115
1206
  <EmailHTMLEditor
1116
1207
  {...defaultProps}
1117
1208
  isGetFormData
1118
- isFullMode={false}
1209
+ isFullMode
1119
1210
  metaEntities={{
1120
1211
  tags: {
1121
1212
  standard: [{ name: 'customer.name' }],
@@ -1139,10 +1230,11 @@ describe('EmailHTMLEditor', () => {
1139
1230
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1140
1231
  mockGetAllIssues.mockReturnValue([]);
1141
1232
 
1142
- // Liquid validation runs only in library mode
1233
+ // Set subject and content via component interactions
1143
1234
  const { rerender } = renderWithIntl({
1144
1235
  isGetFormData: false,
1145
- isFullMode: false,
1236
+ isFullMode: true,
1237
+ isLiquidEnabled: true,
1146
1238
  getLiquidTags,
1147
1239
  });
1148
1240
  const input = screen.getByTestId('subject-input');
@@ -1161,7 +1253,7 @@ describe('EmailHTMLEditor', () => {
1161
1253
  await act(async () => {
1162
1254
  rerender(
1163
1255
  <IntlProvider locale="en" messages={{}}>
1164
- <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} />
1256
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} />
1165
1257
  </IntlProvider>
1166
1258
  );
1167
1259
  });
@@ -1188,10 +1280,11 @@ describe('EmailHTMLEditor', () => {
1188
1280
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1189
1281
  mockGetAllIssues.mockReturnValue([]);
1190
1282
 
1191
- // Liquid validation runs only in library mode
1283
+ // Set subject and content via component interactions
1192
1284
  const { rerender } = renderWithIntl({
1193
1285
  isGetFormData: false,
1194
- isFullMode: false,
1286
+ isFullMode: true,
1287
+ isLiquidEnabled: true,
1195
1288
  getLiquidTags,
1196
1289
  showLiquidErrorInFooter,
1197
1290
  onValidationFail,
@@ -1212,7 +1305,7 @@ describe('EmailHTMLEditor', () => {
1212
1305
  await act(async () => {
1213
1306
  rerender(
1214
1307
  <IntlProvider locale="en" messages={{}}>
1215
- <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} showLiquidErrorInFooter={showLiquidErrorInFooter} onValidationFail={onValidationFail} />
1308
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} showLiquidErrorInFooter={showLiquidErrorInFooter} onValidationFail={onValidationFail} />
1216
1309
  </IntlProvider>
1217
1310
  );
1218
1311
  });
@@ -1232,20 +1325,27 @@ describe('EmailHTMLEditor', () => {
1232
1325
  return Promise.resolve(true);
1233
1326
  });
1234
1327
 
1235
- const getFormdata = jest.fn();
1328
+ const emailActions = {
1329
+ ...defaultProps.emailActions,
1330
+ transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
1331
+ createTemplate: jest.fn((obj, callback) => {
1332
+ callback({ templateId: { _id: '123', versions: {} } });
1333
+ }),
1334
+ };
1236
1335
  const getLiquidTags = jest.fn((content, callback) => {
1237
1336
  callback({ askAiraResponse: { data: [] }, isError: false });
1238
1337
  });
1239
1338
  // Ensure no HTML/Label/Liquid errors from HtmlEditor
1240
1339
  mockGetAllIssues.mockReturnValue([]);
1241
1340
 
1242
- // Liquid validation runs only in library mode; then performSave calls getFormdata
1341
+ // Set subject and content via component interactions
1243
1342
  const { rerender } = renderWithIntl({
1244
1343
  isGetFormData: false,
1245
- isFullMode: false,
1344
+ isFullMode: true,
1345
+ isLiquidEnabled: true,
1246
1346
  getLiquidTags,
1247
- getFormdata,
1248
- location: { query: { module: 'library' } },
1347
+ emailActions,
1348
+ templateName: 'New Template',
1249
1349
  });
1250
1350
  const input = screen.getByTestId('subject-input');
1251
1351
  await act(async () => {
@@ -1263,7 +1363,7 @@ describe('EmailHTMLEditor', () => {
1263
1363
  await act(async () => {
1264
1364
  rerender(
1265
1365
  <IntlProvider locale="en" messages={{}}>
1266
- <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} getFormdata={getFormdata} location={{ query: { module: 'library' } }} />
1366
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} emailActions={emailActions} templateName="New Template" />
1267
1367
  </IntlProvider>
1268
1368
  );
1269
1369
  });
@@ -1273,11 +1373,12 @@ describe('EmailHTMLEditor', () => {
1273
1373
  }, { timeout: 5000 });
1274
1374
 
1275
1375
  await waitFor(() => {
1276
- expect(getFormdata).toHaveBeenCalled();
1376
+ expect(emailActions.createTemplate).toHaveBeenCalled();
1277
1377
  }, { timeout: 5000 });
1278
1378
  });
1279
1379
 
1280
1380
  it('saves in full mode with create template', async () => {
1381
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1281
1382
  const emailActions = {
1282
1383
  ...defaultProps.emailActions,
1283
1384
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1311,6 +1412,7 @@ describe('EmailHTMLEditor', () => {
1311
1412
  });
1312
1413
 
1313
1414
  it('saves in full mode with edit template', async () => {
1415
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1314
1416
  const emailActions = {
1315
1417
  ...defaultProps.emailActions,
1316
1418
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1380,6 +1482,7 @@ describe('EmailHTMLEditor', () => {
1380
1482
  });
1381
1483
 
1382
1484
  it('handles create template error response', async () => {
1485
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1383
1486
  const emailActions = {
1384
1487
  ...defaultProps.emailActions,
1385
1488
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1414,6 +1517,7 @@ describe('EmailHTMLEditor', () => {
1414
1517
  });
1415
1518
 
1416
1519
  it('handles create template success with getFormdata', async () => {
1520
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1417
1521
  const emailActions = {
1418
1522
  ...defaultProps.emailActions,
1419
1523
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1450,6 +1554,7 @@ describe('EmailHTMLEditor', () => {
1450
1554
  });
1451
1555
 
1452
1556
  it('handles create template success without getFormdata', async () => {
1557
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1453
1558
  const emailActions = {
1454
1559
  ...defaultProps.emailActions,
1455
1560
  transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
@@ -1485,6 +1590,7 @@ describe('EmailHTMLEditor', () => {
1485
1590
  });
1486
1591
 
1487
1592
  it('saves in library mode with getFormdata', async () => {
1593
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1488
1594
  const getFormdata = jest.fn();
1489
1595
 
1490
1596
  // Set subject and content via component interactions
@@ -1512,6 +1618,7 @@ describe('EmailHTMLEditor', () => {
1512
1618
  });
1513
1619
 
1514
1620
  it('saves in library mode without library module', async () => {
1621
+ mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
1515
1622
  const getFormdata = jest.fn();
1516
1623
 
1517
1624
  // Set subject and content via component interactions
@@ -1651,6 +1758,7 @@ describe('EmailHTMLEditor', () => {
1651
1758
  renderWithIntl({
1652
1759
  getLiquidTags: null,
1653
1760
  globalActions,
1761
+ isLiquidEnabled: true,
1654
1762
  isGetFormData: true,
1655
1763
  subject: 'Valid Subject',
1656
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',