@capillarytech/creatives-library 8.0.353-alpha.4 → 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 (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/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/RcsPreviewContent.js +157 -15
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -76
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  26. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  27. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  28. package/v2Components/CommonTestAndPreview/index.js +691 -186
  29. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  30. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  31. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  32. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  33. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  36. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  37. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  38. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  40. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  41. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  42. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +36 -26
  43. package/v2Components/FormBuilder/index.js +74 -166
  44. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  45. package/v2Components/SmsFallback/constants.js +73 -0
  46. package/v2Components/SmsFallback/index.js +956 -0
  47. package/v2Components/SmsFallback/index.scss +265 -0
  48. package/v2Components/SmsFallback/messages.js +78 -0
  49. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  50. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  51. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  52. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  55. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  56. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  57. package/v2Components/TemplatePreview/_templatePreview.scss +38 -23
  58. package/v2Components/TemplatePreview/constants.js +2 -0
  59. package/v2Components/TemplatePreview/index.js +143 -31
  60. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  61. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  62. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  63. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  64. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  65. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  66. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  67. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  68. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  69. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  70. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  71. package/v2Containers/CreativesContainer/constants.js +9 -0
  72. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  73. package/v2Containers/CreativesContainer/index.js +346 -163
  74. package/v2Containers/CreativesContainer/index.scss +51 -1
  75. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  77. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  81. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  82. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  83. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  84. package/v2Containers/Email/index.js +1 -1
  85. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  86. package/v2Containers/Rcs/constants.js +119 -10
  87. package/v2Containers/Rcs/index.js +2445 -813
  88. package/v2Containers/Rcs/index.scss +280 -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 +98018 -70073
  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 +120 -52
  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
@@ -122,7 +122,7 @@ import { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } from '../InApp/const
122
122
  import { ZALO_STATUS_OPTIONS, ZALO_STATUSES } from '../Zalo/constants';
123
123
  import { getWhatsappContent, getWhatsappStatus, getWhatsappCategory, getWhatsappCta, getWhatsappQuickReply, getWhatsappAutoFill, getWhatsappCarouselButtonView } from '../Whatsapp/utils';
124
124
  import { getRCSContent } from '../Rcs/utils';
125
- import {RCS_STATUSES} from '../Rcs/constants';
125
+ import { RCS_STATUSES, HOST_INFOBIP } from '../Rcs/constants';
126
126
  import zaloMessages from '../Zalo/messages';
127
127
  import rcsMessages from '../Rcs/messages';
128
128
  import inAppMessages from '../InApp/messages';
@@ -460,7 +460,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
460
460
  if (this.props.location.query.type === 'embedded') {
461
461
  this.props.actions.resetAccount();
462
462
  }
463
- 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())) {
464
470
  const queryParams = {
465
471
  // name: this.state.searchText,
466
472
  // sortBy: this.state.sortBy,
@@ -1006,8 +1012,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1006
1012
 
1007
1013
  componentWillUnmount() {
1008
1014
  window.removeEventListener("message", this.handleFrameTasks);
1009
- this.props.actions.resetTemplateStoreData();
1010
- 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
+ }
1011
1025
  // Clear any pending timeouts to prevent memory leaks
1012
1026
  if (this._clearEditTimeout) {
1013
1027
  clearTimeout(this._clearEditTimeout);
@@ -1797,12 +1811,20 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1797
1811
  }
1798
1812
 
1799
1813
  filterRcsTemplates = (templates) => {
1800
- let { selectedRcsStatus } = this.state;
1801
- selectedRcsStatus = !this.props.isFullMode ? RCS_STATUSES.approved : '';
1802
- if (selectedRcsStatus) {
1803
- 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
+ );
1804
1821
  }
1805
- 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;
1806
1828
  }
1807
1829
 
1808
1830
  filterZaloTemplates = (templates) => {
@@ -1992,7 +2014,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1992
2014
  style={{ marginRight: "16px" }}
1993
2015
  type="eye"
1994
2016
  onClick={() => {
1995
- if (!this.props.isFullMode || this.props.isDltFromRcs) {
2017
+ if (!this.props.isFullMode || this.props.isDltFromRcs || this.props.isSmsFallbackFromRcs) {
1996
2018
  if (!get(template, "versions.base.content.zalo.previewUrl", "")) {
1997
2019
  this.setState({ zaloPreviewItemId: template?._id });
1998
2020
  }
@@ -2474,13 +2496,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2474
2496
  <CapRow type="flex" align="middle">
2475
2497
  {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2476
2498
  <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2477
- <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">
2478
2500
  <CapStatus
2479
2501
  type={statusDisplay}
2480
- text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2481
- labelType="label3"
2482
- />
2483
- </CapRow>
2502
+ text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2503
+ labelType="label3"
2504
+ />
2505
+ </CapRow>
2506
+ }
2484
2507
  </CapRow>
2485
2508
  );
2486
2509
 
@@ -3056,6 +3079,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3056
3079
  let routeParams = {};
3057
3080
  const {fbAdManager} = this.props;
3058
3081
  const isLibraryMode = this.isEnabledInLibraryModule("callCreateFromProps");
3082
+
3059
3083
  if (!isLibraryMode) {
3060
3084
  timeTracker.startTimer(CHANNEL_CREATE_TRACK_MAPPING[channel]);
3061
3085
  }
@@ -3405,6 +3429,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3405
3429
  this.setState({modeType});
3406
3430
  }
3407
3431
  const { _id: id } = template;
3432
+ const {
3433
+ localTemplatesConfig,
3434
+ fbAdManager,
3435
+ isDltFromRcs,
3436
+ isSmsFallbackFromRcs,
3437
+ onSelectTemplate,
3438
+ } = this.props;
3408
3439
  const type = this.props.location.query.type;
3409
3440
  const module = this.props.location.query.module;
3410
3441
  const isLanguageSupport = (this.props.location.query.isLanguageSupport) ? this.props.location.query.isLanguageSupport : false;
@@ -3490,10 +3521,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3490
3521
  }
3491
3522
  if (this.isEnabledInLibraryModule("callSelectFromProps")) {
3492
3523
  let data = id;
3493
- if (this.props.fbAdManager || this.props.isDltFromRcs) {
3524
+ if (localTemplatesConfig?.useLocalTemplates) {
3525
+ data = template;
3526
+ } else if (fbAdManager || isDltFromRcs || isSmsFallbackFromRcs) {
3494
3527
  data = this.selectTemplate(id);
3495
3528
  }
3496
- this.props.onSelectTemplate(data, this.props.fbAdManager);
3529
+ onSelectTemplate(data, fbAdManager);
3497
3530
  } else {
3498
3531
  timeTracker.startTimer(CHANNEL_EDIT_TRACK_MAPPING[this.state.channel.toLowerCase()]);
3499
3532
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4452,9 +4485,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4452
4485
  const isWechatEmbedded = !this.props.isFullMode && channel.toUpperCase() === WECHAT;
4453
4486
  const channelLowerCase = (channel || '').toLowerCase();
4454
4487
  const isTraiDltFeature = this.checkDLTfeatureEnable();
4455
-
4456
4488
  const createButton =
4457
- ( (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
+ )
4458
4496
  ? (
4459
4497
  <CapLink
4460
4498
  onClick={this.openCreativesFullMode}
@@ -4486,17 +4524,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4486
4524
  const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4487
4525
  const _renderHasSelection = _isArchivalEnabled && this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4488
4526
 
4489
- const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4490
- {isfilterContentVisisble && <CapInput.Search
4491
- className="search-text"
4492
- style={{width: '210px'}}
4493
- placeholder={_isArchivedMode ? this.props.intl.formatMessage(messages.searchArchivedTemplates) : this.props.intl.formatMessage(messages.searchText)}
4494
- value={this.state.searchText}
4495
- onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4496
- disabled={this.checkSearchDisabled()}
4497
- onClear={() => this.searchTemplate('', this.state.channel)}
4498
- onScroll={(e) => e.stopPropagation()}
4499
- />}
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}
4500
4543
  {
4501
4544
  channel.toUpperCase() === WECHAT && <CapRadio.CapRadioGroup className="wechat-filters" defaultValue={wechatFilter} onChange={this.setWechatFilter}>
4502
4545
  <CapRadio.Button value={WECHAT_FILTERS.ALL}><CapLabel type="label2">
@@ -4643,16 +4686,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4643
4686
  )
4644
4687
  }
4645
4688
  <div className="template-listing-header-actions">
4646
- {!_isArchivedMode && !_renderHasSelection && (
4647
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4648
- <CapTooltip title={whatsappCountExceedText}>
4649
- <div className="button-disabled-tooltip-wrapper">
4650
- {createButton}
4651
- </div>
4652
- </CapTooltip>
4653
- )
4654
- : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4655
- )}
4656
4689
  {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active, archive flag enabled */}
4657
4690
  {commonUtil.hasCreativesArchivalEnabled() && !_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4658
4691
  <CapDropdown
@@ -4675,7 +4708,28 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4675
4708
  </CapDropdown>
4676
4709
  )}
4677
4710
  </div>
4678
- </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;
4679
4733
  let htmlPreviewContent = "";
4680
4734
  if (this.state.channel.toLowerCase() === 'ebill') {
4681
4735
  htmlPreviewContent = this.state.previewTemplate && this.state.previewTemplate.versions && this.state.previewTemplate.versions.base && this.state.previewTemplate.versions.base['ebill-editor'];
@@ -4685,7 +4739,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4685
4739
 
4686
4740
 
4687
4741
  const creativesParams = this.getCreativesParams();
4688
- 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;
4689
4746
  const {route} = this.props;
4690
4747
  const loadingTipMap = {
4691
4748
  sendingFile: 'uploadingFile',
@@ -4700,9 +4757,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4700
4757
  (deleteRcsTemplateInProgress && 'deletingTemplate') ||
4701
4758
  (this.props.EmailCreate.duplicateTemplateInProgress && 'duplicatingTemplate');
4702
4759
 
4703
- 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));
4704
4763
  const showNoTemplatesFoundZalo = this.state.channel.toUpperCase() === ZALO && isEmpty(this.state.searchedZaloTemplates) && this.state.searchingZaloTemplate;
4705
- 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));
4706
4765
  const showNoTemplatesFound = showNoTemplatesFoundZalo || showNoTemplatesFoundOther;
4707
4766
 
4708
4767
  return (
@@ -4746,7 +4805,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4746
4805
  />
4747
4806
  ) : null}
