@capillarytech/creatives-library 9.0.26 → 9.0.29-alpha.1

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 (149) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +260 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +328 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/Email/index.js +3 -54
  96. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/carouselUtils.js +224 -0
  99. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  100. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  101. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  102. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  103. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  104. package/v2Containers/Rcs/constants.js +132 -16
  105. package/v2Containers/Rcs/index.js +1808 -949
  106. package/v2Containers/Rcs/index.scss +443 -8
  107. package/v2Containers/Rcs/messages.js +45 -22
  108. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +484 -0
  109. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  110. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  111. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  112. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  113. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  114. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  115. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  116. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  117. package/v2Containers/Rcs/tests/index.test.js +219 -40
  118. package/v2Containers/Rcs/tests/mockData.js +38 -0
  119. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
  120. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  121. package/v2Containers/Rcs/utils.js +514 -12
  122. package/v2Containers/Sms/Create/index.js +115 -48
  123. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  124. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  125. package/v2Containers/SmsTrai/Create/index.js +9 -4
  126. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  127. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  128. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  129. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  130. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  131. package/v2Containers/SmsWrapper/index.js +37 -8
  132. package/v2Containers/TagList/index.js +5 -0
  133. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  134. package/v2Containers/Templates/_templates.scss +196 -12
  135. package/v2Containers/Templates/actions.js +11 -0
  136. package/v2Containers/Templates/constants.js +2 -0
  137. package/v2Containers/Templates/index.js +131 -59
  138. package/v2Containers/Templates/sagas.js +57 -13
  139. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  140. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  141. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  142. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  143. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  144. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  145. package/v2Containers/TemplatesV2/index.js +88 -25
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  147. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  148. package/v2Containers/Whatsapp/index.js +3 -20
  149. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -126,7 +126,7 @@ import { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } from '../InApp/const
126
126
  import { ZALO_STATUS_OPTIONS, ZALO_STATUSES } from '../Zalo/constants';
127
127
  import { getWhatsappContent, getWhatsappStatus, getWhatsappCategory, getWhatsappCta, getWhatsappQuickReply, getWhatsappAutoFill, getWhatsappCarouselButtonView, getWhatsappDocPreview } from '../Whatsapp/utils';
128
128
  import { getRCSContent } from '../Rcs/utils';
129
- import {RCS_STATUSES} from '../Rcs/constants';
129
+ import { RCS_STATUSES, HOST_INFOBIP } from '../Rcs/constants';
130
130
  import zaloMessages from '../Zalo/messages';
131
131
  import rcsMessages from '../Rcs/messages';
132
132
  import inAppMessages from '../InApp/messages';
@@ -432,8 +432,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
432
432
  channel = '';
433
433
  }
434
434
  this.setState({ channel, activeMode });
435
- // Always reset archive mode and selection when mounting a new channel
436
- this.props.actions.setArchivedMode(false);
435
+ // Always reset archive mode and selection when mounting a new channel.
436
+ // Skip for local-template instances (e.g. SmsFallback selector) — SET_ARCHIVED_MODE clears
437
+ // Templates.templates in the reducer, which would wipe the background RCS template list.
438
+ const useLocalTemplatesOnMount = get(this.props, 'localTemplatesConfig.useLocalTemplates', false);
439
+ if (!useLocalTemplatesOnMount) {
440
+ this.props.actions.setArchivedMode(false);
441
+ }
437
442
  this.props.actions.clearTemplateSelection();
438
443
  // Clear templates when entering account selection mode to prevent showing old channel templates
