@capillarytech/creatives-library 9.0.70-alpha.sprint-cap-213712.13 → 9.0.71

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 (46) hide show
  1. package/constants/unified.js +0 -1
  2. package/package.json +1 -1
  3. package/utils/common.js +0 -6
  4. package/v2Components/FormBuilder/Classic.js +3 -0
  5. package/v2Containers/Cap/tests/index.test.js +1 -0
  6. package/v2Containers/CreativesContainer/SlideBoxContent.js +39 -30
  7. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  8. package/v2Containers/CreativesContainer/constants.js +1 -0
  9. package/v2Containers/CreativesContainer/index.js +28 -3
  10. package/v2Containers/CreativesContainer/messages.js +4 -0
  11. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +40 -12
  12. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +10 -0
  13. package/v2Containers/CreativesContainer/tests/switchToEditAfterCreate.test.js +152 -0
  14. package/v2Containers/Email/index.js +41 -6
  15. package/v2Containers/Email/tests/index.test.js +123 -0
  16. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +32 -12
  17. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +34 -0
  18. package/v2Containers/InApp/index.js +19 -23
  19. package/v2Containers/InApp/tests/index.test.js +52 -9
  20. package/v2Containers/InappAdvance/index.js +19 -24
  21. package/v2Containers/InappAdvance/tests/index.test.js +39 -15
  22. package/v2Containers/Line/Container/index.js +18 -8
  23. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +129 -108
  24. package/v2Containers/Line/Container/tests/index.test.js +68 -0
  25. package/v2Containers/MobilePush/Create/index.js +24 -5
  26. package/v2Containers/MobilePush/Create/test/stayOpenOnSave.test.js +80 -0
  27. package/v2Containers/MobilePush/Edit/index.js +15 -9
  28. package/v2Containers/MobilePush/Edit/test/stayOpenOnSave.test.js +60 -0
  29. package/v2Containers/MobilePushNew/index.js +41 -7
  30. package/v2Containers/MobilePushNew/tests/index.test.js +61 -0
  31. package/v2Containers/Sms/Create/index.js +32 -14
  32. package/v2Containers/Sms/Create/test/stayOpenOnSave.test.js +92 -0
  33. package/v2Containers/Sms/Edit/index.js +22 -14
  34. package/v2Containers/Sms/Edit/tests/index.test.js +66 -0
  35. package/v2Containers/Templates/constants.js +1 -0
  36. package/v2Containers/Templates/index.js +48 -31
  37. package/v2Containers/Templates/messages.js +4 -0
  38. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +128 -4
  39. package/v2Containers/Templates/tests/archival.test.js +221 -0
  40. package/v2Containers/Viber/index.js +15 -6
  41. package/v2Containers/Viber/tests/index.test.js +97 -28
  42. package/v2Containers/WebPush/Create/components/FormActions.js +1 -1
  43. package/v2Containers/WebPush/Create/components/FormActions.test.js +3 -3
  44. package/v2Containers/WebPush/Create/index.js +32 -6
  45. package/v2Containers/WebPush/Create/messages.js +0 -4
  46. package/v2Containers/WebPush/Create/tests/stayOpenOnSave.test.js +268 -0
@@ -117,7 +117,7 @@ import {
117
117
  URL,
118
118
  SITE_URL,
119
119
  } from '../App/constants';
120
- import {MAX_WHATSAPP_TEMPLATES, WARNING_WHATSAPP_TEMPLATES , ACCOUNT_MAPPING_ON_CHANNEL, noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc, noApprovedWhatsappZaloTemplatesTitle, noApprovedWhatsappTemplatesDesc, zaloDescIllustration, noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc, ARCHIVE_STATUS_ACTIVE, ARCHIVE_STATUS_ARCHIVED, ARCHIVE_REFRESH_TYPE_ARCHIVE, ARCHIVE_REFRESH_TYPE_UNARCHIVE} from './constants';
120
+ import {MAX_WHATSAPP_TEMPLATES, WARNING_WHATSAPP_TEMPLATES , ACCOUNT_MAPPING_ON_CHANNEL, noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc, noApprovedWhatsappZaloTemplatesTitle, noApprovedWhatsappTemplatesDesc, zaloDescIllustration, noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc, ARCHIVE_STATUS_ACTIVE, ARCHIVE_STATUS_ARCHIVED, ARCHIVE_REFRESH_TYPE_ARCHIVE, ARCHIVE_REFRESH_TYPE_UNARCHIVE, DEFAULT_ARCHIVAL_LIMIT} from './constants';
121
121
  import { COPY_OF, EMBEDDED } from '../../constants/unified';
