@capillarytech/creatives-library 8.0.345-alpha.14 → 8.0.345-alpha.15

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 (129) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +13 -0
  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/templateVarUtils.test.js +204 -0
  8. package/v2Components/CapActionButton/constants.js +7 -0
  9. package/v2Components/CapActionButton/index.js +167 -109
  10. package/v2Components/CapActionButton/index.scss +157 -6
  11. package/v2Components/CapActionButton/messages.js +19 -3
  12. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  13. package/v2Components/CapTagList/index.js +10 -0
  14. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  21. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  22. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -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 +676 -186
  29. package/v2Components/CommonTestAndPreview/messages.js +49 -3
  30. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  31. package/v2Components/CommonTestAndPreview/sagas.js +15 -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 +2 -2
  43. package/v2Components/FormBuilder/index.js +8 -10
  44. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  45. package/v2Components/SmsFallback/constants.js +73 -0
  46. package/v2Components/SmsFallback/index.js +955 -0
  47. package/v2Components/SmsFallback/index.scss +265 -0
  48. package/v2Components/SmsFallback/messages.js +78 -0
  49. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -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 +197 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -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 +33 -23
  58. package/v2Components/TemplatePreview/constants.js +2 -0
  59. package/v2Components/TemplatePreview/index.js +143 -28
  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 +67 -0
  73. package/v2Containers/CreativesContainer/index.js +300 -103
  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/reducer.js +3 -11
  85. package/v2Containers/Email/sagas.js +5 -9
  86. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +0 -4
  87. package/v2Containers/Email/tests/sagas.test.js +3 -21
  88. package/v2Containers/Rcs/constants.js +119 -8
  89. package/v2Containers/Rcs/index.js +2379 -807
  90. package/v2Containers/Rcs/index.js.rej +1336 -0
  91. package/v2Containers/Rcs/index.scss +276 -6
  92. package/v2Containers/Rcs/index.scss.rej +74 -0
  93. package/v2Containers/Rcs/messages.js +38 -3
  94. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  95. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  96. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  97. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  98. package/v2Containers/Rcs/tests/index.test.js +152 -121
  99. package/v2Containers/Rcs/tests/mockData.js +38 -0
  100. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  101. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  102. package/v2Containers/Rcs/utils.js +478 -11
  103. package/v2Containers/Sms/Create/index.js +100 -40
  104. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  105. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  106. package/v2Containers/SmsTrai/Create/index.js +9 -4
  107. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  108. package/v2Containers/SmsTrai/Edit/index.js +636 -130
  109. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  110. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  111. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  112. package/v2Containers/SmsWrapper/index.js +37 -8
  113. package/v2Containers/TagList/index.js +6 -0
  114. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  115. package/v2Containers/Templates/_templates.scss +163 -2
  116. package/v2Containers/Templates/actions.js +11 -0
  117. package/v2Containers/Templates/constants.js +2 -0
  118. package/v2Containers/Templates/index.js +119 -54
  119. package/v2Containers/Templates/sagas.js +57 -12
  120. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  121. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  122. package/v2Containers/Templates/tests/sagas.test.js +193 -123
  123. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  124. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  125. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  126. package/v2Containers/TemplatesV2/index.js +86 -23
  127. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  128. package/v2Containers/Whatsapp/index.js +3 -20
  129. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -132,7 +132,7 @@ import { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES, INAPP_MEDIA_TYPES, BI
132
132
  import { ZALO_STATUS_OPTIONS, ZALO_STATUSES } from '../Zalo/constants';
133
133
  import { getWhatsappContent, getWhatsappStatus, getWhatsappCategory, getWhatsappCta, getWhatsappQuickReply, getWhatsappAutoFill, getWhatsappCarouselButtonView } from '../Whatsapp/utils';
134
134
  import { getRCSContent } from '../Rcs/utils';
135
- import {RCS_STATUSES} from '../Rcs/constants';
135
+ import { RCS_STATUSES, HOST_INFOBIP } from '../Rcs/constants';
136
136
  import zaloMessages from '../Zalo/messages';
137
137
  import rcsMessages from '../Rcs/messages';
138
138
  import inAppMessages from '../InApp/messages';
@@ -468,7 +468,13 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
468
468
  if (this.props.location.query.type === 'embedded') {
469
469
  this.props.actions.resetAccount();
470
470
  }
471
- if (['line', VIBER_CHANNEL, FACEBOOK_CHANNEL, 'sms', 'email', 'ebill'].includes((this.state.channel || '').toLowerCase())) {
471
+ // When using local templates (e.g. SMS fallback selector), do not fetch from API or we overwrite global store and break background RCS list
472
+ const useLocalTemplates = get(
473
+ this.props,
474
+ 'localTemplatesConfig.useLocalTemplates',
475
+ get(this.props, 'useLocalTemplates', false),
476
+ );
477
+ if (!useLocalTemplates && ['line', VIBER_CHANNEL, FACEBOOK_CHANNEL, 'sms', 'email', 'ebill'].includes((this.state.channel || '').toLowerCase())) {
472
478
  const queryParams = {
473
479
  // name: this.state.searchText,
474
480
  // sortBy: this.state.sortBy,
@@ -981,8 +987,16 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
981
987
 
982
988
  componentWillUnmount() {
983
989
  window.removeEventListener("message", this.handleFrameTasks);
984
- this.props.actions.resetTemplateStoreData();
985
- this.props.globalActions.clearMetaEntities();
990
+ // When using local templates (e.g. SMS fallback selector), do not clear global store or background RCS list is wiped
991
+ const useLocalTemplates = get(
992
+ this.props,
993
+ 'localTemplatesConfig.useLocalTemplates',
994
+ get(this.props, 'useLocalTemplates', false),
995
+ );
996
+ if (!useLocalTemplates) {
997
+ this.props.actions.resetTemplateStoreData();
998
+ this.props.globalActions.clearMetaEntities();
999
+ }
986
1000
  // Clear any pending timeouts to prevent memory leaks
987
1001
  if (this._clearEditTimeout) {
988
1002
  clearTimeout(this._clearEditTimeout);
@@ -1767,12 +1781,20 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1767
1781
  }
1768
1782
 
1769
1783
  filterRcsTemplates = (templates) => {
1770
- let { selectedRcsStatus } = this.state;
1771
- selectedRcsStatus = !this.props.isFullMode ? RCS_STATUSES.approved : '';
1772
- if (selectedRcsStatus) {
1773
- return templates?.filter((template) => template?.versions?.base?.content?.RCS?.rcsContent?.cardContent?.[0]?.Status === selectedRcsStatus);
1784
+ const selectedRcsAccountName = this.props?.Templates?.selectedRcsAccount?.name || '';
1785
+ const hostName = this.state?.hostName;
1786
+ let nextTemplates = templates || [];
1787
+ if (selectedRcsAccountName) {
1788
+ nextTemplates = nextTemplates.filter(
1789
+ (t) => get(t, 'versions.base.content.RCS.rcsContent.accountName', '') === selectedRcsAccountName
1790
+ );
1774
1791
  }
1775
- return templates;
1792
+ if (!this.props.isFullMode && hostName !== HOST_INFOBIP) {
1793
+ return nextTemplates.filter(
1794
+ (t) => get(t, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', 'unavailable') === RCS_STATUSES.approved
1795
+ );
1796
+ }
1797
+ return nextTemplates;
1776
1798
  }
1777
1799
 
1778
1800
  filterZaloTemplates = (templates) => {
@@ -1947,7 +1969,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1947
1969
  style={{ marginRight: "16px" }}
1948
1970
  type="eye"
1949
1971
  onClick={() => {
1950
- if (!this.props.isFullMode || this.props.isDltFromRcs) {
1972
+ if (!this.props.isFullMode || this.props.isDltFromRcs || this.props.isSmsFallbackFromRcs) {
1951
1973
  if (!get(template, "versions.base.content.zalo.previewUrl", "")) {
1952
1974
  this.setState({ zaloPreviewItemId: template?._id });
1953
1975
  }
@@ -2407,13 +2429,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2407
2429
  templateData.title = (
2408
2430
  <CapRow>
2409
2431
  <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2410
- <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2432
+ {this.state.hostName !== HOST_INFOBIP && <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2411
2433
  <CapStatus
2412
2434
  type={statusDisplay}
2413
- text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2414
- labelType="label3"
2415
- />
2416
- </CapRow>
2435
+ text={statusDisplay && this.props.intl.formatMessage(rcsMessages?.[`${statusDisplay}_STATUS`])}
2436
+ labelType="label3"
2437
+ />
2438
+ </CapRow>
2439
+ }
2417
2440
  </CapRow>
2418
2441
  );
2419
2442
 
@@ -2937,6 +2960,7 @@ return (<div>
2937
2960
  let routeParams = {};
2938
2961
  const {fbAdManager} = this.props;
2939
2962
  const isLibraryMode = this.isEnabledInLibraryModule("callCreateFromProps");
2963
+
2940
2964
  if (!isLibraryMode) {
2941
2965
  timeTracker.startTimer(CHANNEL_CREATE_TRACK_MAPPING[channel]);
2942
2966
  }
@@ -3282,6 +3306,13 @@ return (<div>
3282
3306
  this.setState({modeType});
3283
3307
  }
3284
3308
  const { _id: id } = template;
3309
+ const {
3310
+ localTemplatesConfig,
3311
+ fbAdManager,
3312
+ isDltFromRcs,
3313
+ isSmsFallbackFromRcs,
3314
+ onSelectTemplate,
3315
+ } = this.props;
3285
3316
  const type = this.props.location.query.type;
3286
3317
  const module = this.props.location.query.module;
3287
3318
  const isLanguageSupport = (this.props.location.query.isLanguageSupport) ? this.props.location.query.isLanguageSupport : false;
@@ -3367,10 +3398,12 @@ return (<div>
3367
3398
  }
3368
3399
  if (this.isEnabledInLibraryModule("callSelectFromProps")) {
3369
3400
  let data = id;
3370
- if (this.props.fbAdManager || this.props.isDltFromRcs) {
3401
+ if (localTemplatesConfig?.useLocalTemplates) {
3402
+ data = template;
3403
+ } else if (fbAdManager || isDltFromRcs || isSmsFallbackFromRcs) {
3371
3404
  data = this.selectTemplate(id);
3372
3405
  }
3373
- this.props.onSelectTemplate(data, this.props.fbAdManager);
3406
+ onSelectTemplate(data, fbAdManager);
3374
3407
  } else {
3375
3408
  timeTracker.startTimer(CHANNEL_EDIT_TRACK_MAPPING[this.state.channel.toLowerCase()]);
3376
3409
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4186,9 +4219,14 @@ return (<div>
4186
4219
  const isWechatEmbedded = !this.props.isFullMode && channel.toUpperCase() === WECHAT;
4187
4220
  const channelLowerCase = (channel || '').toLowerCase();
4188
4221
  const isTraiDltFeature = this.checkDLTfeatureEnable();
4189
-
4190
4222
  const createButton =
4191
- ( (channelLowerCase === WHATSAPP_LOWERCASE || channelLowerCase === RCS_LOWERCASE) && !this.props.isFullMode )
4223
+ (
4224
+ (
4225
+ channelLowerCase === WHATSAPP_LOWERCASE
4226
+ || channelLowerCase === RCS_LOWERCASE
4227
+ )
4228
+ && !this.props.isFullMode
4229
+ )
4192
4230
  ? (
4193
4231
  <CapLink
4194
4232
  onClick={this.openCreativesFullMode}
@@ -4214,17 +4252,23 @@ return (<div>
4214
4252
  if (([WHATSAPP_LOWERCASE, ZALO_LOWERCASE, RCS_LOWERCASE].includes(this.state?.channel?.toLocaleLowerCase()) && isEmpty(this.state?.hostName))) {
4215
4253
  isfilterContentVisisble = false;
4216
4254
  }
4217
- const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4218
- {isfilterContentVisisble && <CapInput.Search
4219
- className="search-text"
4220
- style={{width: '210px'}}
4221
- placeholder={this.props.intl.formatMessage(messages.searchText)}
4222
- value={this.state.searchText}
4223
- onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4224
- disabled={this.checkSearchDisabled()}
4225
- onClear={() => this.searchTemplate('', this.state.channel)}
4226
- onScroll={(e) => e.stopPropagation()}
4227
- />}
4255
+
4256
+ const useLocalTemplates = this.props.localTemplatesConfig?.useLocalTemplates;
4257
+ const builtFilterContent = ((isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && (
4258
+ <div className="action-container">
4259
+ <div className="action-container__toolbar-row">
4260
+ {isfilterContentVisisble ? (
4261
+ <CapInput.Search
4262
+ className="search-text"
4263
+ placeholder={this.props.intl.formatMessage(messages.searchText)}
4264
+ value={this.state.searchText}
4265
+ onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4266
+ onSearch={() => this.searchTemplate(this.state.searchText, this.state.channel)}
4267
+ onClear={() => this.searchTemplate('', this.state.channel)}
4268
+ onScroll={(e) => e.stopPropagation()}
4269
+ disabled={this.checkSearchDisabled()}
4270
+ />
4271
+ ) : null}
4228
4272
  {
4229
4273
  channel.toUpperCase() === WECHAT && <CapRadio.CapRadioGroup className="wechat-filters" defaultValue={wechatFilter} onChange={this.setWechatFilter}>
4230
4274
  <CapRadio.Button value={WECHAT_FILTERS.ALL}><CapLabel type="label2">
@@ -4370,20 +4414,27 @@ return (<div>
4370
4414
  </div>
4371
4415
  )
4372
4416
  }
4373
- <div style={{display: "flex", justifyContent: "space-between", alignItems: 'center'}}>
4374
- {
4375
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded)? (
4376
- <CapTooltip title={whatsappCountExceedText}>
4377
- <div className="button-disabled-tooltip-wrapper">
4378
- {createButton}
4379
- </div>
4380
- </CapTooltip>
4381
- )
4382
- : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4383
- }
4417
+ </div>
4418
+ <div>
4419
+ <div className="action-container__create-row">
4420
+ {
4421
+ this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4422
+ <CapTooltip title={whatsappCountExceedText}>
4423
+ <div className="button-disabled-tooltip-wrapper">
4424
+ {createButton}
4425
+ </div>
4426
+ </CapTooltip>
4427
+ )
4428
+ : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && !this.props.isSmsFallbackFromRcs && createButton
4429
+ }
4430
+ </div>
4431
+ </div>
4384
4432
  </div>
4385
-
4386
- </div>);
4433
+ ));
4434
+ const localTemplatesFilterContent = get(this.props, 'localTemplatesConfig.localTemplatesFilterContent', null);
4435
+ const filterContent = (useLocalTemplates && localTemplatesFilterContent) != null
4436
+ ? localTemplatesFilterContent
4437
+ : builtFilterContent;
4387
4438
  let htmlPreviewContent = "";
4388
4439
  if (this.state.channel.toLowerCase() === 'ebill') {
4389
4440
  htmlPreviewContent = this.state.previewTemplate && this.state.previewTemplate.versions && this.state.previewTemplate.versions.base && this.state.previewTemplate.versions.base['ebill-editor'];
@@ -4393,7 +4444,10 @@ return (<div>
4393
4444
 
4394
4445
 
4395
4446
  const creativesParams = this.getCreativesParams();
4396
- const templates = this.props.TemplatesList || [];
4447
+ const templates = useLocalTemplates
4448
+ ? (this.props.localTemplatesConfig?.localTemplates || [])
4449
+ : (this.props.TemplatesList || []);
4450
+ const isLoadingWhenLocal = useLocalTemplates && !!this.props.localTemplatesConfig?.localTemplatesLoading;
4397
4451
  const {route} = this.props;
4398
4452
  const loadingTipMap = {
4399
4453
  sendingFile: 'uploadingFile',
@@ -4408,9 +4462,11 @@ return (<div>
4408
4462
  (deleteRcsTemplateInProgress && 'deletingTemplate') ||
4409
4463
  (this.props.EmailCreate.duplicateTemplateInProgress && 'duplicatingTemplate');
4410
4464
 
4411
- const loadingTip = messages[loadingTipIntl] ? this.props.intl.formatMessage(messages[loadingTipIntl]) : this.props.intl.formatMessage(messages.gettingAllTemplates);
4465
+ const loadingTip = useLocalTemplates && this.props.localTemplatesConfig?.localTemplatesLoadingTip
4466
+ ? this.props.localTemplatesConfig.localTemplatesLoadingTip
4467
+ : (messages[loadingTipIntl] ? this.props.intl.formatMessage(messages[loadingTipIntl]) : this.props.intl.formatMessage(messages.gettingAllTemplates));
4412
4468
  const showNoTemplatesFoundZalo = this.state.channel.toUpperCase() === ZALO && isEmpty(this.state.searchedZaloTemplates) && this.state.searchingZaloTemplate;
4413
- const showNoTemplatesFoundOther = ![ZALO].includes(this.state.channel.toUpperCase()) && isEmpty(this.props.TemplatesList) && !this.props.Templates.getAllTemplatesInProgress && !isEmpty(this.state.searchText);
4469
+ const showNoTemplatesFoundOther = ![ZALO].includes(this.state.channel.toUpperCase()) && isEmpty(templates) && (useLocalTemplates ? !isLoadingWhenLocal : !this.props.Templates.getAllTemplatesInProgress) && (useLocalTemplates || !isEmpty(this.state.searchText));
4414
4470
  const showNoTemplatesFound = showNoTemplatesFoundZalo || showNoTemplatesFoundOther;
4415
4471
 
4416
4472
  return (
@@ -4442,7 +4498,7 @@ return (<div>
4442
4498
  />
4443
4499
  ) : null}
4444
4500
 
4445
- {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode ? (
4501
+ {channel.toLowerCase() === RCS_LOWERCASE && !isFullMode && this.state?.hostName !== HOST_INFOBIP ? (
4446
4502
  <CapInfoNote
4447
4503
  message={formatMessage(messages.rcsOnlyApprovedTemplates)}
4448
4504
  />
@@ -4455,22 +4511,22 @@ return (<div>
4455
4511
  ) : null}
4456
4512
  <CapRow>
4457
4513
  <Pagination
4458
- templateInProgress={
4459
- this.props.Templates.getAllTemplatesInProgress
4460
- }
4514
+ templateInProgress={useLocalTemplates ? isLoadingWhenLocal : this.props.Templates.getAllTemplatesInProgress}
4461
4515
  onPageChange={
4462
- templates.length ? this.onPaginationChange : () => {}
4516
+ templates.length
4517
+ ? (useLocalTemplates ? (this.props.localTemplatesConfig?.localTemplatesOnPageChange || (() => {})) : this.onPaginationChange)
4518
+ : () => {}
4463
4519
  }
4464
4520
  >
4465
4521
  {this.getTemplateDataForGrid({
4466
4522
  previewTemplateId: this.state.zaloPreviewItemId,
4467
- isLoading,
4468
- isInitialLoading,
4523
+ isLoading: useLocalTemplates ? isLoadingWhenLocal : isLoading,
4524
+ isInitialLoading: useLocalTemplates ? isLoadingWhenLocal && templates.length === 0 : isInitialLoading,
4469
4525
  loadingTip,
4470
4526
  channel: this.state.channel,
4471
4527
  templates: this.state.searchingZaloTemplate
4472
4528
  ? this.state.searchedZaloTemplates
4473
- : this.props.TemplatesList,
4529
+ : templates,
4474
4530
  filterContent,
4475
4531
  handlers: {
4476
4532
  handlePreviewClick: this.handlePreviewClick,
@@ -4653,6 +4709,15 @@ Templates.propTypes = {
4653
4709
  WebPush: PropTypes.object,
4654
4710
  smsRegister: PropTypes.any,
4655
4711
  isDltFromRcs: PropTypes.bool,
4712
+ isSmsFallbackFromRcs: PropTypes.bool,
4713
+ localTemplatesConfig: PropTypes.shape({
4714
+ useLocalTemplates: PropTypes.bool,
4715
+ localTemplates: PropTypes.arrayOf(PropTypes.object),
4716
+ localTemplatesLoading: PropTypes.bool,
4717
+ localTemplatesLoadingTip: PropTypes.string,
4718
+ localTemplatesFilterContent: PropTypes.node,
4719
+ localTemplatesOnPageChange: PropTypes.func,
4720
+ }),
4656
4721
  };
4657
4722
 
4658
4723
  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 { schema, normalize } from 'normalizr';
@@ -7,31 +7,69 @@ import * as Api from '../../services/api';
7
7
  import * as types from './constants';
8
8
  import { saveCdnConfigs, removeAllCdnLocalStorageItems } from '../../utils/cdnTransformation';
9
9
  import { COPY_OF } from '../../constants/unified';
10
+ import { fetchSmsTemplatesFromQuery } from './utils/smsTemplatesListApi';
10
11
  import { ZALO_TEMPLATE_INFO_REQUEST } from '../Zalo/constants';
11
12
  import { getTemplateInfoById } from '../Zalo/saga';
12
- // Individual exports for testing
13
- export function* getAllTemplates(channel, queryParams) {
13
+
14
+ export function* getLocalSmsTemplates(action) {
14
15
  try {
15
- const result = yield call(Api.getAllTemplates, channel, queryParams);
16
- const channelTemplates = (channel.channel === 'wechat') ? { templates: [...result.response.mapped, ...result.response.richmedia] } : result.response;
17
- // const sidebar = result.response.sidebar;
18
- if (channel.channel === 'wechat' && channel.queryParams && channel.queryParams.sortBy && channel.queryParams.sortBy.toLocaleLowerCase() === ("Most Recent").toLocaleLowerCase()) {
16
+ const fetched = yield call(
17
+ fetchSmsTemplatesFromQuery,
18
+ action.queryParams,
19
+ action.intlCopyOf,
20
+ );
21
+ if (typeof action.onSuccess === 'function') {
22
+ yield call(action.onSuccess, fetched);
23
+ }
24
+ } catch (error) {
25
+ if (typeof action.onFailure === 'function') {
26
+ yield call(action.onFailure, error);
27
+ }
28
+ }
29
+ }
30
+
31
+ export function* getAllTemplates(action) {
32
+ try {
33
+ if (action.channel && String(action.channel).toLowerCase() === 'sms') {
34
+ const fetched = yield call(
35
+ fetchSmsTemplatesFromQuery,
36
+ action.queryParams,
37
+ action.intlCopyOf,
38
+ );
39
+ yield put({
40
+ type: types.GET_ALL_TEMPLATES_SUCCESS,
41
+ data: fetched.channelTemplates,
42
+ weCRMTemplate: fetched.weCRMTemplate,
43
+ isReset: get(action, 'queryParams.page') === 1,
44
+ });
45
+ return;
46
+ }
47
+
48
+ const result = yield call(Api.getAllTemplates, action);
49
+ const channelTemplates = (action.channel === 'wechat')
50
+ ? { templates: [...result.response.mapped, ...result.response.richmedia] }
51
+ : result.response;
52
+ if (action.channel === 'wechat' && action.queryParams && action.queryParams.sortBy && action.queryParams.sortBy.toLocaleLowerCase() === ("Most Recent").toLocaleLowerCase()) {
19
53
  channelTemplates.templates.sort((a, b) => {
20
54
  const dateA = new Date(a.updatedAt);
21
55
  const dateB = new Date(b.updatedAt);
22
56
  return dateB - dateA;
23
57
  });
24
- } else if (channel.channel === 'wechat' && channel.queryParams && channel.queryParams.sortBy && channel.queryParams.sortBy.toLocaleLowerCase() === ("Alphabetically").toLocaleLowerCase()) {
58
+ } else if (action.channel === 'wechat' && action.queryParams && action.queryParams.sortBy && action.queryParams.sortBy.toLocaleLowerCase() === ("Alphabetically").toLocaleLowerCase()) {
25
59
  channelTemplates.templates.sort((a, b) => b.name - a.name);
26
60
  }
27
- // Update the "name" property in each template
28
- if (channel.intlCopyOf && channelTemplates?.templates) {
61
+ if (action.intlCopyOf && channelTemplates?.templates) {
29
62
  channelTemplates.templates = channelTemplates.templates.map((template) => ({
30
63
  ...template,
31
- name: template.name.replace(new RegExp(COPY_OF, 'g'), channel.intlCopyOf),
64
+ name: template.name.replace(new RegExp(COPY_OF, 'g'), action.intlCopyOf),
32
65
  }));
33
66
  }
34
- yield put({ type: types.GET_ALL_TEMPLATES_SUCCESS, data: channelTemplates, weCRMTemplate: result.response.unMapped, isReset: channel.queryParams.page === 1 });
67
+ yield put({
68
+ type: types.GET_ALL_TEMPLATES_SUCCESS,
69
+ data: channelTemplates,
70
+ weCRMTemplate: result.response.unMapped,
71
+ isReset: get(action, 'queryParams.page') === 1,
72
+ });
35
73
  } catch (error) {
36
74
  yield put({ type: types.GET_ALL_TEMPLATES_FAILURE, error });
37
75
  }
@@ -205,6 +243,11 @@ export function* watchGetAllTemplates() {
205
243
  yield takeLatest(types.GET_ALL_TEMPLATES_REQUEST, getAllTemplates);
206
244
  }
207
245
 
246
+
247
+ export function* watchGetLocalSmsTemplates() {
248
+ yield takeEvery(types.GET_LOCAL_SMS_TEMPLATES_REQUEST, getLocalSmsTemplates);
249
+ }
250
+
208
251
  export function* watchDeleteTemplate() {
209
252
  yield takeLatest(types.DELETE_TEMPLATE_REQUEST, deleteTemplate);
210
253
  }
@@ -258,6 +301,7 @@ export function* watchForGetTemplateInfoById() {
258
301
  // All sagas to be loaded
259
302
  export default [
260
303
  watchGetAllTemplates,
304
+ watchGetLocalSmsTemplates,
261
305
  watchDeleteTemplate,
262
306
  watchDeleteRcsTemplate,
263
307
  watchGetUserList,
@@ -274,6 +318,7 @@ export default [
274
318
  export function* v2TemplateSaga() {
275
319
  yield all([
276
320
  watchGetAllTemplates(),
321
+ watchGetLocalSmsTemplates(),
277
322
  watchDeleteTemplate(),
278
323
  watchDeleteRcsTemplate(),
279
324
  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
+ });