@capillarytech/creatives-library 8.0.358 → 8.0.359-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 (125) 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/CapImageUpload/index.js +2 -2
  15. package/v2Components/CapTagList/index.js +10 -0
  16. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  23. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -76
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +150 -4
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  29. package/v2Components/CommonTestAndPreview/index.js +810 -222
  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/RcsPreviewContent.test.js +281 -283
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  41. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  42. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  43. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  44. package/v2Components/FormBuilder/index.js +5 -4
  45. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  46. package/v2Components/SmsFallback/constants.js +73 -0
  47. package/v2Components/SmsFallback/index.js +956 -0
  48. package/v2Components/SmsFallback/index.scss +265 -0
  49. package/v2Components/SmsFallback/messages.js +78 -0
  50. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  51. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  52. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  56. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  57. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  58. package/v2Components/TemplatePreview/_templatePreview.scss +37 -22
  59. package/v2Components/TemplatePreview/constants.js +2 -0
  60. package/v2Components/TemplatePreview/index.js +143 -31
  61. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  62. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  63. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  64. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  65. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  66. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  67. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  68. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  69. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  70. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  71. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  72. package/v2Containers/CreativesContainer/constants.js +9 -0
  73. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  74. package/v2Containers/CreativesContainer/index.js +322 -103
  75. package/v2Containers/CreativesContainer/index.scss +83 -1
  76. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  77. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  78. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  81. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  82. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  83. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  84. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  85. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  86. package/v2Containers/Rcs/constants.js +120 -11
  87. package/v2Containers/Rcs/index.js +2577 -812
  88. package/v2Containers/Rcs/index.scss +281 -8
  89. package/v2Containers/Rcs/messages.js +34 -3
  90. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  91. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98036 -70145
  92. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  93. package/v2Containers/Rcs/tests/index.test.js +152 -121
  94. package/v2Containers/Rcs/tests/mockData.js +38 -0
  95. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  96. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  97. package/v2Containers/Rcs/utils.js +478 -11
  98. package/v2Containers/Sms/Create/index.js +106 -40
  99. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  100. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  101. package/v2Containers/SmsTrai/Create/index.js +9 -4
  102. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  103. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  104. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  105. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  106. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  107. package/v2Containers/SmsWrapper/index.js +37 -8
  108. package/v2Containers/TagList/index.js +6 -0
  109. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  110. package/v2Containers/Templates/_templates.scss +166 -9
  111. package/v2Containers/Templates/actions.js +11 -0
  112. package/v2Containers/Templates/constants.js +2 -0
  113. package/v2Containers/Templates/index.js +121 -53
  114. package/v2Containers/Templates/sagas.js +56 -12
  115. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  116. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  117. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  118. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  119. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  120. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  121. package/v2Containers/TemplatesV2/index.js +86 -23
  122. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  123. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  124. package/v2Containers/Whatsapp/index.js +3 -20
  125. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -125,7 +125,7 @@ import { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } from '../InApp/const
125
125
  import { ZALO_STATUS_OPTIONS, ZALO_STATUSES } from '../Zalo/constants';
126
126
  import { getWhatsappContent, getWhatsappStatus, getWhatsappCategory, getWhatsappCta, getWhatsappQuickReply, getWhatsappAutoFill, getWhatsappCarouselButtonView } from '../Whatsapp/utils';
127
127
  import { getRCSContent } from '../Rcs/utils';
128
- import {RCS_STATUSES} from '../Rcs/constants';
128
+ import { RCS_STATUSES, HOST_INFOBIP } from '../Rcs/constants';
129
129
  import zaloMessages from '../Zalo/messages';
130
130
  import rcsMessages from '../Rcs/messages';
131
131
  import inAppMessages from '../InApp/messages';
@@ -463,7 +463,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
463
463
  if (this.props.location.query.type === 'embedded') {
464
464
  this.props.actions.resetAccount();
465
465
  }
