@capillarytech/creatives-library 9.0.70 → 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.
@@ -59,7 +59,6 @@ export const EXTENDED_TAG = 'ExtendedTagMessage';
59
59
  export const BADGES_UI_ENABLED = 'BADGES_UI_ENABLED';
60
60
  export const JP_LOCALE_HIDE_FEATURE = 'JP_LOCALE_HIDE_FEATURE';
61
61
  export const ENABLE_WECHAT = 'ENABLE_WECHAT';
62
- export const ENABLE_CREATIVES_ARCHIVAL = 'ENABLE_CREATIVES_ARCHIVAL';
63
62
  export const ENABLE_WEBPUSH = 'ENABLE_WEBPUSH';
64
63
  export const ENABLE_CUSTOMER_BARCODE_TAG = 'ENABLE_CUSTOMER_BARCODE_TAG';
65
64
  export const EMAIL_UNSUBSCRIBE_TAG_MANDATORY = 'EMAIL_UNSUBSCRIBE_TAG_MANDATORY';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.70",
4
+ "version": "9.0.71",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -29,7 +29,6 @@ import {
29
29
  ENABLE_NEW_FORMBUILDER_SMS,
30
30
  ENABLE_NEW_FORMBUILDER_MPUSH,
31
31
  SUPPORT_ENGAGEMENT_MODULE,
32
- ENABLE_CREATIVES_ARCHIVAL,
33
32
  } from '../constants/unified';
34
33
  import { apiMessageFormatHandler } from './commonUtils';
35
34
 
@@ -121,11 +120,6 @@ export const hasWechatFeatureEnabled = Auth.hasFeatureAccess.bind(
121
120
  ENABLE_WECHAT,
122
121
  );
123
122
 