122
122
  import { sanitizeFileName, exportEmailTemplateHtml } from '../../utils/downloadFile';
123
123
  import {
@@ -152,7 +152,7 @@ import {
152
152
  CAP_SPACE_16, CAP_G08, CAP_G05, CAP_SPACE_08, CAP_SPACE_12, CAP_YELLOW01,
153
153
  } from '@capillarytech/cap-ui-library/styled/variables';
154
154
  import { SMS_CATEGORY_TAG_STYLES, SMS_CATEGORY_LABELS } from '../../v2Components/SmsFallback/constants';
155
- import { GA } from '@capillarytech/cap-ui-utils';
155
+ import { GA, getEnvVariable } from '@capillarytech/cap-ui-utils';
156
156
  import { MAPP_SDK } from '../InApp/constants';
157
157
  import injectReducer from '../../utils/injectReducer';
158
158
  import v2TemplateReducer from './reducer';
@@ -186,6 +186,7 @@ const AccountScrollDiv = styled.div`
186
186
  overflow: scroll;
187
187
  height: calc(100vh - 18.75rem);
188
188
  `;
189
+ const ARCHIVAL_LIMIT = parseInt(getEnvVariable('ARCHIVAL_LIMIT'), 10) || DEFAULT_ARCHIVAL_LIMIT;
189
190
  const ACCOUNT_SELECTION_MODE = "accountSelectionMode";
190
191
  const ACCOUNT_CHANGE_MODE = 'accountChangeMode';
191
192
  const TEMPLATES_MODE = "templatesMode";
@@ -726,9 +727,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
726
727
  });
727
728
 
728
729
  this.getAllTemplates({params, resetPage: true});
729
-
730
- this.props.mobilePushNewActions.clearData();
731
-
730
+
731
+ // Open form still uses this data; cleared on close (onCreateComplete).
732
+ if (!this.state.routeParams) {
733
+ this.props.mobilePushNewActions.clearData();
734
+ }
735
+
732
736
  this.props.mobilePushNewActions.clearEditResponse();
733
737
 
734
738
  // Reset flag after a delay to allow for next edit
@@ -1728,6 +1732,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1728
1732
  }
1729
1733
 