466
- if (['line', VIBER_CHANNEL, FACEBOOK_CHANNEL, 'sms', 'email', 'ebill'].includes((this.state.channel || '').toLowerCase())) {
466
+ // When using local templates (e.g. SMS fallback selector), do not fetch from API or we overwrite global store and break background RCS list
467
+ const useLocalTemplates = get(
468
+ this.props,
469
+ 'localTemplatesConfig.useLocalTemplates',
470
+ get(this.props, 'useLocalTemplates', false),
471
+ );
472
+ if (!useLocalTemplates && [LINE_LOWERCASE, VIBER_CHANNEL, FACEBOOK_CHANNEL, SMS_LOWERCASE, EMAIL_LOWERCASE, EBILL_LOWERCASE].includes((this.state.channel || '').toLowerCase())) {
467
473
  const queryParams = {
468
474
  // name: this.state.searchText,
469
475
  // sortBy: this.state.sortBy,
@@ -546,7 +552,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
546
552
  selectedTemplateForPreview: null,
547
553
  testAndPreviewContent: null,
548
554
  }, () => {
549
- 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))) {
555
+ 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))) {
550
556
  if (this.state.channel.toLowerCase() === 'wechat' && !isEmpty(nextProps.Templates.selectedWeChatAccount)) {
551
557
  params.wecrmId = (nextProps.Templates.selectedWeChatAccount.configs || {}).wecrm_app_id;
552
558
  params.wecrmToken = (nextProps.Templates.selectedWeChatAccount.configs || {}).wecrm_token;
@@ -1009,8 +1015,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1009
1015
 
1010
1016
  componentWillUnmount() {
1011
1017
  window.removeEventListener("message", this.handleFrameTasks);
1012
- this.props.actions.resetTemplateStoreData();
1013
- this.props.globalActions.clearMetaEntities();
1018
+ // When using local templates (e.g. SMS fallback selector), do not clear global store or background RCS list is wiped
1019
+ const useLocalTemplates = get(
1020
+ this.props,
1021
+ 'localTemplatesConfig.useLocalTemplates',
1022
+ get(this.props, 'useLocalTemplates', false),
1023
+ );
1024
+ if (!useLocalTemplates) {
1025
+ this.props.actions.resetTemplateStoreData();
1026
+ this.props.globalActions.clearMetaEntities();
1027
+ }
1014
1028
  // Clear any pending timeouts to prevent memory leaks
1015
1029
  if (this._clearEditTimeout) {
1016
1030
  clearTimeout(this._clearEditTimeout);
@@ -1867,12 +1881,20 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1867
1881
  }
1868
1882
 
1869
1883
  filterRcsTemplates = (templates) => {
1870
- let { selectedRcsStatus } = this.state;
1871
- selectedRcsStatus = !this.props.isFullMode ? RCS_STATUSES.approved : '';
1872
- if (selectedRcsStatus) {
1873
- return templates?.filter((template) => template?.versions?.base?.content?.RCS?.rcsContent?.cardContent?.[0]?.Status === selectedRcsStatus);
1884
+ const selectedRcsAccountName = this.props?.Templates?.selectedRcsAccount?.name || '';
1885
+ const hostName = this.state?.hostName;
1886
+ let nextTemplates = templates || [];
1887
+ if (selectedRcsAccountName) {
1888
+ nextTemplates = nextTemplates.filter(
1889
+ (t) => get(t, 'versions.base.content.RCS.rcsContent.accountName', '') === selectedRcsAccountName
1890
+ );
1874
1891
  }
1875
- return templates;
1892
+ if (!this.props.isFullMode && hostName !== HOST_INFOBIP) {
1893
+ return nextTemplates.filter(
1894
+ (t) => get(t, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', 'unavailable') === RCS_STATUSES.approved
1895
+ );
1896
+ }
1897
+ return nextTemplates;
1876
1898
  }
1877
1899
 
1878
1900
  filterZaloTemplates = (templates) => {
@@ -2062,7 +2084,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2062
2084
  style={{ marginRight: "16px" }}
2063
2085
  type="eye"
2064
2086
  onClick={() => {
2065
- if (!this.props.isFullMode || this.props.isDltFromRcs) {
2087
+ if (!this.props.isFullMode || this.props.isDltFromRcs || this.props.isSmsFallbackFromRcs) {
2066
2088
  if (!get(template, "versions.base.content.zalo.previewUrl", "")) {
2067
2089
  this.setState({ zaloPreviewItemId: template?._id });
2068
2090
  }
@@ -2544,13 +2566,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2544
2566
  <CapRow type="flex" align="middle">
2545
2567
  {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2546
2568
  <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2547
- <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2569
+ {this.state.hostName !== HOST_INFOBIP && <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2548
2570
  <CapStatus
2549
2571
  type={statusDisplay}
2550
- text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2551
- labelType="label3"
2552
- />
2553
- </CapRow>
2572
+ text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2573
+ labelType="label3"
2574
+ />
2575
+ </CapRow>
2576
+ }
2554
2577
  </CapRow>
2555
2578
  );
2556
2579
 
@@ -3126,6 +3149,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3126
3149
  let routeParams = {};
3127
3150
  const {fbAdManager} = this.props;
3128
3151
  const isLibraryMode = this.isEnabledInLibraryModule("callCreateFromProps");
3152
+
3129
3153
  if (!isLibraryMode) {
3130
3154
  timeTracker.startTimer(CHANNEL_CREATE_TRACK_MAPPING[channel]);
3131
3155
  }
@@ -3475,6 +3499,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3475
3499
  this.setState({modeType});
3476
3500
  }
3477
3501
  const { _id: id } = template;
3502
+ const {
3503
+ localTemplatesConfig,
3504
+ fbAdManager,
3505
+ isDltFromRcs,
3506
+ isSmsFallbackFromRcs,
3507
+ onSelectTemplate,
3508
+ } = this.props;
3478
3509
  const type = this.props.location.query.type;
3479
3510
  const module = this.props.location.query.module;
3480
3511
  const isLanguageSupport = (this.props.location.query.isLanguageSupport) ? this.props.location.query.isLanguageSupport : false;
@@ -3560,10 +3591,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3560
3591
  }
3561
3592
  if (this.isEnabledInLibraryModule("callSelectFromProps")) {
3562
3593
  let data = id;
3563
- if (this.props.fbAdManager || this.props.isDltFromRcs) {
3594
+ if (localTemplatesConfig?.useLocalTemplates) {
3595
+ data = template;
3596
+ } else if (fbAdManager || isDltFromRcs || isSmsFallbackFromRcs) {
3564
3597
  data = this.selectTemplate(id);
3565
3598
  }
3566
- this.props.onSelectTemplate(data, this.props.fbAdManager);
3599
+ onSelectTemplate(data, fbAdManager);
3567
3600
  } else {
3568
3601
  timeTracker.startTimer(CHANNEL_EDIT_TRACK_MAPPING[this.state.channel.toLowerCase()]);
3569
3602
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4522,9 +4555,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4522
4555
  const isWechatEmbedded = !this.props.isFullMode && channel.toUpperCase() === WECHAT;
4523
4556
  const channelLowerCase = (channel || '').toLowerCase();
4524
4557
  const isTraiDltFeature = this.checkDLTfeatureEnable();
4525
-
4526
4558
  const createButton =
4527
- ( (channelLowerCase === WHATSAPP_LOWERCASE || channelLowerCase === RCS_LOWERCASE) && !this.props.isFullMode )
4559
+ (
4560
+ (
4561
+ channelLowerCase === WHATSAPP_LOWERCASE
4562
+ || channelLowerCase === RCS_LOWERCASE
4563
+ )
4564
+ && !this.props.isFullMode
4565
+ )
4528
4566
  ? (
4529
4567
  <CapLink
4530
4568
  onClick={this.openCreativesFullMode}
@@ -4556,17 +4594,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4556
4594
  const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4557
4595
  const _renderHasSelection = _isArchivalEnabled && this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4558
4596
 
4559
- const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4560
- {isfilterContentVisisble && <CapInput.Search
4561
- className="search-text"
4562
- style={{width: '210px'}}
4563
- placeholder={_isArchivedMode ? this.props.intl.formatMessage(messages.searchArchivedTemplates) : this.props.intl.formatMessage(messages.searchText)}
4564
- value={this.state.searchText}
4565
- onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4566
- disabled={this.checkSearchDisabled()}
4567
- onClear={() => this.searchTemplate('', this.state.channel)}
4568
- onScroll={(e) => e.stopPropagation()}
4569
- />}
4597
+ const useLocalTemplates = this.props.localTemplatesConfig?.useLocalTemplates;
4598
+ const builtFilterContent = ((isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && (
4599
+ <div className="action-container">
4600
+ <div className="action-container__toolbar-row">
4601
+ {isfilterContentVisisble ? (
4602
+ <CapInput.Search
4603
+ className="search-text"
4604
+ placeholder={this.props.intl.formatMessage(messages.searchText)}
4605
+ value={this.state.searchText}
4606
+ onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4607
+ onSearch={() => this.searchTemplate(this.state.searchText, this.state.channel)}
4608
+ onClear={() => this.searchTemplate('', this.state.channel)}
4609
+ onScroll={(e) => e.stopPropagation()}
4610
+ disabled={this.checkSearchDisabled()}
4611
+ />
4612
+ ) : null}
4570
4613
  {
4571
4614
  channel.toUpperCase() === WECHAT && <CapRadio.CapRadioGroup className="wechat-filters" defaultValue={wechatFilter} onChange={this.setWechatFilter}>
4572
4615
  <CapRadio.Button value={WECHAT_FILTERS.ALL}><CapLabel type="label2">
@@ -4713,16 +4756,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4713
4756
  )
4714
4757
  }
4715
4758
  <div className="template-listing-header-actions">
4716
- {!_isArchivedMode && !_renderHasSelection && (
4717
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4718
- <CapTooltip title={whatsappCountExceedText}>
4719
- <div className="button-disabled-tooltip-wrapper">
4720
- {createButton}
4721
- </div>
4722
- </CapTooltip>
4723
- )
4724
- : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4725
- )}
4726
4759
  {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active, archive flag enabled */}
4727
4760
  {commonUtil.hasCreativesArchivalEnabled() && !_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4728
4761
  <CapDropdown
@@ -4745,7 +4778,28 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4745
4778
  </CapDropdown>
4746
4779
  )}
4747
4780
  </div>
4748
- </div>);
4781
+ </div>
4782
+ <div>
4783
+ <div className="action-container__create-row">
4784
+ {
4785
+ isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && !this.props.isSmsFallbackFromRcs && (
4786
+ this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && isWhatsappCountExeeded ? (
4787
+ <CapTooltip title={whatsappCountExceedText}>
4788
+ <div className="button-disabled-tooltip-wrapper">
4789
+ {createButton}
4790
+ </div>
4791
+ </CapTooltip>
4792
+ ) : createButton
4793
+ )
4794
+ }
4795
+ </div>
4796
+ </div>
4797
+ </div>
4798
+ ));
4799
+ const localTemplatesFilterContent = get(this.props, 'localTemplatesConfig.localTemplatesFilterContent', null);
4800
+ const filterContent = (useLocalTemplates && localTemplatesFilterContent) != null
4801
+ ? localTemplatesFilterContent
4802
+ : builtFilterContent;
4749
4803
  let htmlPreviewContent = "";
4750
4804
  if (this.state.channel.toLowerCase() === 'ebill') {
4751
4805
  htmlPreviewContent = this.state.previewTemplate && this.state.previewTemplate.versions && this.state.previewTemplate.versions.base && this.state.previewTemplate.versions.base['ebill-editor'];
@@ -4755,7 +4809,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4755
4809
 
4756
4810
 
4757
4811
  const creativesParams = this.getCreativesParams();
4758
- const templates = this.props.TemplatesList || [];
4812
+ const templates = useLocalTemplates
4813
+ ? (this.props.localTemplatesConfig?.localTemplates || [])
4814
+ : (this.props.TemplatesList || []);
4815
+ const isLoadingWhenLocal = useLocalTemplates && !!this.props.localTemplatesConfig?.localTemplatesLoading;
4759
4816
  const {route} = this.props;
4760
4817
  const loadingTipMap = {
4761
4818
  sendingFile: 'uploadingFile',
@@ -4770,9 +4827,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4770
4827
  (deleteRcsTemplateInProgress && 'deletingTemplate') ||
4771
4828
  (this.props.EmailCreate.duplicateTemplateInProgress && 'duplicatingTemplate');
4772
4829
 
4773
- const loadingTip = messages[loadingTipIntl] ? this.props.intl.formatMessage(messages[loadingTipIntl]) : this.props.intl.formatMessage(messages.gettingAllTemplates);
4830
+ const loadingTip = useLocalTemplates && this.props.localTemplatesConfig?.localTemplatesLoadingTip
4831
+ ? this.props.localTemplatesConfig.localTemplatesLoadingTip
4832
+ : (messages[loadingTipIntl] ? this.props.intl.formatMessage(messages[loadingTipIntl]) : this.props.intl.formatMessage(messages.gettingAllTemplates));
4774
4833
  const showNoTemplatesFoundZalo = this.state.channel.toUpperCase() === ZALO && isEmpty(this.state.searchedZaloTemplates) && this.state.searchingZaloTemplate;
4775
- const showNoTemplatesFoundOther = ![ZALO].includes(this.state.channel.toUpperCase()) && isEmpty(this.props.TemplatesList) && !this.props.Templates.getAllTemplatesInProgress && !isEmpty(this.state.searchText);
4834
+ const showNoTemplatesFoundOther = ![ZALO].includes(this.state.channel.toUpperCase()) && isEmpty(templates) && (useLocalTemplates ? !isLoadingWhenLocal : !this.props.Templates.getAllTemplatesInProgress) && (useLocalTemplates || !isEmpty(this.state.searchText));
4776
4835
  const showNoTemplatesFound = showNoTemplatesFoundZalo || showNoTemplatesFoundOther;
4777
4836
 
4778
4837
  return (
@@ -4816,7 +4875,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4816
4875
  />
4817
4876
  ) : null}
4818
4877
 
4819
- {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode ? (
4878
+ {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode && this.state?.hostName !== HOST_INFOBIP ? (
4820
4879
  <CapInfoNote
4821
4880
  message={formatMessage(messages.rcsOnlyApprovedTemplates)}
4822
4881
  />
@@ -4829,22 +4888,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4829
4888
  ) : null}
4830
4889
  <CapRow>
4831
4890
  <Pagination
4832
- templateInProgress={
4833
- this.props.Templates.getAllTemplatesInProgress
4834
- }
4891
+ templateInProgress={useLocalTemplates ? isLoadingWhenLocal : this.props.Templates.getAllTemplatesInProgress}
4835
4892
  onPageChange={
4836
- templates.length ? this.onPaginationChange : () => {}
4893
+ templates.length
4894
+ ? (useLocalTemplates ? (this.props.localTemplatesConfig?.localTemplatesOnPageChange || (() => {})) : this.onPaginationChange)
4895
+ : () => {}
4837
4896
  }
4838
4897
  >
4839
4898
  {this.getTemplateDataForGrid({
4840
4899
  previewTemplateId: this.state.zaloPreviewItemId,
4841
- isLoading,
4842
- isInitialLoading,
4900
+ isLoading: useLocalTemplates ? isLoadingWhenLocal : isLoading,
4901
+ isInitialLoading: useLocalTemplates ? isLoadingWhenLocal && templates.length === 0 : isInitialLoading,
4843
4902
  loadingTip,
4844
4903
  channel: this.state.channel,
4845
4904
  templates: this.state.searchingZaloTemplate
4846
4905
  ? this.state.searchedZaloTemplates
4847
- : this.props.TemplatesList,
4906
+ : templates,
4848
4907
  filterContent,
4849
4908
  handlers: {
4850
4909
  handlePreviewClick: this.handlePreviewClick,
@@ -5027,6 +5086,15 @@ Templates.propTypes = {
5027
5086
  WebPush: PropTypes.object,
5028
5087
  smsRegister: PropTypes.any,
5029
5088
  isDltFromRcs: PropTypes.bool,
5089
+ isSmsFallbackFromRcs: PropTypes.bool,
5090
+ localTemplatesConfig: PropTypes.shape({
5091
+ useLocalTemplates: PropTypes.bool,
5092
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
5093
+ localTemplatesLoading: PropTypes.bool,
5094
+ localTemplatesLoadingTip: PropTypes.string,
5095
+ localTemplatesFilterContent: PropTypes.node,
5096
+ localTemplatesOnPageChange: PropTypes.func,
5097
+ }),
5030
5098
  };
5031
5099
 
5032
5100
  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, initCdnConfigFromEnv } 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
  }
@@ -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,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
+ });