4748
4807
 
4749
- {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode ? (
4808
+ {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode && this.state?.hostName !== HOST_INFOBIP ? (
4750
4809
  <CapInfoNote
4751
4810
  message={formatMessage(messages.rcsOnlyApprovedTemplates)}
4752
4811
  />
@@ -4759,22 +4818,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4759
4818
  ) : null}
4760
4819
  <CapRow>
4761
4820
  <Pagination
4762
- templateInProgress={
4763
- this.props.Templates.getAllTemplatesInProgress
4764
- }
4821
+ templateInProgress={useLocalTemplates ? isLoadingWhenLocal : this.props.Templates.getAllTemplatesInProgress}
4765
4822
  onPageChange={
4766
- templates.length ? this.onPaginationChange : () => {}
4823
+ templates.length
4824
+ ? (useLocalTemplates ? (this.props.localTemplatesConfig?.localTemplatesOnPageChange || (() => {})) : this.onPaginationChange)
4825
+ : () => {}
4767
4826
  }
4768
4827
  >
4769
4828
  {this.getTemplateDataForGrid({
4770
4829
  previewTemplateId: this.state.zaloPreviewItemId,
4771
- isLoading,
4772
- isInitialLoading,
4830
+ isLoading: useLocalTemplates ? isLoadingWhenLocal : isLoading,
4831
+ isInitialLoading: useLocalTemplates ? isLoadingWhenLocal && templates.length === 0 : isInitialLoading,
4773
4832
  loadingTip,
4774
4833
  channel: this.state.channel,
4775
4834
  templates: this.state.searchingZaloTemplate
4776
4835
  ? this.state.searchedZaloTemplates
4777
- : this.props.TemplatesList,
4836
+ : templates,
4778
4837
  filterContent,
4779
4838
  handlers: {
4780
4839
  handlePreviewClick: this.handlePreviewClick,
@@ -4957,6 +5016,15 @@ Templates.propTypes = {
4957
5016
  WebPush: PropTypes.object,
4958
5017
  smsRegister: PropTypes.any,
4959
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
+ }),
4960
5028
  };
4961
5029
 
4962
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
+ });