@capillarytech/creatives-library 8.0.353-alpha.5 → 8.0.353-alpha.6

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 (136) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapTagList/index.js +10 -0
  15. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +213 -21
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  22. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +0 -17
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -146
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +138 -48
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -4
  29. package/v2Components/CommonTestAndPreview/index.js +691 -235
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +0 -159
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -256
  42. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -2
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -198
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +36 -26
  46. package/v2Components/FormBuilder/index.js +11 -6
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +956 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +38 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -31
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +15 -3
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/App/constants.js +0 -3
  71. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  72. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  73. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  74. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  75. package/v2Containers/CreativesContainer/constants.js +9 -0
  76. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  77. package/v2Containers/CreativesContainer/index.js +322 -103
  78. package/v2Containers/CreativesContainer/index.scss +51 -1
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  88. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  89. package/v2Containers/Rcs/constants.js +119 -10
  90. package/v2Containers/Rcs/index.js +2445 -813
  91. package/v2Containers/Rcs/index.scss +280 -8
  92. package/v2Containers/Rcs/messages.js +34 -3
  93. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  94. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  95. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  96. package/v2Containers/Rcs/tests/index.test.js +152 -121
  97. package/v2Containers/Rcs/tests/mockData.js +38 -0
  98. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  99. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  100. package/v2Containers/Rcs/utils.js +478 -11
  101. package/v2Containers/Sms/Create/index.js +106 -40
  102. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  103. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  104. package/v2Containers/SmsTrai/Create/index.js +9 -4
  105. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  106. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  107. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  108. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  109. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  110. package/v2Containers/SmsWrapper/index.js +37 -8
  111. package/v2Containers/TagList/index.js +6 -0
  112. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  113. package/v2Containers/Templates/_templates.scss +166 -9
  114. package/v2Containers/Templates/actions.js +11 -0
  115. package/v2Containers/Templates/constants.js +2 -0
  116. package/v2Containers/Templates/index.js +122 -120
  117. package/v2Containers/Templates/sagas.js +56 -12
  118. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  119. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  120. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  121. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  122. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  123. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  124. package/v2Containers/TemplatesV2/index.js +86 -23
  125. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  126. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  127. package/v2Containers/WebPush/Create/index.js +8 -91
  128. package/v2Containers/WebPush/Create/index.scss +0 -7
  129. package/v2Containers/Whatsapp/index.js +3 -20
  130. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  131. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +0 -169
  132. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +0 -522
  133. package/v2Containers/App/tests/constants.test.js +0 -61
  134. package/v2Containers/Templates/tests/webpush.test.js +0 -375
  135. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +0 -338
  136. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +0 -325
@@ -104,9 +104,6 @@ import {
104
104
  VIBER as VIBER_CHANNEL,
105
105
  FACEBOOK as FACEBOOK_CHANNEL,
106
106
  CREATE,
107
- EXTERNAL_URL,
108
- URL,
109
- SITE_URL,
110
107
  } from '../App/constants';
111
108
  import {MAX_WHATSAPP_TEMPLATES, WARNING_WHATSAPP_TEMPLATES , ACCOUNT_MAPPING_ON_CHANNEL, noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc, noApprovedWhatsappZaloTemplatesTitle, noApprovedWhatsappTemplatesDesc, zaloDescIllustration, noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc, ARCHIVE_STATUS_ACTIVE, ARCHIVE_STATUS_ARCHIVED, ARCHIVE_REFRESH_TYPE_ARCHIVE, ARCHIVE_REFRESH_TYPE_UNARCHIVE} from './constants';
112
109
  import { COPY_OF, EMBEDDED } from '../../constants/unified';
@@ -125,7 +122,7 @@ import { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } from '../InApp/const
125
122
  import { ZALO_STATUS_OPTIONS, ZALO_STATUSES } from '../Zalo/constants';
126
123
  import { getWhatsappContent, getWhatsappStatus, getWhatsappCategory, getWhatsappCta, getWhatsappQuickReply, getWhatsappAutoFill, getWhatsappCarouselButtonView } from '../Whatsapp/utils';
127
124
  import { getRCSContent } from '../Rcs/utils';
128
- import {RCS_STATUSES} from '../Rcs/constants';
125
+ import { RCS_STATUSES, HOST_INFOBIP } from '../Rcs/constants';
129
126
  import zaloMessages from '../Zalo/messages';
130
127
  import rcsMessages from '../Rcs/messages';
131
128
  import inAppMessages from '../InApp/messages';