124
- export const hasCreativesArchivalEnabled = Auth.hasFeatureAccess.bind(
125
- null,
126
- ENABLE_CREATIVES_ARCHIVAL,
127
- );
128
-
129
123
  export const hasWebPushFeatureEnabled = Auth.hasFeatureAccess.bind(
130
124
  null,
131
125
  ENABLE_WEBPUSH,
@@ -8,6 +8,7 @@ import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
8
8
  const { match, location, capGlobal, language } = mockData;
9
9
 
10
10
  jest.mock('@capillarytech/cap-ui-utils', () => ({
11
+ getEnvVariable: jest.fn(),
11
12
  GA: {
12
13
  initialize: jest.fn(),
13
14
  timeTracker: {
@@ -320,7 +320,7 @@ export class Creatives extends React.Component {
320
320
  };
321
321
 
322
322
  onEditTemplate = () => {
323
- if (commonUtil.hasCreativesArchivalEnabled() && this.props.templateData?.isArchived) {
323
+ if (this.props.templateData?.isArchived) {
324
324
  CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
325
325
  return;
326
326
  }
@@ -2472,7 +2472,7 @@ export class Creatives extends React.Component {
2472
2472
  onSave={this.saveMessage}
2473
2473
  onDiscard={this.discardMessage}
2474
2474
  onEditTemplate={this.onEditTemplate}
2475
- isTemplateArchived={!!(commonUtil.hasCreativesArchivalEnabled() && this.props.templateData && this.props.templateData.isArchived)}
2475
+ isTemplateArchived={!!(this.props.templateData && this.props.templateData.isArchived)}
2476
2476
  slidBoxContent={slidBoxContent}
2477
2477
  onCreateNextStep={this.onCreateNextStep}
2478
2478
  currentChannel={currentChannel.toUpperCase()}
@@ -95,6 +95,7 @@ export const noApprovedRcsTemplatesTitle='noApprovedRcsTemplatesTitle'
95
95
  export const noApprovedRcsTemplatesDesc='noApprovedRcsTemplatesDesc'
96
96
 
97
97
  // Archive feature
98
+ export const DEFAULT_ARCHIVAL_LIMIT = 25;
98
99
  export const ARCHIVE_TEMPLATE_REQUEST = 'app/v2Containers/Templates/ARCHIVE_TEMPLATE_REQUEST';
99
100
  export const ARCHIVE_TEMPLATE_SUCCESS = 'app/v2Containers/Templates/ARCHIVE_TEMPLATE_SUCCESS';
100
101
  export const ARCHIVE_TEMPLATE_FAILURE = 'app/v2Containers/Templates/ARCHIVE_TEMPLATE_FAILURE';
@@ -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";
@@ -2159,10 +2160,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2159
2160
  // Archive eligibility per template: Zalo never; WhatsApp/RCS not when pending/awaiting
2160
2161
  const cardWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
2161
2162
  const cardRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
2162
- const isArchivalEnabled = commonUtil.hasCreativesArchivalEnabled();
2163
- const isCardArchiveEligible = isArchivalEnabled && this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
2164
- const isArchivedMode = isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
2165
- 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'));
2166
2166
  const templateData = {
2167
2167
  key: `${currentChannel}-card-${template?.name}`,
2168
2168
  title: (
@@ -2307,7 +2307,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2307
2307
  </CapMenu.Item>
2308
2308
  )}
2309
2309
  {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2310
- {commonUtil.hasCreativesArchivalEnabled() && (() => {
2310
+ {(() => {
2311
2311
  const channelUp = this.state.channel.toUpperCase();
2312
2312
  if (!this.isChannelArchiveEligible(channelUp, status, rcsStatus)) return null;
2313
2313
  const canArchive = this.canPerform(PERMISSIONS.CREATIVE_DELETE);
@@ -2928,12 +2928,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2928
2928
 
2929
2929
  const noLoaderAndSearchText = isEmpty(this.state.searchText) && !isLoading;
2930
2930
 
2931
- const isArchivalEnabledLocal = commonUtil.hasCreativesArchivalEnabled();
2932
- const isArchivedModeLocal = isArchivalEnabledLocal && get(this.props, 'Templates.isArchivedMode', false);
2931
+ const isArchivedModeLocal = get(this.props, 'Templates.isArchivedMode', false);
2933
2932
  const selectedIdsLocal = get(this.props, 'Templates.selectedTemplateIds', []);
2934
2933
  const selectedIdsArrayLocal = selectedIdsLocal && typeof selectedIdsLocal.toJS === 'function' ? selectedIdsLocal.toJS() : (Array.isArray(selectedIdsLocal) ? selectedIdsLocal : []);
2935
2934
  const selectedCountLocal = selectedIdsArrayLocal.length;
2936
- const hasSelectionLocal = isArchivalEnabledLocal && this.props.isFullMode && selectedCountLocal > 0;
2935
+ const hasSelectionLocal = this.props.isFullMode && selectedCountLocal > 0;
2936
+ const isArchiveLimitExceeded = selectedCountLocal > ARCHIVAL_LIMIT;
2937
2937
 
2938
2938
  return (<div>
2939
2939
  {[WECHAT, MOBILE_PUSH, WEBPUSH, INAPP, WHATSAPP, ZALO, RCS].includes(currentChannel) && this.showAccountName()}
@@ -2945,16 +2945,21 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2945
2945
  {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2946
2946
  </CapLabel>
2947
2947
  <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
2948
- <CapButton
2949
- type="primary"
2950
- className="archive-template-button"
2951
- prefix={<CapIcon type="archive" size="s" />}
2952
- onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2953
- >
2954
- <span className="archive-btn-label">
2955
- <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>
2956
2961
  </span>
2957
- </CapButton>
2962
+ </CapTooltip>
2958
2963
  </NoPermissionWrapper>
2959
2964
  <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2960
2965
  <FormattedMessage {...messages.archiveConfirmCancel} />
@@ -4195,7 +4200,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4195
4200
  <span className="template-name" style={{ fontWeight: `${this.state.channel.toLowerCase() === 'wechat' ? '400' : '600'}` }}>
4196
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>}
4197
4202
  {template?.name}
4198
- {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>}
4199
4204
  </span>
4200
4205
  {this.props.location.query.type !== EMBEDDED && <CapPopover
4201
4206
  trigger="click"
@@ -4208,7 +4213,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4208
4213
  </CapButton>
4209
4214
  </NoPermissionWrapper>
4210
4215
  </div>}
4211
- {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">
4212
4217
  <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4213
4218
  <CapButton
4214
4219
  type="link"
@@ -4220,7 +4225,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4220
4225
  </CapButton>
4221
4226
  </NoPermissionWrapper>
4222
4227
  </div>}
4223
- {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">
4224
4229
  <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4225
4230
  <CapButton
4226
4231
  type="link"
@@ -4867,11 +4872,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4867
4872
  if (([WHATSAPP_LOWERCASE, ZALO_LOWERCASE, RCS_LOWERCASE].includes(this.state?.channel?.toLocaleLowerCase()) && isEmpty(this.state?.hostName))) {
4868
4873
  isfilterContentVisisble = false;
4869
4874
  }
4870
- const _isArchivalEnabled = commonUtil.hasCreativesArchivalEnabled();
4871
- const _isArchivedMode = _isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
4875
+ const _isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
4872
4876
  const _renderSelectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
4873
4877
  const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4874
- const _renderHasSelection = _isArchivalEnabled && this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4878
+ const _renderHasSelection = this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4875
4879
 
4876
4880
  const useLocalTemplates = this.props.localTemplatesConfig?.useLocalTemplates;
4877
4881
  const builtFilterContent = ((isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && (
@@ -5033,7 +5037,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
5033
5037
  }
5034
5038
  <div className="template-listing-header-actions">
5035
5039
  {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active, archive flag enabled */}
5036
- {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 && (
5037
5041
  <CapDropdown
5038
5042
  trigger={['click']}
5039
5043
  overlay={
@@ -5129,7 +5133,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
5129
5133
  />
5130
5134
 
5131
5135
  {/* Archived mode header with back arrow (full mode only, archive flag enabled) */}
5132
- {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
5136
+ {this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
5133
5137
  <CapRow type="flex" align="middle" className="archived-mode-header">
5134
5138
  <CapIcon
5135
5139
  type="back"
@@ -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
+ });