1730
1734
  onCreateComplete = (isReloadTemplates) => {
1735
+ // Push edit data kept for the open slidebox (see the MobilePushNew edit-response handling) is cleared on close
1736
+ if (this.state.channel?.toLowerCase() === MOBILE_PUSH_LOWERCASE && this.props.mobilePushNewActions?.clearData) {
1737
+ this.props.mobilePushNewActions.clearData();
1738
+ }
1731
1739
  this.setState({routeParams: null, previewOpen: false}, () => {
1732
1740
  if (this.isFullMode() && isReloadTemplates) {
1733
1741
  this.getAllTemplates({params: {}}, true);
@@ -2152,10 +2160,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2152
2160
  // Archive eligibility per template: Zalo never; WhatsApp/RCS not when pending/awaiting
2153
2161
  const cardWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
2154
2162
  const cardRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
2155
- const isArchivalEnabled = commonUtil.hasCreativesArchivalEnabled();
2156
- const isCardArchiveEligible = isArchivalEnabled && this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
2157
- const isArchivedMode = isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
2158
- const isAnyArchiveInProgress = isArchivalEnabled && !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
2163
+ const isCardArchiveEligible = this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
2164
+ const isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
2165
+ const isAnyArchiveInProgress = !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
2159
2166
  const templateData = {
2160
2167
  key: `${currentChannel}-card-${template?.name}`,
2161
2168
  title: (
@@ -2300,7 +2307,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2300
2307
  </CapMenu.Item>
2301
2308
  )}
2302
2309
  {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2303
- {commonUtil.hasCreativesArchivalEnabled() && (() => {
2310
+ {(() => {
2304
2311
  const channelUp = this.state.channel.toUpperCase();
2305
2312
  if (!this.isChannelArchiveEligible(channelUp, status, rcsStatus)) return null;
2306
2313
  const canArchive = this.canPerform(PERMISSIONS.CREATIVE_DELETE);
@@ -2921,12 +2928,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2921
2928
 
2922
2929
  const noLoaderAndSearchText = isEmpty(this.state.searchText) && !isLoading;
2923
2930
 
2924
- const isArchivalEnabledLocal = commonUtil.hasCreativesArchivalEnabled();
2925
- const isArchivedModeLocal = isArchivalEnabledLocal && get(this.props, 'Templates.isArchivedMode', false);
2931
+ const isArchivedModeLocal = get(this.props, 'Templates.isArchivedMode', false);
2926
2932
  const selectedIdsLocal = get(this.props, 'Templates.selectedTemplateIds', []);
2927
2933
  const selectedIdsArrayLocal = selectedIdsLocal && typeof selectedIdsLocal.toJS === 'function' ? selectedIdsLocal.toJS() : (Array.isArray(selectedIdsLocal) ? selectedIdsLocal : []);
2928
2934
  const selectedCountLocal = selectedIdsArrayLocal.length;
2929
- const hasSelectionLocal = isArchivalEnabledLocal && this.props.isFullMode && selectedCountLocal > 0;
2935
+ const hasSelectionLocal = this.props.isFullMode && selectedCountLocal > 0;
2936
+ const isArchiveLimitExceeded = selectedCountLocal > ARCHIVAL_LIMIT;
2930
2937
 
2931
2938
  return (<div>
2932
2939
  {[WECHAT, MOBILE_PUSH, WEBPUSH, INAPP, WHATSAPP, ZALO, RCS].includes(currentChannel) && this.showAccountName()}
@@ -2938,16 +2945,21 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2938
2945
  {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2939
2946
  </CapLabel>
2940
2947
  <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
2941
- <CapButton
2942
- type="primary"
2943
- className="archive-template-button"
2944
- prefix={<CapIcon type="archive" size="s" />}
2945
- onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2946
- >
2947
- <span className="archive-btn-label">
2948
- <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2948
+ <CapTooltip title={isArchiveLimitExceeded ? this.props.intl.formatMessage(messages.archiveLimitExceeded, { limit: ARCHIVAL_LIMIT }) : ''}>
2949
+ <span>
2950
+ <CapButton
2951
+ type="primary"
2952
+ className="archive-template-button"
2953
+ disabled={isArchiveLimitExceeded}
2954
+ prefix={<CapIcon type="archive" size="s" />}
2955
+ onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2956
+ >
2957
+ <span className="archive-btn-label">
2958
+ <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2959
+ </span>
2960
+ </CapButton>
2949
2961
  </span>
2950
- </CapButton>
2962
+ </CapTooltip>
2951
2963
  </NoPermissionWrapper>
2952
2964
  <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2953
2965
  <FormattedMessage {...messages.archiveConfirmCancel} />
@@ -4188,7 +4200,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4188
4200
  <span className="template-name" style={{ fontWeight: `${this.state.channel.toLowerCase() === 'wechat' ? '400' : '600'}` }}>
4189
4201
  { template && template.versions && template.versions.history && template.versions.history.length > 1 && this.state.channel.toLowerCase() !== 'mobilepush' && <i style={{fontSize: '16px', margin: '0 8px 0 0', verticalAlign: 'middle'}} className="material-icons">filter_none</i>}
4190
4202
  {template?.name}
4191
- {commonUtil.hasCreativesArchivalEnabled() && template.isArchived && <CapColoredTag tagColor={CAP_G08} tagTextColor={CAP_G05} className="archived-tag">{this.props.intl.formatMessage(messages.archivedTag)}</CapColoredTag>}
4203
+ {template.isArchived && <CapColoredTag tagColor={CAP_G08} tagTextColor={CAP_G05} className="archived-tag">{this.props.intl.formatMessage(messages.archivedTag)}</CapColoredTag>}
4192
4204
  </span>
4193
4205
  {this.props.location.query.type !== EMBEDDED && <CapPopover
4194
4206
  trigger="click"
@@ -4201,7 +4213,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4201
4213
  </CapButton>
4202
4214
  </NoPermissionWrapper>
4203
4215
  </div>}
4204
- {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
4216
+ {this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
4205
4217
  <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4206
4218
  <CapButton
4207
4219
  type="link"
@@ -4213,7 +4225,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4213
4225
  </CapButton>
4214
4226
  </NoPermissionWrapper>
4215
4227
  </div>}
4216
- {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
4228
+ {this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
4217
4229
  <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4218
4230
  <CapButton
4219
4231
  type="link"
@@ -4860,11 +4872,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4860
4872
  if (([WHATSAPP_LOWERCASE, ZALO_LOWERCASE, RCS_LOWERCASE].includes(this.state?.channel?.toLocaleLowerCase()) && isEmpty(this.state?.hostName))) {
4861
4873
  isfilterContentVisisble = false;
4862
4874
  }
4863
- const _isArchivalEnabled = commonUtil.hasCreativesArchivalEnabled();
4864
- const _isArchivedMode = _isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
4875
+ const _isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
4865
4876
  const _renderSelectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
4866
4877
  const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4867
- const _renderHasSelection = _isArchivalEnabled && this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4878
+ const _renderHasSelection = this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4868
4879
 
4869
4880
  const useLocalTemplates = this.props.localTemplatesConfig?.useLocalTemplates;
4870
4881
  const builtFilterContent = ((isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && (
@@ -5026,7 +5037,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
5026
5037
  }
5027
5038
  <div className="template-listing-header-actions">
5028
5039
  {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active, archive flag enabled */}
5029
- {commonUtil.hasCreativesArchivalEnabled() && !_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
5040
+ {!_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
5030
5041
  <CapDropdown
5031
5042
  trigger={['click']}
5032
5043
  overlay={
@@ -5122,7 +5133,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
5122
5133
  />
5123
5134
 
5124
5135
  {/* Archived mode header with back arrow (full mode only, archive flag enabled) */}
5125
- {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
5136
+ {this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
5126
5137
  <CapRow type="flex" align="middle" className="archived-mode-header">
5127
5138
  <CapIcon
5128
5139
  type="back"
@@ -5205,9 +5216,15 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
5205
5216
  messages={messages}
5206
5217
  isFullMode={isFullMode}
5207
5218
  templateData={creativesParams}
5208
- getCreativesData={() => {
5219
+ getCreativesData={(creativesData) => {
5220
+ // Saved but slidebox stays open: refresh the list without closing.
5221
+ if (creativesData?.keepSlideBoxOpen) {
5222
+ this.getAllTemplates({params: {}}, true);
5223
+ return;
5224
+ }
5209
5225
  this.onCreateComplete(true);
5210
5226
  }}
5227
+ switchToEditAfterCreate={this.canPerform(PERMISSIONS.CREATIVE_EDIT)}
5211
5228
  channel={route.name.toUpperCase()}
5212
5229
  handleCloseCreatives={this.onCreateComplete}
5213
5230
  selectedWeChatAccount={get(
@@ -714,6 +714,10 @@ export default defineMessages({
714
714
  id: `${scope}.unarchiveTemplateSuccess`,
715
715
  defaultMessage: 'Template unarchived successfully',
716
716
  },
717
+ "archiveLimitExceeded": {
718
+ id: `${scope}.archiveLimitExceeded`,
719
+ defaultMessage: 'Archive limit exceeded, it cannot be greater than {limit}.',
720
+ },
717
721
  "bulkArchiveSuccess": {
718
722
  id: `${scope}.bulkArchiveSuccess`,
719
723
  defaultMessage: '{count} {count, plural, one {template} other {templates}} archived successfully',
@@ -813,7 +813,38 @@ exports[`Test Templates container Should render temlates when whatsapp templates
813
813
  <Component />
814
814
  <div
815
815
  className="template-listing-header-actions"
816
- />
816
+ >
817
+ <CapDropdownComponent
818
+ overlay={
819
+ <CapMenuComponent>
820
+ <withItemHOC
821
+ onClick={[Function]}
822
+ >
823
+ <FormattedMessage
824
+ defaultMessage="Archived templates"
825
+ id="creatives.containersV2.Templates.archivedTemplates"
826
+ values={Object {}}
827
+ />
828
+ </withItemHOC>
829
+ </CapMenuComponent>
830
+ }
831
+ placement="bottomRight"
832
+ trigger={
833
+ Array [
834
+ "click",
835
+ ]
836
+ }
837
+ >
838
+ <CapButton
839
+ className="template-listing-more-btn"
840
+ type="flat"
841
+ >
842
+ <CapIcon
843
+ type="more"
844
+ />
845
+ </CapButton>
846
+ </CapDropdownComponent>
847
+ </div>
817
848
  </div>
818
849
  <div>
819
850
  <div
@@ -1227,7 +1258,38 @@ exports[`Test Templates container Test max templates exceeded 1`] = `
1227
1258
  <Component />
1228
1259
  <div
1229
1260
  className="template-listing-header-actions"
1230
- />
1261
+ >
1262
+ <CapDropdownComponent
1263
+ overlay={
1264
+ <CapMenuComponent>
1265
+ <withItemHOC
1266
+ onClick={[Function]}
1267
+ >
1268
+ <FormattedMessage
1269
+ defaultMessage="Archived templates"
1270
+ id="creatives.containersV2.Templates.archivedTemplates"
1271
+ values={Object {}}
1272
+ />
1273
+ </withItemHOC>
1274
+ </CapMenuComponent>
1275
+ }
1276
+ placement="bottomRight"
1277
+ trigger={
1278
+ Array [
1279
+ "click",
1280
+ ]
1281
+ }
1282
+ >
1283
+ <CapButton
1284
+ className="template-listing-more-btn"
1285
+ type="flat"
1286
+ >
1287
+ <CapIcon
1288
+ type="more"
1289
+ />
1290
+ </CapButton>
1291
+ </CapDropdownComponent>
1292
+ </div>
1231
1293
  </div>
1232
1294
  <div>
1233
1295
  <div
@@ -1714,7 +1776,38 @@ exports[`Test Templates container Test max templates not exceeded 1`] = `
1714
1776
  <Component />
1715
1777
  <div
1716
1778
  className="template-listing-header-actions"
1717
- />
1779
+ >
1780
+ <CapDropdownComponent
1781
+ overlay={
1782
+ <CapMenuComponent>
1783
+ <withItemHOC
1784
+ onClick={[Function]}
1785
+ >
1786
+ <FormattedMessage
1787
+ defaultMessage="Archived templates"
1788
+ id="creatives.containersV2.Templates.archivedTemplates"
1789
+ values={Object {}}
1790
+ />
1791
+ </withItemHOC>
1792
+ </CapMenuComponent>
1793
+ }
1794
+ placement="bottomRight"
1795
+ trigger={
1796
+ Array [
1797
+ "click",
1798
+ ]
1799
+ }
1800
+ >
1801
+ <CapButton
1802
+ className="template-listing-more-btn"
1803
+ type="flat"
1804
+ >
1805
+ <CapIcon
1806
+ type="more"
1807
+ />
1808
+ </CapButton>
1809
+ </CapDropdownComponent>
1810
+ </div>
1718
1811
  </div>
1719
1812
  <div>
1720
1813
  <div
@@ -2201,7 +2294,38 @@ exports[`Test Templates container Test max templates warning 1`] = `
2201
2294
  <Component />
2202
2295
  <div
2203
2296
  className="template-listing-header-actions"
2204
- />
2297
+ >
2298
+ <CapDropdownComponent
2299
+ overlay={
2300
+ <CapMenuComponent>
2301
+ <withItemHOC
2302
+ onClick={[Function]}
2303
+ >
2304
+ <FormattedMessage
2305
+ defaultMessage="Archived templates"
2306
+ id="creatives.containersV2.Templates.archivedTemplates"
2307
+ values={Object {}}
2308
+ />
2309
+ </withItemHOC>
2310
+ </CapMenuComponent>
2311
+ }
2312
+ placement="bottomRight"
2313
+ trigger={
2314
+ Array [
2315
+ "click",
2316
+ ]
2317
+ }
2318
+ >
2319
+ <CapButton
2320
+ className="template-listing-more-btn"
2321
+ type="flat"
2322
+ >
2323
+ <CapIcon
2324
+ type="more"
2325
+ />
2326
+ </CapButton>
2327
+ </CapDropdownComponent>
2328
+ </div>
2205
2329
  </div>
2206
2330
  <div>
2207
2331
  <div
@@ -0,0 +1,221 @@
1
+ import React from 'react';
2
+ import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
3
+ import { Templates } from '../index';
4
+ import mockdata from '../../mockdata';
5
+ import { DEFAULT_ARCHIVAL_LIMIT } from '../constants';
6
+ import { CREATIVE_CREATE, CREATIVE_DELETE, CREATIVE_EDIT } from '../../App/constants';
7
+
8
+ const { whatsappTemplates: TemplatesProp } = mockdata;
9
+
10
+ jest.mock('@capillarytech/cap-ui-library', () => ({
11
+ ...jest.requireActual('@capillarytech/cap-ui-library'),
12
+ CapNotification: { error: jest.fn(), success: jest.fn(), warning: jest.fn() },
13
+ }));
14
+
15
+ jest.mock('../../CreativesContainer', () => ({
16
+ __esModule: true,
17
+ default: () => null,
18
+ }));
19
+
20
+ const buildTemplateIds = count => Array.from({ length: count }, (_, index) => `template_${index}`);
21
+
22
+ // Walks a rendered element/plain-object tree (cards are never mounted) for a node by className.
23
+ const findByClassName = (node, className, seen = new Set()) => {
24
+ if (!node || typeof node !== 'object' || seen.has(node)) return null;
25
+ seen.add(node);
26
+ if (Array.isArray(node)) {
27
+ return node.reduce((found, child) => found || findByClassName(child, className, seen), null);
28
+ }
29
+ if (node.props && typeof node.props.className === 'string' && node.props.className.includes(className)) {
30
+ return node;
31
+ }
32
+ return Object.values(node.props || node).reduce(
33
+ (found, value) => found || findByClassName(value, className, seen),
34
+ null,
35
+ );
36
+ };
37
+
38
+ describe('Templates archival (enabled by default)', () => {
39
+ const actions = {
40
+ getWeCrmAccounts: jest.fn(),
41
+ setChannelAccount: jest.fn(),
42
+ getAllTemplates: jest.fn(),
43
+ getUserList: jest.fn(),
44
+ getSenderDetails: jest.fn(),
45
+ resetTemplate: jest.fn(),
46
+ setArchivedMode: jest.fn(),
47
+ clearTemplateSelection: jest.fn(),
48
+ toggleTemplateSelection: jest.fn(),
49
+ bulkArchiveTemplates: jest.fn(),
50
+ bulkUnarchiveTemplates: jest.fn(),
51
+ archiveTemplate: jest.fn(),
52
+ unarchiveTemplate: jest.fn(),
53
+ };
54
+ let renderedComponent;
55
+
56
+ const renderTemplates = ({ channel = 'email', isFullMode = true, templatesProps = {} } = {}) => {
57
+ renderedComponent = shallowWithIntl(
58
+ <Templates
59
+ route={{ name: channel }}
60
+ cap={{ user: { accessiblePermissions: [CREATIVE_CREATE, CREATIVE_EDIT, CREATIVE_DELETE] } }}
61
+ Templates={{ ...TemplatesProp, ...templatesProps }}
62
+ actions={actions}
63
+ location={{ pathname: `/${channel}`, query: {}, search: '' }}
64
+ EmailCreate={{ duplicateTemplateInProgress: false }}
65
+ isFullMode={isFullMode}
66
+ intl={{ formatMessage: ({ defaultMessage }) => defaultMessage }}
67
+ />,
68
+ );
69
+ return renderedComponent;
70
+ };
71
+
72
+ const getCardList = (templates, channel = 'email') =>
73
+ renderedComponent.instance().getTemplateDataForGrid({
74
+ templates,
75
+ handlers: { handlePreviewClick: jest.fn(), handleEditClick: jest.fn() },
76
+ filterContent: null,
77
+ channel,
78
+ isLoading: false,
79
+ isInitialLoading: false,
80
+ loadingTip: '',
81
+ previewTemplateId: null,
82
+ });
83
+
84
+ beforeEach(() => {
85
+ jest.clearAllMocks();
86
+ });
87
+
88
+ describe('bulk selection bar', () => {
89
+ const getBulkArchiveButton = () =>
90
+ renderedComponent
91
+ .find('.bulk-selection-bar')
92
+ .find('.archive-template-button');
93
+
94
+ it('renders without any feature flag once templates are selected', () => {
95
+ renderTemplates({ templatesProps: { selectedTemplateIds: buildTemplateIds(2) } });
96
+ expect(renderedComponent.find('.bulk-selection-bar')).toHaveLength(1);
97
+ expect(getBulkArchiveButton().prop('disabled')).toBe(false);
98
+ });
99
+
100
+ it('is not rendered when nothing is selected', () => {
101
+ renderTemplates({ templatesProps: { selectedTemplateIds: [] } });
102
+ expect(renderedComponent.find('.bulk-selection-bar')).toHaveLength(0);
103
+ });
104
+
105
+ it('is not rendered outside full mode', () => {
106
+ renderTemplates({ isFullMode: false, templatesProps: { selectedTemplateIds: buildTemplateIds(2) } });
107
+ expect(renderedComponent.find('.bulk-selection-bar')).toHaveLength(0);
108
+ });
109
+
110
+ it('keeps the action enabled at exactly the archival limit', () => {
111
+ renderTemplates({ templatesProps: { selectedTemplateIds: buildTemplateIds(DEFAULT_ARCHIVAL_LIMIT) } });
112
+ expect(getBulkArchiveButton().prop('disabled')).toBe(false);
113
+ });
114
+
115
+ it('disables the action and shows the limit message above the archival limit', () => {
116
+ renderTemplates({ templatesProps: { selectedTemplateIds: buildTemplateIds(DEFAULT_ARCHIVAL_LIMIT + 1) } });
117
+ expect(getBulkArchiveButton().prop('disabled')).toBe(true);
118
+ const tooltip = renderedComponent.find('.bulk-selection-bar').find('CapTooltip').first();
119
+ expect(tooltip.prop('title')).toContain('Archive limit exceeded');
120
+ expect(tooltip.prop('title')).toContain(String(DEFAULT_ARCHIVAL_LIMIT));
121
+ });
122
+
123
+ it('triggers bulk unarchive for the selection in archived mode', () => {
124
+ const selectedIds = buildTemplateIds(2);
125
+ renderTemplates({ templatesProps: { selectedTemplateIds: selectedIds, isArchivedMode: true } });
126
+ const handleBulkArchiveAction = jest
127
+ .spyOn(renderedComponent.instance(), 'handleBulkArchiveAction')
128
+ .mockImplementation(() => {});
129
+ getBulkArchiveButton().prop('onClick')();
130
+ expect(handleBulkArchiveAction).toHaveBeenCalledWith({ ids: selectedIds, count: 2, isUnarchive: true });
131
+ });
132
+ });
133
+
134
+ describe('handleBulkArchiveAction', () => {
135
+ it('opens the confirm modal and bulk archives the selected ids on confirm', () => {
136
+ renderTemplates();
137
+ const instance = renderedComponent.instance();
138
+ const showArchiveConfirm = jest.spyOn(instance, 'showArchiveConfirm').mockImplementation(() => {});
139
+ instance.handleBulkArchiveAction({ ids: ['a', 'b'], count: 2 });
140
+ expect(showArchiveConfirm).toHaveBeenCalledWith(expect.objectContaining({ count: 2, isUnarchive: false }));
141
+ showArchiveConfirm.mock.calls[0][0].onConfirm();
142
+ expect(actions.bulkArchiveTemplates).toHaveBeenCalledWith('Email', ['a', 'b'], expect.any(Function));
143
+ expect(actions.bulkUnarchiveTemplates).not.toHaveBeenCalled();
144
+ });
145
+
146
+ it('bulk unarchives when isUnarchive is set', () => {
147
+ renderTemplates();
148
+ const instance = renderedComponent.instance();
149
+ const showArchiveConfirm = jest.spyOn(instance, 'showArchiveConfirm').mockImplementation(() => {});
150
+ instance.handleBulkArchiveAction({ ids: ['a'], count: 1, isUnarchive: true });
151
+ showArchiveConfirm.mock.calls[0][0].onConfirm();
152
+ expect(actions.bulkUnarchiveTemplates).toHaveBeenCalledWith('Email', ['a'], expect.any(Function));
153
+ expect(actions.bulkArchiveTemplates).not.toHaveBeenCalled();
154
+ });
155
+ });
156
+
157
+ describe('single template card', () => {
158
+ const activeTemplate = { _id: 'tpl_1', name: 'Active template', versions: { base: {} } };
159
+ const archivedTemplate = { _id: 'tpl_2', name: 'Archived template', isArchived: true, versions: { base: {} } };
160
+
161
+ it('shows the archive menu action for an active template without any feature flag', () => {
162
+ renderTemplates();
163
+ const cardList = getCardList([activeTemplate]);
164
+ expect(findByClassName(cardList, 'archive-email')).not.toBeNull();
165
+ expect(findByClassName(cardList, 'unarchive-email')).toBeNull();
166
+ });
167
+
168
+ it('shows the unarchive menu action for an archived template', () => {
169
+ renderTemplates();
170
+ const cardList = getCardList([archivedTemplate]);
171
+ expect(findByClassName(cardList, 'unarchive-email')).not.toBeNull();
172
+ });
173
+
174
+ it('shows the archived tag and unarchive popover action for an archived template', () => {
175
+ renderTemplates({ channel: 'sms' });
176
+ const cardList = renderedComponent.instance().populateTemplatesList([archivedTemplate], false, false);
177
+ expect(findByClassName(cardList, 'archived-tag')).not.toBeNull();
178
+ expect(findByClassName(cardList, 'popover-archive-action')).not.toBeNull();
179
+ });
180
+
181
+ it('shows no archived tag for an active template', () => {
182
+ renderTemplates({ channel: 'sms' });
183
+ const cardList = renderedComponent.instance().populateTemplatesList([activeTemplate], false, false);
184
+ expect(findByClassName(cardList, 'archived-tag')).toBeNull();
185
+ expect(findByClassName(cardList, 'popover-archive-action')).not.toBeNull();
186
+ });
187
+
188
+ it('hides the archive actions outside full mode', () => {
189
+ renderTemplates({ isFullMode: false });
190
+ const cardList = getCardList([activeTemplate]);
191
+ expect(findByClassName(cardList, 'archive-email')).toBeNull();
192
+ });
193
+
194
+ it('archives a single template through the confirm modal', () => {
195
+ renderTemplates();
196
+ const instance = renderedComponent.instance();
197
+ const showArchiveConfirm = jest.spyOn(instance, 'showArchiveConfirm').mockImplementation(() => {});
198
+ instance.handleTemplateArchiveAction({ templateId: 'tpl_1', templateName: 'Active template' });
199
+ expect(showArchiveConfirm).toHaveBeenCalledTimes(1);
200
+ showArchiveConfirm.mock.calls[0][0].onConfirm();
201
+ expect(actions.archiveTemplate).toHaveBeenCalled();
202
+ });
203
+
204
+ it('unarchives a single template through the confirm modal', () => {
205
+ renderTemplates();
206
+ const instance = renderedComponent.instance();
207
+ const showArchiveConfirm = jest.spyOn(instance, 'showArchiveConfirm').mockImplementation(() => {});
208
+ instance.handleTemplateArchiveAction({ templateId: 'tpl_2', templateName: 'Archived template', isUnarchive: true });
209
+ showArchiveConfirm.mock.calls[0][0].onConfirm();
210
+ expect(actions.unarchiveTemplate).toHaveBeenCalled();
211
+ });
212
+ });
213
+
214
+ describe('isChannelArchiveEligible', () => {
215
+ it('does not allow archiving Zalo templates', () => {
216
+ renderTemplates();
217
+ expect(renderedComponent.instance().isChannelArchiveEligible('ZALO')).toBe(false);
218
+ expect(renderedComponent.instance().isChannelArchiveEligible('SMS')).toBe(true);
219
+ });
220
+ });
221
+ });