@@ -463,7 +460,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
463
460
  if (this.props.location.query.type === 'embedded') {
464
461
  this.props.actions.resetAccount();
465
462
  }
466
- if (['line', VIBER_CHANNEL, FACEBOOK_CHANNEL, 'sms', 'email', 'ebill'].includes((this.state.channel || '').toLowerCase())) {
463
+ // When using local templates (e.g. SMS fallback selector), do not fetch from API or we overwrite global store and break background RCS list
464
+ const useLocalTemplates = get(
465
+ this.props,
466
+ 'localTemplatesConfig.useLocalTemplates',
467
+ get(this.props, 'useLocalTemplates', false),
468
+ );
469
+ if (!useLocalTemplates && ['line', VIBER_CHANNEL, FACEBOOK_CHANNEL, 'sms', 'email', 'ebill'].includes((this.state.channel || '').toLowerCase())) {
467
470
  const queryParams = {
468
471
  // name: this.state.searchText,
469
472
  // sortBy: this.state.sortBy,
@@ -1009,8 +1012,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1009
1012
 
1010
1013
  componentWillUnmount() {
1011
1014
  window.removeEventListener("message", this.handleFrameTasks);
1012
- this.props.actions.resetTemplateStoreData();
1013
- this.props.globalActions.clearMetaEntities();
1015
+ // When using local templates (e.g. SMS fallback selector), do not clear global store or background RCS list is wiped
1016
+ const useLocalTemplates = get(
1017
+ this.props,
1018
+ 'localTemplatesConfig.useLocalTemplates',
1019
+ get(this.props, 'useLocalTemplates', false),
1020
+ );
1021
+ if (!useLocalTemplates) {
1022
+ this.props.actions.resetTemplateStoreData();
1023
+ this.props.globalActions.clearMetaEntities();
1024
+ }
1014
1025
  // Clear any pending timeouts to prevent memory leaks
1015
1026
  if (this._clearEditTimeout) {
1016
1027
  clearTimeout(this._clearEditTimeout);
@@ -1402,69 +1413,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1402
1413
  };
1403
1414
  }
1404
1415
 
1405
- case WEBPUSH: {
1406
- // WebPush content is stored in creatives format (brandIcon, onClickAction, ctas)
1407
- // Must be transformed to campaign/test-message format matching getTemplateContent() in WebPush/Create/index.js
1408
- const webpushContent = get(baseContent, 'content.webpush', {});
1409
- const title = webpushContent?.title || '';
1410
- const message = webpushContent?.message || '';
1411
- const accountId = get(template, 'definition.accountId', null);
1412
- const templateName = template?.name || '';
1413
-
1414
- // iconImageUrl stored as brandIcon in creatives format
1415
- const iconImageUrl = webpushContent?.brandIcon || webpushContent?.iconImageUrl || undefined;
1416
-
1417
- // cta stored as onClickAction in creatives format (type: URL|SITE_URL, url)
1418
- // or already as cta in campaign format (type: EXTERNAL_URL|SITE_URL, actionLink)
1419
- let cta = null;
1420
- const onClickAction = webpushContent?.onClickAction;
1421
- const existingCta = webpushContent?.cta;
1422
- if (onClickAction) {
1423
- const ctaType = onClickAction.type === URL ? EXTERNAL_URL : (onClickAction.type || SITE_URL);
1424
- cta = { type: ctaType, actionLink: onClickAction.url || '' };
1425
- } else if (existingCta) {
1426
- cta = { type: existingCta.type || EXTERNAL_URL, actionLink: existingCta.actionLink || '' };
1427
- }
1428
-
1429
- // expandableDetails: image → media[], ctas[] → mapped ctas
1430
- const image = webpushContent?.image;
1431
- const rawCtas = webpushContent?.ctas;
1432
- const existingExpandable = webpushContent?.expandableDetails;
1433
- let expandableDetails = null;
1434
- const hasImage = !!image;
1435
- const hasCtas = Array.isArray(rawCtas) && rawCtas.length > 0;
1436
- if (hasImage || hasCtas) {
1437
- expandableDetails = {
1438
- media: hasImage ? [{ url: image, type: IMAGE }] : [],
1439
- ctas: hasCtas ? rawCtas.map((ctaItem) => ({
1440
- type: ctaItem?.type === URL ? EXTERNAL_URL : (ctaItem?.type || EXTERNAL_URL),
1441
- action: ctaItem?.action || '',
1442
- title: ctaItem?.actionText || ctaItem?.title || '',
1443
- actionLink: ctaItem?.actionLink || '',
1444
- })) : [],
1445
- };
1446
- } else if (existingExpandable) {
1447
- expandableDetails = {
1448
- media: existingExpandable?.media || [],
1449
- ctas: existingExpandable?.ctas || [],
1450
- };
1451
- }
1452
-
1453
- return {
1454
- channel: WEBPUSH,
1455
- accountId,
1456
- content: {
1457
- title,
1458
- message,
1459
- ...(iconImageUrl ? { iconImageUrl } : {}),
1460
- ...(cta ? { cta } : {}),
1461
- ...(expandableDetails ? { expandableDetails } : {}),
1462
- },
1463
- messageSubject: templateName || title,
1464
- offers: [],
1465
- };
1466
- }
1467
-
1468
1416
  default:
1469
1417
  console.warn(`Unsupported channel for content extraction: ${channelUpper}`);
1470
1418
  return null;
@@ -1477,7 +1425,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1477
1425
  * @returns {Boolean} - True if channel supports Test and Preview
1478
1426
  */
1479
1427
  isTestAndPreviewSupported = () => {
1480
- const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
1428
+ const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO];
1481
1429
  return supportedChannels.includes(this.state.channel.toUpperCase());
1482
1430
  }
1483
1431
 
@@ -1863,12 +1811,20 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1863
1811
  }
1864
1812
 
1865
1813
  filterRcsTemplates = (templates) => {
1866
- let { selectedRcsStatus } = this.state;
1867
- selectedRcsStatus = !this.props.isFullMode ? RCS_STATUSES.approved : '';
1868
- if (selectedRcsStatus) {
1869
- return templates?.filter((template) => template?.versions?.base?.content?.RCS?.rcsContent?.cardContent?.[0]?.Status === selectedRcsStatus);
1814
+ const selectedRcsAccountName = this.props?.Templates?.selectedRcsAccount?.name || '';
1815
+ const hostName = this.state?.hostName;
1816
+ let nextTemplates = templates || [];
1817
+ if (selectedRcsAccountName) {
1818
+ nextTemplates = nextTemplates.filter(
1819
+ (t) => get(t, 'versions.base.content.RCS.rcsContent.accountName', '') === selectedRcsAccountName
1820
+ );
1870
1821
  }
1871
- return templates;
1822
+ if (!this.props.isFullMode && hostName !== HOST_INFOBIP) {
1823
+ return nextTemplates.filter(
1824
+ (t) => get(t, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', 'unavailable') === RCS_STATUSES.approved
1825
+ );
1826
+ }
1827
+ return nextTemplates;
1872
1828
  }
1873
1829
 
1874
1830
  filterZaloTemplates = (templates) => {
@@ -2037,7 +1993,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2037
1993
  // Show preview icon only for channels that don't support Test and Preview
2038
1994
  (() => {
2039
1995
  // Channels that have Test and Preview integrated
2040
- const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
1996
+ const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO];
2041
1997
  const isTestAndPreviewSupported = testAndPreviewChannels.includes(currentChannel.toUpperCase());
2042
1998
 
2043
1999
  // Don't show preview icon if channel supports Test and Preview
@@ -2058,7 +2014,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2058
2014
  style={{ marginRight: "16px" }}
2059
2015
  type="eye"
2060
2016
  onClick={() => {
2061
- if (!this.props.isFullMode || this.props.isDltFromRcs) {
2017
+ if (!this.props.isFullMode || this.props.isDltFromRcs || this.props.isSmsFallbackFromRcs) {
2062
2018
  if (!get(template, "versions.base.content.zalo.previewUrl", "")) {
2063
2019
  this.setState({ zaloPreviewItemId: template?._id });
2064
2020
  }
@@ -2540,13 +2496,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2540
2496
  <CapRow type="flex" align="middle">
2541
2497
  {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2542
2498
  <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2543
- <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2499
+ {this.state.hostName !== HOST_INFOBIP && <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2544
2500
  <CapStatus
2545
2501
  type={statusDisplay}
2546
- text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2547
- labelType="label3"
2548
- />
2549
- </CapRow>
2502
+ text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2503
+ labelType="label3"
2504
+ />
2505
+ </CapRow>
2506
+ }
2550
2507
  </CapRow>
2551
2508
  );
2552
2509
 
@@ -3122,6 +3079,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3122
3079
  let routeParams = {};
3123
3080
  const {fbAdManager} = this.props;
3124
3081
  const isLibraryMode = this.isEnabledInLibraryModule("callCreateFromProps");
3082
+
3125
3083
  if (!isLibraryMode) {
3126
3084
  timeTracker.startTimer(CHANNEL_CREATE_TRACK_MAPPING[channel]);
3127
3085
  }
@@ -3471,6 +3429,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3471
3429
  this.setState({modeType});
3472
3430
  }
3473
3431
  const { _id: id } = template;
3432
+ const {
3433
+ localTemplatesConfig,
3434
+ fbAdManager,
3435
+ isDltFromRcs,
3436
+ isSmsFallbackFromRcs,
3437
+ onSelectTemplate,
3438
+ } = this.props;
3474
3439
  const type = this.props.location.query.type;
3475
3440
  const module = this.props.location.query.module;
3476
3441
  const isLanguageSupport = (this.props.location.query.isLanguageSupport) ? this.props.location.query.isLanguageSupport : false;
@@ -3556,10 +3521,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3556
3521
  }
3557
3522
  if (this.isEnabledInLibraryModule("callSelectFromProps")) {
3558
3523
  let data = id;
3559
- if (this.props.fbAdManager || this.props.isDltFromRcs) {
3524
+ if (localTemplatesConfig?.useLocalTemplates) {
3525
+ data = template;
3526
+ } else if (fbAdManager || isDltFromRcs || isSmsFallbackFromRcs) {
3560
3527
  data = this.selectTemplate(id);
3561
3528
  }
3562
- this.props.onSelectTemplate(data, this.props.fbAdManager);
3529
+ onSelectTemplate(data, fbAdManager);
3563
3530
  } else {
3564
3531
  timeTracker.startTimer(CHANNEL_EDIT_TRACK_MAPPING[this.state.channel.toLowerCase()]);
3565
3532
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4518,9 +4485,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4518
4485
  const isWechatEmbedded = !this.props.isFullMode && channel.toUpperCase() === WECHAT;
4519
4486
  const channelLowerCase = (channel || '').toLowerCase();
4520
4487
  const isTraiDltFeature = this.checkDLTfeatureEnable();
4521
-
4522
4488
  const createButton =
4523
- ( (channelLowerCase === WHATSAPP_LOWERCASE || channelLowerCase === RCS_LOWERCASE) && !this.props.isFullMode )
4489
+ (
4490
+ (
4491
+ channelLowerCase === WHATSAPP_LOWERCASE
4492
+ || channelLowerCase === RCS_LOWERCASE
4493
+ )
4494
+ && !this.props.isFullMode
4495
+ )
4524
4496
  ? (
4525
4497
  <CapLink
4526
4498
  onClick={this.openCreativesFullMode}
@@ -4552,17 +4524,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4552
4524
  const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4553
4525
  const _renderHasSelection = _isArchivalEnabled && this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4554
4526
 
4555
- const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4556
- {isfilterContentVisisble && <CapInput.Search
4557
- className="search-text"
4558
- style={{width: '210px'}}
4559
- placeholder={_isArchivedMode ? this.props.intl.formatMessage(messages.searchArchivedTemplates) : this.props.intl.formatMessage(messages.searchText)}
4560
- value={this.state.searchText}
4561
- onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4562
- disabled={this.checkSearchDisabled()}
4563
- onClear={() => this.searchTemplate('', this.state.channel)}
4564
- onScroll={(e) => e.stopPropagation()}
4565
- />}
4527
+ const useLocalTemplates = this.props.localTemplatesConfig?.useLocalTemplates;
4528
+ const builtFilterContent = ((isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && (
4529
+ <div className="action-container">
4530
+ <div className="action-container__toolbar-row">
4531
+ {isfilterContentVisisble ? (
4532
+ <CapInput.Search
4533
+ className="search-text"
4534
+ placeholder={this.props.intl.formatMessage(messages.searchText)}
4535
+ value={this.state.searchText}
4536
+ onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4537
+ onSearch={() => this.searchTemplate(this.state.searchText, this.state.channel)}
4538
+ onClear={() => this.searchTemplate('', this.state.channel)}
4539
+ onScroll={(e) => e.stopPropagation()}
4540
+ disabled={this.checkSearchDisabled()}
4541
+ />
4542
+ ) : null}
4566
4543
  {
4567
4544
  channel.toUpperCase() === WECHAT && <CapRadio.CapRadioGroup className="wechat-filters" defaultValue={wechatFilter} onChange={this.setWechatFilter}>
4568
4545
  <CapRadio.Button value={WECHAT_FILTERS.ALL}><CapLabel type="label2">
@@ -4709,16 +4686,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4709
4686
  )
4710
4687
  }
4711
4688
  <div className="template-listing-header-actions">
4712
- {!_isArchivedMode && !_renderHasSelection && (
4713
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4714
- <CapTooltip title={whatsappCountExceedText}>
4715
- <div className="button-disabled-tooltip-wrapper">
4716
- {createButton}
4717
- </div>
4718
- </CapTooltip>
4719
- )
4720
- : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4721
- )}
4722
4689
  {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active, archive flag enabled */}
4723
4690
  {commonUtil.hasCreativesArchivalEnabled() && !_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4724
4691
  <CapDropdown
@@ -4741,7 +4708,28 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4741
4708
  </CapDropdown>
4742
4709
  )}
4743
4710
  </div>
4744
- </div>);
4711
+ </div>
4712
+ <div>
4713
+ <div className="action-container__create-row">
4714
+ {
4715
+ isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && !this.props.isSmsFallbackFromRcs && (
4716
+ this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && isWhatsappCountExeeded ? (
4717
+ <CapTooltip title={whatsappCountExceedText}>
4718
+ <div className="button-disabled-tooltip-wrapper">
4719
+ {createButton}
4720
+ </div>
4721
+ </CapTooltip>
4722
+ ) : createButton
4723
+ )
4724
+ }
4725
+ </div>
4726
+ </div>
4727
+ </div>
4728
+ ));
4729
+ const localTemplatesFilterContent = get(this.props, 'localTemplatesConfig.localTemplatesFilterContent', null);
4730
+ const filterContent = (useLocalTemplates && localTemplatesFilterContent) != null
4731
+ ? localTemplatesFilterContent
4732
+ : builtFilterContent;
4745
4733
  let htmlPreviewContent = "";
4746
4734
  if (this.state.channel.toLowerCase() === 'ebill') {
4747
4735
  htmlPreviewContent = this.state.previewTemplate && this.state.previewTemplate.versions && this.state.previewTemplate.versions.base && this.state.previewTemplate.versions.base['ebill-editor'];
@@ -4751,7 +4739,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4751
4739
 
4752
4740
 
4753
4741
  const creativesParams = this.getCreativesParams();
4754
- const templates = this.props.TemplatesList || [];
4742
+ const templates = useLocalTemplates
4743
+ ? (this.props.localTemplatesConfig?.localTemplates || [])
4744
+ : (this.props.TemplatesList || []);
4745
+ const isLoadingWhenLocal = useLocalTemplates && !!this.props.localTemplatesConfig?.localTemplatesLoading;
4755
4746
  const {route} = this.props;
4756
4747
  const loadingTipMap = {
4757
4748
  sendingFile: 'uploadingFile',
@@ -4766,9 +4757,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4766
4757
  (deleteRcsTemplateInProgress && 'deletingTemplate') ||
4767
4758
  (this.props.EmailCreate.duplicateTemplateInProgress && 'duplicatingTemplate');
4768
4759
 
4769
- const loadingTip = messages[loadingTipIntl] ? this.props.intl.formatMessage(messages[loadingTipIntl]) : this.props.intl.formatMessage(messages.gettingAllTemplates);
4760
+ const loadingTip = useLocalTemplates && this.props.localTemplatesConfig?.localTemplatesLoadingTip
4761
+ ? this.props.localTemplatesConfig.localTemplatesLoadingTip
4762
+ : (messages[loadingTipIntl] ? this.props.intl.formatMessage(messages[loadingTipIntl]) : this.props.intl.formatMessage(messages.gettingAllTemplates));
4770
4763
  const showNoTemplatesFoundZalo = this.state.channel.toUpperCase() === ZALO && isEmpty(this.state.searchedZaloTemplates) && this.state.searchingZaloTemplate;
4771
- const showNoTemplatesFoundOther = ![ZALO].includes(this.state.channel.toUpperCase()) && isEmpty(this.props.TemplatesList) && !this.props.Templates.getAllTemplatesInProgress && !isEmpty(this.state.searchText);
4764
+ const showNoTemplatesFoundOther = ![ZALO].includes(this.state.channel.toUpperCase()) && isEmpty(templates) && (useLocalTemplates ? !isLoadingWhenLocal : !this.props.Templates.getAllTemplatesInProgress) && (useLocalTemplates || !isEmpty(this.state.searchText));
4772
4765
  const showNoTemplatesFound = showNoTemplatesFoundZalo || showNoTemplatesFoundOther;
4773
4766
 
4774
4767
  return (
@@ -4812,7 +4805,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4812
4805
  />
4813
4806
  ) : null}
4814
4807
 
4815
- {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode ? (
4808
+ {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode && this.state?.hostName !== HOST_INFOBIP ? (
4816
4809
  <CapInfoNote
4817
4810
  message={formatMessage(messages.rcsOnlyApprovedTemplates)}
4818
4811
  />
@@ -4825,22 +4818,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4825
4818
  ) : null}
4826
4819
  <CapRow>
4827
4820
  <Pagination
4828
- templateInProgress={
4829
- this.props.Templates.getAllTemplatesInProgress
4830
- }
4821
+ templateInProgress={useLocalTemplates ? isLoadingWhenLocal : this.props.Templates.getAllTemplatesInProgress}
4831
4822
  onPageChange={
4832
- templates.length ? this.onPaginationChange : () => {}
4823
+ templates.length
4824
+ ? (useLocalTemplates ? (this.props.localTemplatesConfig?.localTemplatesOnPageChange || (() => {})) : this.onPaginationChange)
4825
+ : () => {}
4833
4826
  }
4834
4827
  >
4835
4828
  {this.getTemplateDataForGrid({
4836
4829
  previewTemplateId: this.state.zaloPreviewItemId,
4837
- isLoading,
4838
- isInitialLoading,
4830
+ isLoading: useLocalTemplates ? isLoadingWhenLocal : isLoading,
4831
+ isInitialLoading: useLocalTemplates ? isLoadingWhenLocal && templates.length === 0 : isInitialLoading,
4839
4832
  loadingTip,
4840
4833
  channel: this.state.channel,
4841
4834
  templates: this.state.searchingZaloTemplate
4842
4835
  ? this.state.searchedZaloTemplates
4843
- : this.props.TemplatesList,
4836
+ : templates,
4844
4837
  filterContent,
4845
4838
  handlers: {
4846
4839
  handlePreviewClick: this.handlePreviewClick,
@@ -5023,6 +5016,15 @@ Templates.propTypes = {
5023
5016
  WebPush: PropTypes.object,
5024
5017
  smsRegister: PropTypes.any,
5025
5018
  isDltFromRcs: PropTypes.bool,
5019
+ isSmsFallbackFromRcs: PropTypes.bool,
5020
+ localTemplatesConfig: PropTypes.shape({
5021
+ useLocalTemplates: PropTypes.bool,
5022
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
5023
+ localTemplatesLoading: PropTypes.bool,
5024
+ localTemplatesLoadingTip: PropTypes.string,
5025
+ localTemplatesFilterContent: PropTypes.node,
5026
+ localTemplatesOnPageChange: PropTypes.func,
5027
+ }),
5026
5028
  };
5027
5029
 
5028
5030
  const mapStateToProps = createStructuredSelector({
@@ -1,5 +1,5 @@
1
1
  import {
2
- call, put, takeLatest, all,
2
+ call, put, takeLatest, takeEvery, all,
3
3
  } from 'redux-saga/effects';
4
4
  import get from 'lodash/get';
5
5
  import { CapNotification } from '@capillarytech/cap-ui-library';
@@ -8,32 +8,69 @@ import * as Api from '../../services/api';
8
8
  import * as types from './constants';
9
9
  import { saveCdnConfigs, removeAllCdnLocalStorageItems } from '../../utils/cdnTransformation';
10
10
  import { COPY_OF } from '../../constants/unified';
11
+ import { fetchSmsTemplatesFromQuery } from './utils/smsTemplatesListApi';
11
12
  import { ZALO_TEMPLATE_INFO_REQUEST } from '../Zalo/constants';
12
13
  import { getTemplateInfoById } from '../Zalo/saga';
13
14
 
14
- // Individual exports for testing
15
- export function* getAllTemplates(channel, queryParams) {
15
+ export function* getLocalSmsTemplates(action) {
16
16
  try {
17
- const result = yield call(Api.getAllTemplates, channel, queryParams);
18
- const channelTemplates = (channel.channel === 'wechat') ? { templates: [...result.response.mapped, ...result.response.richmedia] } : result.response;
19
- // const sidebar = result.response.sidebar;
20
- if (channel.channel === 'wechat' && channel.queryParams && channel.queryParams.sortBy && channel.queryParams.sortBy.toLocaleLowerCase() === ("Most Recent").toLocaleLowerCase()) {
17
+ const fetched = yield call(
18
+ fetchSmsTemplatesFromQuery,
19
+ action.queryParams,
20
+ action.intlCopyOf,
21
+ );
22
+ if (typeof action.onSuccess === 'function') {
23
+ yield call(action.onSuccess, fetched);
24
+ }
25
+ } catch (error) {
26
+ if (typeof action.onFailure === 'function') {
27
+ yield call(action.onFailure, error);
28
+ }
29
+ }
30
+ }
31
+
32
+ export function* getAllTemplates(action) {
33
+ try {
34
+ if (action.channel && String(action.channel).toLowerCase() === 'sms') {
35
+ const fetched = yield call(
36
+ fetchSmsTemplatesFromQuery,
37
+ action.queryParams,
38
+ action.intlCopyOf,
39
+ );
40
+ yield put({
41
+ type: types.GET_ALL_TEMPLATES_SUCCESS,
42
+ data: fetched.channelTemplates,
43
+ weCRMTemplate: fetched.weCRMTemplate,
44
+ isReset: get(action, 'queryParams.page') === 1,
45
+ });
46
+ return;
47
+ }
48
+
49
+ const result = yield call(Api.getAllTemplates, action);
50
+ const channelTemplates = (action.channel === 'wechat')
51
+ ? { templates: [...result.response.mapped, ...result.response.richmedia] }
52
+ : result.response;
53
+ if (action.channel === 'wechat' && action.queryParams && action.queryParams.sortBy && action.queryParams.sortBy.toLocaleLowerCase() === ("Most Recent").toLocaleLowerCase()) {
21
54
  channelTemplates.templates.sort((a, b) => {
22
55
  const dateA = new Date(a.updatedAt);
23
56
  const dateB = new Date(b.updatedAt);
24
57
  return dateB - dateA;
25
58
  });
26
- } else if (channel.channel === 'wechat' && channel.queryParams && channel.queryParams.sortBy && channel.queryParams.sortBy.toLocaleLowerCase() === ("Alphabetically").toLocaleLowerCase()) {
59
+ } else if (action.channel === 'wechat' && action.queryParams && action.queryParams.sortBy && action.queryParams.sortBy.toLocaleLowerCase() === ("Alphabetically").toLocaleLowerCase()) {
27
60
  channelTemplates.templates.sort((a, b) => b.name - a.name);
28
61
  }
29
- // Update the "name" property in each template
30
- if (channel.intlCopyOf && channelTemplates?.templates) {
62
+ if (action.intlCopyOf && channelTemplates?.templates) {
31
63
  channelTemplates.templates = channelTemplates.templates.map((template) => ({
32
64
  ...template,
33
- name: template.name.replace(new RegExp(COPY_OF, 'g'), channel.intlCopyOf),
65
+ name: template.name.replace(new RegExp(COPY_OF, 'g'), action.intlCopyOf),
34
66
  }));
35
67
  }
36
- yield put({ type: types.GET_ALL_TEMPLATES_SUCCESS, data: channelTemplates, weCRMTemplate: result.response.unMapped, isReset: channel.queryParams.page === 1 });
68
+ yield put({
69
+ type: types.GET_ALL_TEMPLATES_SUCCESS,
70
+ data: channelTemplates,
71
+ weCRMTemplate: result.response.unMapped,
72
+ isReset: get(action, 'queryParams.page') === 1,
73
+ });
37
74
  } catch (error) {
38
75
  yield put({ type: types.GET_ALL_TEMPLATES_FAILURE, error });
39
76
  }
@@ -265,6 +302,11 @@ export function* watchGetAllTemplates() {
265
302
  yield takeLatest(types.GET_ALL_TEMPLATES_REQUEST, getAllTemplates);
266
303
  }
267
304
 
305
+
306
+ export function* watchGetLocalSmsTemplates() {
307
+ yield takeEvery(types.GET_LOCAL_SMS_TEMPLATES_REQUEST, getLocalSmsTemplates);
308
+ }
309
+
268
310
  export function* watchDeleteTemplate() {
269
311
  yield takeLatest(types.DELETE_TEMPLATE_REQUEST, deleteTemplate);
270
312
  }
@@ -318,6 +360,7 @@ export function* watchForGetTemplateInfoById() {
318
360
  // All sagas to be loaded
319
361
  export default [
320
362
  watchGetAllTemplates,
363
+ watchGetLocalSmsTemplates,
321
364
  watchDeleteTemplate,
322
365
  watchDeleteRcsTemplate,
323
366
  watchGetUserList,
@@ -334,6 +377,7 @@ export default [
334
377
  export function* v2TemplateSaga() {
335
378
  yield all([
336
379
  watchGetAllTemplates(),
380
+ watchGetLocalSmsTemplates(),
337
381
  watchDeleteTemplate(),
338
382
  watchDeleteRcsTemplate(),
339
383
  watchGetUserList(),
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import React from 'react';
5
+ import { render, screen, fireEvent } from '@testing-library/react';
6
+ import '@testing-library/jest-dom';
7
+ import TemplatesActionBar from '../TemplatesActionBar';
8
+
9
+ jest.mock('@capillarytech/cap-ui-library/CapInput', () => {
10
+ const React = require('react');
11
+ function Search(props) {
12
+ return React.createElement('input', {
13
+ 'data-testid': 'cap-input-search',
14
+ value: props?.value,
15
+ placeholder: props?.placeholder,
16
+ onChange: props?.onChange,
17
+ onKeyDown: (e) => {
18
+ if (e.key === 'Enter' && props.onPressEnter) {
19
+ props.onPressEnter(e);
20
+ }
21
+ },
22
+ });
23
+ }
24
+ function CapInput() {
25
+ return null;
26
+ }
27
+ CapInput.Search = Search;
28
+ return { __esModule: true, default: CapInput };
29
+ });
30
+
31
+ jest.mock('@capillarytech/cap-ui-library/CapButton', () => {
32
+ const React = require('react');
33
+ return function CapButton(props) {
34
+ return React.createElement('button', {
35
+ type: 'button',
36
+ 'data-testid': 'cta',
37
+ onClick: props?.onClick,
38
+ disabled: props?.disabled,
39
+ }, props?.children);
40
+ };
41
+ });
42
+
43
+ describe('TemplatesActionBar', () => {
44
+ it('renders search when searchPlaceholder is set', () => {
45
+ render(
46
+ <TemplatesActionBar
47
+ searchPlaceholder="Find templates"
48
+ searchValue="hi"
49
+ ctaLabel="Create"
50
+ />,
51
+ );
52
+ expect(screen.getByTestId('cap-input-search')).toHaveAttribute('placeholder', 'Find templates');
53
+ });
54
+
55
+ it('omits search when searchPlaceholder is empty', () => {
56
+ const { container } = render(
57
+ <TemplatesActionBar searchPlaceholder="" ctaLabel="Go" />,
58
+ );
59
+ expect(container.querySelector('[data-testid="cap-input-search"]')).toBeNull();
60
+ });
61
+
62
+ it('fires onSearchChange and onCtaClick', () => {
63
+ const onSearchChange = jest.fn();
64
+ const onCtaClick = jest.fn();
65
+ render(
66
+ <TemplatesActionBar
67
+ searchPlaceholder="S"
68
+ onSearchChange={onSearchChange}
69
+ ctaLabel="New"
70
+ onCtaClick={onCtaClick}
71
+ />,
72
+ );
73
+ fireEvent.change(screen.getByTestId('cap-input-search'), { target: { value: 'x' } });
74
+ fireEvent.click(screen.getByTestId('cta'));
75
+ expect(onSearchChange).toHaveBeenCalled();
76
+ expect(onCtaClick).toHaveBeenCalled();
77
+ });
78
+
79
+ it('fires onSearch when Enter is pressed (antd Input has no native onSearch)', () => {
80
+ const onSearch = jest.fn();
81
+ render(
82
+ <TemplatesActionBar
83
+ searchPlaceholder="S"
84
+ searchValue="query"
85
+ onSearch={onSearch}
86
+ ctaLabel="New"
87
+ />,
88
+ );
89
+ const input = screen.getByTestId('cap-input-search');
90
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
91
+ expect(onSearch).toHaveBeenCalledWith('query');
92
+ });
93
+
94
+ it('renders ctaNode instead of default button when provided', () => {
95
+ render(
96
+ <TemplatesActionBar
97
+ searchPlaceholder="S"
98
+ ctaNode={<span data-testid="custom-cta">Custom</span>}
99
+ />,
100
+ );
101
+ expect(screen.getByTestId('custom-cta')).toBeInTheDocument();
102
+ expect(screen.queryByTestId('cta')).toBeNull();
103
+ });
104
+
105
+ it('hides CTA area when showCta is false', () => {
106
+ const { container } = render(
107
+ <TemplatesActionBar searchPlaceholder="S" showCta={false} ctaLabel="X" />,
108
+ );
109
+ expect(container.querySelector('[data-testid="cta"]')).toBeNull();
110
+ });
111
+
112
+ it('renders children in toolbar row', () => {
113
+ render(
114
+ <TemplatesActionBar searchPlaceholder="S" ctaLabel="C">
115
+ <span data-testid="child">extra</span>
116
+ </TemplatesActionBar>,
117
+ );
118
+ expect(screen.getByTestId('child')).toBeInTheDocument();
119
+ });
120
+ });