439
444
  if (activeMode === ACCOUNT_SELECTION_MODE) {
@@ -466,7 +471,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
466
471
  if (this.props.location.query.type === 'embedded') {
467
472
  this.props.actions.resetAccount();
468
473
  }
469
- if (['line', VIBER_CHANNEL, FACEBOOK_CHANNEL, 'sms', 'email', 'ebill'].includes((this.state.channel || '').toLowerCase())) {
474
+ // When using local templates (e.g. SMS fallback selector), do not fetch from API or we overwrite global store and break background RCS list
475
+ const useLocalTemplates = get(
476
+ this.props,
477
+ 'localTemplatesConfig.useLocalTemplates',
478
+ get(this.props, 'useLocalTemplates', false),
479
+ );
480
+ if (!useLocalTemplates && [LINE_LOWERCASE, VIBER_CHANNEL, FACEBOOK_CHANNEL, SMS_LOWERCASE, EMAIL_LOWERCASE, EBILL_LOWERCASE].includes((this.state.channel || '').toLowerCase())) {
470
481
  const queryParams = {
471
482
  // name: this.state.searchText,
472
483
  // sortBy: this.state.sortBy,
@@ -549,7 +560,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
549
560
  selectedTemplateForPreview: null,
550
561
  testAndPreviewContent: null,
551
562
  }, () => {
552
- if (['line', VIBER_CHANNEL, FACEBOOK_CHANNEL, 'sms', 'email', 'ebill', RCS_LOWERCASE].includes(this.state.channel.toLowerCase()) || (this.state.channel.toLowerCase() === 'wechat' && !isEmpty(nextProps.Templates.selectedWeChatAccount))) {
563
+ if ([LINE_LOWERCASE, VIBER_CHANNEL, FACEBOOK_CHANNEL, SMS_LOWERCASE, EMAIL_LOWERCASE, EBILL_LOWERCASE, RCS_LOWERCASE].includes(this.state.channel.toLowerCase()) || (this.state.channel.toLowerCase() === 'wechat' && !isEmpty(nextProps.Templates.selectedWeChatAccount))) {
553
564
  if (this.state.channel.toLowerCase() === 'wechat' && !isEmpty(nextProps.Templates.selectedWeChatAccount)) {
554
565
  params.wecrmId = (nextProps.Templates.selectedWeChatAccount.configs || {}).wecrm_app_id;
555
566
  params.wecrmToken = (nextProps.Templates.selectedWeChatAccount.configs || {}).wecrm_token;
@@ -1012,8 +1023,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1012
1023
 
1013
1024
  componentWillUnmount() {
1014
1025
  window.removeEventListener("message", this.handleFrameTasks);
1015
- this.props.actions.resetTemplateStoreData();
1016
- this.props.globalActions.clearMetaEntities();
1026
+ // When using local templates (e.g. SMS fallback selector), do not clear global store or background RCS list is wiped
1027
+ const useLocalTemplates = get(
1028
+ this.props,
1029
+ 'localTemplatesConfig.useLocalTemplates',
1030
+ get(this.props, 'useLocalTemplates', false),
1031
+ );
1032
+ if (!useLocalTemplates) {
1033
+ this.props.actions.resetTemplateStoreData();
1034
+ this.props.globalActions.clearMetaEntities();
1035
+ }
1017
1036
  // Clear any pending timeouts to prevent memory leaks
1018
1037
  if (this._clearEditTimeout) {
1019
1038
  clearTimeout(this._clearEditTimeout);
@@ -1870,12 +1889,20 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1870
1889
  }
1871
1890
 
1872
1891
  filterRcsTemplates = (templates) => {
1873
- let { selectedRcsStatus } = this.state;
1874
- selectedRcsStatus = !this.props.isFullMode ? RCS_STATUSES.approved : '';
1875
- if (selectedRcsStatus) {
1876
- return templates?.filter((template) => template?.versions?.base?.content?.RCS?.rcsContent?.cardContent?.[0]?.Status === selectedRcsStatus);
1892
+ const selectedRcsAccountName = this.props?.Templates?.selectedRcsAccount?.name || '';
1893
+ const hostName = this.state?.hostName;
1894
+ let nextTemplates = templates || [];
1895
+ if (selectedRcsAccountName) {
1896
+ nextTemplates = nextTemplates.filter(
1897
+ (template) => get(template, 'versions.base.content.RCS.rcsContent.accountName', '') === selectedRcsAccountName
1898
+ );
1877
1899
  }
1878
- return templates;
1900
+ if (!this.props.isFullMode && hostName !== HOST_INFOBIP) {
1901
+ return nextTemplates.filter(
1902
+ (template) => get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', 'unavailable') === RCS_STATUSES.approved
1903
+ );
1904
+ }
1905
+ return nextTemplates;
1879
1906
  }
1880
1907
 
1881
1908
  filterZaloTemplates = (templates) => {
@@ -2065,7 +2092,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2065
2092
  style={{ marginRight: "16px" }}
2066
2093
  type="eye"
2067
2094
  onClick={() => {
2068
- if (!this.props.isFullMode || this.props.isDltFromRcs) {
2095
+ if (!this.props.isFullMode || this.props.isDltFromRcs || this.props.isSmsFallbackFromRcs) {
2069
2096
  if (!get(template, "versions.base.content.zalo.previewUrl", "")) {
2070
2097
  this.setState({ zaloPreviewItemId: template?._id });
2071
2098
  }
@@ -2545,17 +2572,20 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2545
2572
  const name = get(template, "name", "");
2546
2573
  const statusDisplay=getRcsStatusType(status);
2547
2574
  templateData.title = (
2548
- <CapRow type='flex' align="middle" justify="start" gap={12}>
2549
- {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2550
- <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2551
- <CapColumn type="flex" align="middle" className="rcs-status-container zalo-status-color">
2575
+ <CapColumn>
2576
+ <CapRow type='flex' align="middle" justify="start" gap={12}>
2577
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2578
+ <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2579
+ </CapRow>
2580
+ {this.state.hostName !== HOST_INFOBIP && <CapColumn type="flex" align="middle" className="rcs-status-container zalo-status-color">
2552
2581
  <CapStatus
2553
2582
  type={statusDisplay}
2554
- text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2555
- labelType="label3"
2556
- />
2557
- </CapColumn>
2558
- </CapRow>
2583
+ text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2584
+ labelType="label3"
2585
+ />
2586
+ </CapColumn>
2587
+ }
2588
+ </CapColumn>
2559
2589
  );
2560
2590
 
2561
2591
  templateData.content = getRCSContent(template);
@@ -3131,6 +3161,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3131
3161
  let routeParams = {};
3132
3162
  const {fbAdManager} = this.props;
3133
3163
  const isLibraryMode = this.isEnabledInLibraryModule("callCreateFromProps");
3164
+
3134
3165
  if (!isLibraryMode) {
3135
3166
  timeTracker.startTimer(CHANNEL_CREATE_TRACK_MAPPING[channel]);
3136
3167
  }
@@ -3480,6 +3511,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3480
3511
  this.setState({modeType});
3481
3512
  }
3482
3513
  const { _id: id } = template;
3514
+ const {
3515
+ localTemplatesConfig,
3516
+ fbAdManager,
3517
+ isDltFromRcs,
3518
+ isSmsFallbackFromRcs,
3519
+ onSelectTemplate,
3520
+ } = this.props;
3483
3521
  const type = this.props.location.query.type;
3484
3522
  const module = this.props.location.query.module;
3485
3523
  const isLanguageSupport = (this.props.location.query.isLanguageSupport) ? this.props.location.query.isLanguageSupport : false;
@@ -3565,10 +3603,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3565
3603
  }
3566
3604
  if (this.isEnabledInLibraryModule("callSelectFromProps")) {
3567
3605
  let data = id;
3568
- if (this.props.fbAdManager || this.props.isDltFromRcs) {
3606
+ if (localTemplatesConfig?.useLocalTemplates) {
3607
+ data = template;
3608
+ } else if (fbAdManager || isDltFromRcs || isSmsFallbackFromRcs) {
3569
3609
  data = this.selectTemplate(id);
3570
3610
  }
3571
- this.props.onSelectTemplate(data, this.props.fbAdManager);
3611
+ onSelectTemplate(data, fbAdManager);
3572
3612
  } else {
3573
3613
  timeTracker.startTimer(CHANNEL_EDIT_TRACK_MAPPING[this.state.channel.toLowerCase()]);
3574
3614
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4546,9 +4586,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4546
4586
  const isWechatEmbedded = !this.props.isFullMode && channel.toUpperCase() === WECHAT;
4547
4587
  const channelLowerCase = (channel || '').toLowerCase();
4548
4588
  const isTraiDltFeature = this.checkDLTfeatureEnable();
4549
-
4550
4589
  const createButton =
4551
- ( (channelLowerCase === WHATSAPP_LOWERCASE || channelLowerCase === RCS_LOWERCASE) && !this.props.isFullMode )
4590
+ (
4591
+ [WHATSAPP_LOWERCASE, RCS_LOWERCASE].includes(channelLowerCase)
4592
+ && !this.props.isFullMode
4593
+ )
4552
4594
  ? (
4553
4595
  <CapLink
4554
4596
  onClick={this.openCreativesFullMode}
@@ -4580,17 +4622,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4580
4622
  const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4581
4623
  const _renderHasSelection = _isArchivalEnabled && this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4582
4624
 
4583
- const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4584
- {isfilterContentVisisble && <CapInput.Search
4585
- className="search-text"
4586
- style={{width: '210px'}}
4587
- placeholder={_isArchivedMode ? this.props.intl.formatMessage(messages.searchArchivedTemplates) : this.props.intl.formatMessage(messages.searchText)}
4588
- value={this.state.searchText}
4589
- onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4590
- disabled={this.checkSearchDisabled()}
4591
- onClear={() => this.searchTemplate('', this.state.channel)}
4592
- onScroll={(e) => e.stopPropagation()}
4593
- />}
4625
+ const useLocalTemplates = this.props.localTemplatesConfig?.useLocalTemplates;
4626
+ const builtFilterContent = ((isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && (
4627
+ <div className="action-container">
4628
+ <div className="action-container__toolbar-row">
4629
+ {isfilterContentVisisble ? (
4630
+ <CapInput.Search
4631
+ className="search-text"
4632
+ placeholder={this.props.intl.formatMessage(messages.searchText)}
4633
+ value={this.state.searchText}
4634
+ onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4635
+ onSearch={() => this.searchTemplate(this.state.searchText, this.state.channel)}
4636
+ onClear={() => this.searchTemplate('', this.state.channel)}
4637
+ onScroll={(e) => e.stopPropagation()}
4638
+ disabled={this.checkSearchDisabled()}
4639
+ />
4640
+ ) : null}
4594
4641
  {
4595
4642
  channel.toUpperCase() === WECHAT && <CapRadio.CapRadioGroup className="wechat-filters" defaultValue={wechatFilter} onChange={this.setWechatFilter}>
4596
4643
  <CapRadio.Button value={WECHAT_FILTERS.ALL}><CapLabel type="label2">
@@ -4737,16 +4784,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4737
4784
  )
4738
4785
  }
4739
4786
  <div className="template-listing-header-actions">
4740
- {!_isArchivedMode && !_renderHasSelection && (
4741
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4742
- <CapTooltip title={whatsappCountExceedText}>
4743
- <div className="button-disabled-tooltip-wrapper">
4744
- {createButton}
4745
- </div>
4746
- </CapTooltip>
4747
- )
4748
- : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4749
- )}
4750
4787
  {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active, archive flag enabled */}
4751
4788
  {commonUtil.hasCreativesArchivalEnabled() && !_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4752
4789
  <CapDropdown
@@ -4769,7 +4806,28 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4769
4806
  </CapDropdown>
4770
4807
  )}
4771
4808
  </div>
4772
- </div>);
4809
+ </div>
4810
+ <div>
4811
+ <div className="action-container__create-row">
4812
+ {
4813
+ isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && !this.props.isSmsFallbackFromRcs && (
4814
+ this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && isWhatsappCountExeeded ? (
4815
+ <CapTooltip title={whatsappCountExceedText}>
4816
+ <div className="button-disabled-tooltip-wrapper">
4817
+ {createButton}
4818
+ </div>
4819
+ </CapTooltip>
4820
+ ) : createButton
4821
+ )
4822
+ }
4823
+ </div>
4824
+ </div>
4825
+ </div>
4826
+ ));
4827
+ const localTemplatesFilterContent = get(this.props, 'localTemplatesConfig.localTemplatesFilterContent', null);
4828
+ const filterContent = (useLocalTemplates && localTemplatesFilterContent) != null
4829
+ ? localTemplatesFilterContent
4830
+ : builtFilterContent;
4773
4831
  let htmlPreviewContent = "";
4774
4832
  if (this.state.channel.toLowerCase() === 'ebill') {
4775
4833
  htmlPreviewContent = this.state.previewTemplate && this.state.previewTemplate.versions && this.state.previewTemplate.versions.base && this.state.previewTemplate.versions.base['ebill-editor'];
@@ -4779,7 +4837,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4779
4837
 
4780
4838
 
4781
4839
  const creativesParams = this.getCreativesParams();
4782
- const templates = this.props.TemplatesList || [];
4840
+ const templates = useLocalTemplates
4841
+ ? (this.props.localTemplatesConfig?.localTemplates || [])
4842
+ : (this.props.TemplatesList || []);
4843
+ const isLoadingWhenLocal = useLocalTemplates && !!this.props.localTemplatesConfig?.localTemplatesLoading;
4783
4844
  const {route} = this.props;
4784
4845
  const loadingTipMap = {
4785
4846
  sendingFile: 'uploadingFile',
@@ -4794,9 +4855,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4794
4855
  (deleteRcsTemplateInProgress && 'deletingTemplate') ||
4795
4856
  (this.props.EmailCreate.duplicateTemplateInProgress && 'duplicatingTemplate');
4796
4857
 
4797
- const loadingTip = messages[loadingTipIntl] ? this.props.intl.formatMessage(messages[loadingTipIntl]) : this.props.intl.formatMessage(messages.gettingAllTemplates);
4858
+ const loadingTip = useLocalTemplates && this.props.localTemplatesConfig?.localTemplatesLoadingTip
4859
+ ? this.props.localTemplatesConfig.localTemplatesLoadingTip
4860
+ : (messages[loadingTipIntl] ? this.props.intl.formatMessage(messages[loadingTipIntl]) : this.props.intl.formatMessage(messages.gettingAllTemplates));
4798
4861
  const showNoTemplatesFoundZalo = this.state.channel.toUpperCase() === ZALO && isEmpty(this.state.searchedZaloTemplates) && this.state.searchingZaloTemplate;
4799
- const showNoTemplatesFoundOther = ![ZALO].includes(this.state.channel.toUpperCase()) && isEmpty(this.props.TemplatesList) && !this.props.Templates.getAllTemplatesInProgress && !isEmpty(this.state.searchText);
4862
+ const showNoTemplatesFoundOther = ![ZALO].includes(this.state.channel.toUpperCase()) && isEmpty(templates) && (useLocalTemplates ? !isLoadingWhenLocal : !this.props.Templates.getAllTemplatesInProgress) && (useLocalTemplates || !isEmpty(this.state.searchText));
4800
4863
  const showNoTemplatesFound = showNoTemplatesFoundZalo || showNoTemplatesFoundOther;
4801
4864
 
4802
4865
  return (
@@ -4840,7 +4903,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4840
4903
  />
4841
4904
  ) : null}
4842
4905
 
4843
- {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode ? (
4906
+ {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode && this.state?.hostName !== HOST_INFOBIP ? (
4844
4907
  <CapInfoNote
4845
4908
  message={formatMessage(messages.rcsOnlyApprovedTemplates)}
4846
4909
  />
@@ -4853,22 +4916,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4853
4916
  ) : null}
4854
4917
  <CapRow useLegacy>
4855
4918
  <Pagination
4856
- templateInProgress={
4857
- this.props.Templates.getAllTemplatesInProgress
4858
- }
4919
+ templateInProgress={useLocalTemplates ? isLoadingWhenLocal : this.props.Templates.getAllTemplatesInProgress}
4859
4920
  onPageChange={
4860
- templates.length ? this.onPaginationChange : () => {}
4921
+ templates.length
4922
+ ? (useLocalTemplates ? (this.props.localTemplatesConfig?.localTemplatesOnPageChange || (() => {})) : this.onPaginationChange)
4923
+ : () => {}
4861
4924
  }
4862
4925
  >
4863
4926
  {this.getTemplateDataForGrid({
4864
4927
  previewTemplateId: this.state.zaloPreviewItemId,
4865
- isLoading,
4866
- isInitialLoading,
4928
+ isLoading: useLocalTemplates ? isLoadingWhenLocal : isLoading,
4929
+ isInitialLoading: useLocalTemplates ? isLoadingWhenLocal && templates.length === 0 : isInitialLoading,
4867
4930
  loadingTip,
4868
4931
  channel: this.state.channel,
4869
4932
  templates: this.state.searchingZaloTemplate
4870
4933
  ? this.state.searchedZaloTemplates
4871
- : this.props.TemplatesList,
4934
+ : templates,
4872
4935
  filterContent,
4873
4936
  handlers: {
4874
4937
  handlePreviewClick: this.handlePreviewClick,
@@ -5052,6 +5115,15 @@ Templates.propTypes = {
5052
5115
  WebPush: PropTypes.object,
5053
5116
  smsRegister: PropTypes.any,
5054
5117
  isDltFromRcs: PropTypes.bool,
5118
+ isSmsFallbackFromRcs: PropTypes.bool,
5119
+ localTemplatesConfig: PropTypes.shape({
5120
+ useLocalTemplates: PropTypes.bool,
5121
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
5122
+ localTemplatesLoading: PropTypes.bool,
5123
+ localTemplatesLoadingTip: PropTypes.string,
5124
+ localTemplatesFilterContent: PropTypes.node,
5125
+ localTemplatesOnPageChange: PropTypes.func,
5126
+ }),
5055
5127
  };
5056
5128
 
5057
5129
  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';
@@ -7,33 +7,70 @@ import { CapNotification } from '@capillarytech/cap-ui-library';
7
7
  import * as Api from '../../services/api';
8
8
  import * as types from './constants';
9
9
  import { saveCdnConfigs, removeAllCdnLocalStorageItems, initCdnConfigFromEnv } from '../../utils/cdnTransformation';
10
- import { COPY_OF } from '../../constants/unified';
10
+ import { COPY_OF, SMS as SMS_CHANNEL } 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_CHANNEL) {
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
  }
@@ -270,6 +307,11 @@ export function* watchGetAllTemplates() {
270
307
  yield takeLatest(types.GET_ALL_TEMPLATES_REQUEST, getAllTemplates);
271
308
  }
272
309
 
310
+
311
+ export function* watchGetLocalSmsTemplates() {
312
+ yield takeEvery(types.GET_LOCAL_SMS_TEMPLATES_REQUEST, getLocalSmsTemplates);
313
+ }
314
+
273
315
  export function* watchDeleteTemplate() {
274
316
  yield takeLatest(types.DELETE_TEMPLATE_REQUEST, deleteTemplate);
275
317
  }
@@ -323,6 +365,7 @@ export function* watchForGetTemplateInfoById() {
323
365
  // All sagas to be loaded
324
366
  export default [
325
367
  watchGetAllTemplates,
368
+ watchGetLocalSmsTemplates,
326
369
  watchDeleteTemplate,
327
370
  watchDeleteRcsTemplate,
328
371
  watchGetUserList,
@@ -339,6 +382,7 @@ export default [
339
382
  export function* v2TemplateSaga() {
340
383
  yield all([
341
384
  watchGetAllTemplates(),
385
+ watchGetLocalSmsTemplates(),
342
386
  watchDeleteTemplate(),
343
387
  watchDeleteRcsTemplate(),
344
388
  watchGetUserList(),
@@ -0,0 +1,129 @@
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
+ onScroll: props?.onScroll,
23
+ });
24
+ }
25
+ function CapInput() {
26
+ return null;
27
+ }
28
+ CapInput.Search = Search;
29
+ return { __esModule: true, default: CapInput };
30
+ });
31
+
32
+ jest.mock('@capillarytech/cap-ui-library/CapButton', () => {
33
+ const React = require('react');
34
+ return function CapButton(props) {
35
+ return React.createElement('button', {
36
+ type: 'button',
37
+ 'data-testid': 'cta',
38
+ onClick: props?.onClick,
39
+ disabled: props?.disabled,
40
+ }, props?.children);
41
+ };
42
+ });
43
+
44
+ describe('TemplatesActionBar', () => {
45
+ it('renders search when searchPlaceholder is set', () => {
46
+ render(
47
+ <TemplatesActionBar
48
+ searchPlaceholder="Find templates"
49
+ searchValue="hi"
50
+ ctaLabel="Create"
51
+ />,
52
+ );
53
+ expect(screen.getByTestId('cap-input-search')).toHaveAttribute('placeholder', 'Find templates');
54
+ });
55
+
56
+ it('omits search when searchPlaceholder is empty', () => {
57
+ const { container } = render(
58
+ <TemplatesActionBar searchPlaceholder="" ctaLabel="Go" />,
59
+ );
60
+ expect(container.querySelector('[data-testid="cap-input-search"]')).toBeNull();
61
+ });
62
+
63
+ it('fires onSearchChange and onCtaClick', () => {
64
+ const onSearchChange = jest.fn();
65
+ const onCtaClick = jest.fn();
66
+ render(
67
+ <TemplatesActionBar
68
+ searchPlaceholder="S"
69
+ onSearchChange={onSearchChange}
70
+ ctaLabel="New"
71
+ onCtaClick={onCtaClick}
72
+ />,
73
+ );
74
+ fireEvent.change(screen.getByTestId('cap-input-search'), { target: { value: 'x' } });
75
+ fireEvent.click(screen.getByTestId('cta'));
76
+ expect(onSearchChange).toHaveBeenCalled();
77
+ expect(onCtaClick).toHaveBeenCalled();
78
+ });
79
+
80
+ it('stops propagation on scroll within the search input', () => {
81
+ render(<TemplatesActionBar searchPlaceholder="S" ctaLabel="New" />);
82
+ const scrollEvent = new Event('scroll', { bubbles: true });
83
+ const stopPropagationSpy = jest.spyOn(scrollEvent, 'stopPropagation');
84
+ screen.getByTestId('cap-input-search').dispatchEvent(scrollEvent);
85
+ expect(stopPropagationSpy).toHaveBeenCalled();
86
+ });
87
+
88
+ it('fires onSearch when Enter is pressed (antd Input has no native onSearch)', () => {
89
+ const onSearch = jest.fn();
90
+ render(
91
+ <TemplatesActionBar
92
+ searchPlaceholder="S"
93
+ searchValue="query"
94
+ onSearch={onSearch}
95
+ ctaLabel="New"
96
+ />,
97
+ );
98
+ const input = screen.getByTestId('cap-input-search');
99
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
100
+ expect(onSearch).toHaveBeenCalledWith('query');
101
+ });
102
+
103
+ it('renders ctaNode instead of default button when provided', () => {
104
+ render(
105
+ <TemplatesActionBar
106
+ searchPlaceholder="S"
107
+ ctaNode={<span data-testid="custom-cta">Custom</span>}
108
+ />,
109
+ );
110
+ expect(screen.getByTestId('custom-cta')).toBeInTheDocument();
111
+ expect(screen.queryByTestId('cta')).toBeNull();
112
+ });
113
+
114
+ it('hides CTA area when showCta is false', () => {
115
+ const { container } = render(
116
+ <TemplatesActionBar searchPlaceholder="S" showCta={false} ctaLabel="X" />,
117
+ );
118
+ expect(container.querySelector('[data-testid="cta"]')).toBeNull();
119
+ });
120
+
121
+ it('renders children in toolbar row', () => {
122
+ render(
123
+ <TemplatesActionBar searchPlaceholder="S" ctaLabel="C">
124
+ <span data-testid="child">extra</span>
125
+ </TemplatesActionBar>,
126
+ );
127
+ expect(screen.getByTestId('child')).toBeInTheDocument();
128
+ });
129
+ });