@capillarytech/creatives-library 8.0.348 → 8.0.349

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 (38) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +20 -0
  3. package/services/tests/api.test.js +59 -0
  4. package/utils/tests/v2Common.test.js +46 -1
  5. package/utils/v2common.js +18 -0
  6. package/v2Components/CapCustomSkeleton/index.js +1 -1
  7. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +6 -18
  9. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +0 -27
  10. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WhatsAppPreviewContent.test.js +0 -48
  11. package/v2Components/TemplatePreview/_templatePreview.scss +0 -21
  12. package/v2Components/TemplatePreview/index.js +6 -18
  13. package/v2Components/TemplatePreview/tests/__snapshots__/index.test.js.snap +0 -1
  14. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +9 -0
  15. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
  16. package/v2Containers/CreativesContainer/index.js +6 -9
  17. package/v2Containers/CreativesContainer/messages.js +4 -0
  18. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +3 -0
  19. package/v2Containers/Templates/ChannelTypeIllustration.js +23 -6
  20. package/v2Containers/Templates/_templates.scss +179 -24
  21. package/v2Containers/Templates/actions.js +44 -0
  22. package/v2Containers/Templates/constants.js +31 -0
  23. package/v2Containers/Templates/index.js +361 -60
  24. package/v2Containers/Templates/messages.js +96 -0
  25. package/v2Containers/Templates/reducer.js +84 -1
  26. package/v2Containers/Templates/sagas.js +64 -0
  27. package/v2Containers/Templates/selectors.js +12 -0
  28. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +12 -0
  29. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1300 -1122
  30. package/v2Containers/Templates/tests/index.test.js +6 -0
  31. package/v2Containers/Templates/tests/reducer.test.js +178 -0
  32. package/v2Containers/Templates/tests/sagas.test.js +390 -8
  33. package/v2Containers/Templates/tests/selector.test.js +32 -0
  34. package/v2Containers/TemplatesV2/TemplatesV2.style.js +1 -1
  35. package/v2Containers/Whatsapp/constants.js +0 -8
  36. package/v2Containers/Whatsapp/index.js +5 -142
  37. package/v2Containers/Whatsapp/index.scss +0 -8
  38. package/v2Containers/Whatsapp/messages.js +0 -16
@@ -50,9 +50,7 @@ import {
50
50
  CapStatus,
51
51
  CapColoredTag,
52
52
  CapSpin,
53
- CapCard,
54
- CapColumn,
55
- CapCarousel
53
+ CapCheckbox,
56
54
  } from "@capillarytech/cap-ui-library";
57
55
  import { makeSelectTemplates, makeSelectTemplatesResponse } from './selectors';
58
56
  import { makeSelectCreate as makeSelectCreateSms } from '../Sms/Create/selectors';
@@ -83,7 +81,7 @@ import * as webpushActions from '../WebPush/actions';
83
81
  import * as globalActions from '../Cap/actions';
84
82
  import { makeSelectAuthenticated } from '../Cap/selectors';
85
83
  import { UserIsAuthenticated } from '../../utils/authWrapper';
86
- import { getObjFromQueryParams } from '../../utils/v2common';
84
+ import { getObjFromQueryParams, buildTemplateNameDescription } from '../../utils/v2common';
87
85
  import messages from './messages';
88
86
  import {checkUnicode} from '../../utils/smsCharCountV2';
89
87
  import { containsBase64Images } from '../../utils/content';
@@ -107,8 +105,8 @@ import {
107
105
  FACEBOOK as FACEBOOK_CHANNEL,
108
106
  CREATE,
109
107
  } from '../App/constants';
110
- import {MAX_WHATSAPP_TEMPLATES, WARNING_WHATSAPP_TEMPLATES , ACCOUNT_MAPPING_ON_CHANNEL, noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc, noApprovedWhatsappZaloTemplatesTitle, noApprovedWhatsappTemplatesDesc, zaloDescIllustration, noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc} from './constants';
111
- import { COPY_OF } from '../../constants/unified';
108
+ 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';
109
+ import { COPY_OF, EMBEDDED } from '../../constants/unified';
112
110
  import {
113
111
  STATUS_OPTIONS,
114
112
  CATEGORY,
@@ -116,19 +114,11 @@ import {
116
114
  STATUS as WHATSAPP_STATUS,
117
115
  WHATSAPP_STATUSES,
118
116
  HOST_GUPSHUP,
119
- HOST_HAPTIC,
120
117
  CATEGORY_OPTIONS_MAP,
121
- HOST_TWILIO,
122
- TWILIO_CATEGORY_OPTIONS,
123
- KARIX_GUPSHUP_CATEGORY_OPTIONS,
124
- ICS_CATEGORY_OPTIONS,
125
- HAPTIC_CATEGORY_OPTIONS,
126
- HOST_ICS,
127
118
  IMAGE,
128
119
  VIDEO,
129
- GIF,
130
120
  } from '../Whatsapp/constants';
131
- import { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES, INAPP_MEDIA_TYPES, BIG_HTML, ANDROID, IOS } from '../InApp/constants';
121
+ import { INAPP_LAYOUT_DETAILS, INAPP_MESSAGE_LAYOUT_TYPES } from '../InApp/constants';
132
122
  import { ZALO_STATUS_OPTIONS, ZALO_STATUSES } from '../Zalo/constants';
133
123
  import { getWhatsappContent, getWhatsappStatus, getWhatsappCategory, getWhatsappCta, getWhatsappQuickReply, getWhatsappAutoFill, getWhatsappCarouselButtonView } from '../Whatsapp/utils';
134
124
  import { getRCSContent } from '../Rcs/utils';
@@ -145,7 +135,7 @@ import {CREATIVE} from '../Facebook/constants';
145
135
  import videoPlay from '../../assets/videoPlay.svg';
146
136
  import whatsappImageEmptyPreview from '../../v2Components/TemplatePreview/assets/images/empty_image_preview.svg';
147
137
  import whatsappVideoEmptyPreview from '../../v2Components/TemplatePreview/assets/images/empty_video_preview.svg';
148
- import { CAP_SPACE_16 } from '@capillarytech/cap-ui-library/styled/variables';
138
+ import { CAP_SPACE_16, CAP_G08, CAP_G05, CAP_SPACE_08, CAP_SPACE_12 } from '@capillarytech/cap-ui-library/styled/variables';
149
139
  import { GA } from '@capillarytech/cap-ui-utils';
150
140
  import { MAPP_SDK } from '../InApp/constants';
151
141
  import injectReducer from '../../utils/injectReducer';
@@ -154,7 +144,6 @@ import { compose } from 'redux';
154
144
  import { v2TemplateSaga } from './sagas';
155
145
  import injectSaga from '../../utils/injectSaga';
156
146
  import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
157
- import { Rcs } from '../Rcs';
158
147
  import { makeSelectRcs } from '../Rcs/selectors';
159
148
  import { getRcsStatusType } from '../Rcs/utils';
160
149
  import { makeSelectWebPush } from '../WebPush/selectors';
@@ -437,6 +426,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
437
426
  channel = '';
438
427
  }
439
428
  this.setState({ channel, activeMode });
429
+ // Always reset archive mode and selection when mounting a new channel
430
+ this.props.actions.setArchivedMode(false);
431
+ this.props.actions.clearTemplateSelection();
440
432
  // Clear templates when entering account selection mode to prevent showing old channel templates
441
433
  if (activeMode === ACCOUNT_SELECTION_MODE) {
442
434
  this.props.actions.resetTemplate();
@@ -827,6 +819,39 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
827
819
  this.getAllTemplates({params, resetPage: true});
828
820
  }
829
821
 
822
+ // Archive / Unarchive / Bulk Archive / Bulk Unarchive — detect completion and refresh listing
823
+ const wasArchiving = this.props.Templates.archiveInProgress;
824
+ const isArchiveDone = !nextProps.Templates.archiveInProgress && wasArchiving;
825
+ if (isArchiveDone && !nextProps.Templates.archiveError) {
826
+ const { successMessage, description } = nextProps.Templates.archiveSuccessPayload || {};
827
+ if (successMessage) CapNotification.success({ message: successMessage, description });
828
+ this.getAllTemplates({ params, resetPage: true });
829
+ }
830
+
831
+ const wasUnarchiving = this.props.Templates.unarchiveInProgress;
832
+ const isUnarchiveDone = !nextProps.Templates.unarchiveInProgress && wasUnarchiving;
833
+ if (isUnarchiveDone && !nextProps.Templates.unarchiveError) {
834
+ const { successMessage, description } = nextProps.Templates.unarchiveSuccessPayload || {};
835
+ if (successMessage) CapNotification.success({ message: successMessage, description });
836
+ this.getAllTemplates({ params, resetPage: true });
837
+ }
838
+
839
+ const wasBulkArchiving = this.props.Templates.bulkArchiveInProgress;
840
+ const isBulkArchiveDone = !nextProps.Templates.bulkArchiveInProgress && wasBulkArchiving;
841
+ if (isBulkArchiveDone && !nextProps.Templates.bulkArchiveError) {
842
+ const { count } = nextProps.Templates.bulkArchiveSuccessPayload || {};
843
+ CapNotification.success({ message: this.props.intl.formatMessage(messages.bulkArchiveSuccess, { count }) });
844
+ this.getAllTemplates({ params, resetPage: true });
845
+ }
846
+
847
+ const wasBulkUnarchiving = this.props.Templates.bulkUnarchiveInProgress;
848
+ const isBulkUnarchiveDone = !nextProps.Templates.bulkUnarchiveInProgress && wasBulkUnarchiving;
849
+ if (isBulkUnarchiveDone && !nextProps.Templates.bulkUnarchiveError) {
850
+ const { count } = nextProps.Templates.bulkUnarchiveSuccessPayload || {};
851
+ CapNotification.success({ message: this.props.intl.formatMessage(messages.bulkUnarchiveSuccess, { count }) });
852
+ this.getAllTemplates({ params, resetPage: true });
853
+ }
854
+
830
855
  if (!nextProps.Templates.sendingFile && !isEqual(this.props.Templates.sendingFile, nextProps.Templates.sendingFile) && !nextProps.Templates.errorSendingFile) {
831
856
  const module = this.props.location.query.module ? this.props.location.query.module : 'default';
832
857
  const isLanguageSupport = (this.props.location.query.isLanguageSupport) ? this.props.location.query.isLanguageSupport : true;
@@ -1595,6 +1620,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1595
1620
  let queryParams = params || {};
1596
1621
  let page = this.state.page;
1597
1622
  const { activeMode } = this.state;
1623
+ // Archive filter — use explicit param if provided (props may not have updated yet due to async dispatch)
1624
+ if (!queryParams.archiveStatus) {
1625
+ const archiveFilter = get(this.props, 'Templates.archiveFilter', 'active');
1626
+ queryParams.archiveStatus = archiveFilter;
1627
+ }
1598
1628
  if (activeMode === ACCOUNT_SELECTION_MODE) {
1599
1629
  this.setTemplatesMode();
1600
1630
  }
@@ -1861,6 +1891,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1861
1891
  const currentChannel = channel.toUpperCase();
1862
1892
  const {channel: stateChannel} = this.state;
1863
1893
  const channelLowerCase = stateChannel.toLowerCase();
1894
+ const _selectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
1895
+ const _selectedIdsArray = _selectedIds && typeof _selectedIds.toJS === 'function' ? _selectedIds.toJS() : (Array.isArray(_selectedIds) ? _selectedIds : []);
1896
+ const hasSelection = this.props.isFullMode && _selectedIdsArray.length > 0;
1864
1897
  let filteredTemplates = templates;
1865
1898
  let isTraiDltFeature = false;
1866
1899
  switch (currentChannel) {
@@ -1900,47 +1933,58 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1900
1933
  const iosBodyType = get(template, 'versions.base.content.IOS.bodyType');
1901
1934
  const inappBodyType = androidBodyType || iosBodyType;
1902
1935
  const isZaloPreviewLoading = previewTemplateId === template?._id;
1936
+ const selectedIdsForCard = get(this.props, 'Templates.selectedTemplateIds', []);
1937
+ const selectedIdsArrayForCard = selectedIdsForCard.toJS ? selectedIdsForCard.toJS() : selectedIdsForCard;
1938
+ // Archive eligibility per template: Zalo never; WhatsApp/RCS not when pending/awaiting
1939
+ const cardWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
1940
+ const cardRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
1941
+ const isCardArchiveEligible = this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
1942
+ const isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
1943
+ const isAnyArchiveInProgress = !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
1903
1944
  const templateData = {
1904
1945
  key: `${currentChannel}-card-${template?.name}`,
1905
1946
  title: (
1906
- <span title={template?.name}>
1907
- {template?.name}
1908
- {currentChannel === INAPP && (
1909
- <CapRow>
1910
- <CapColoredTag
1911
- tagColor={INAPP_LAYOUT_DETAILS[inappBodyType]?.tagColor}
1912
- tagTextColor={
1913
- INAPP_LAYOUT_DETAILS[inappBodyType]?.tagTextColor
1914
- }
1915
- tagHeight="1.25rem"
1916
- tagFontSize="12px"
1917
- >
1918
- {INAPP_LAYOUT_DETAILS[inappBodyType]?.text}
1919
- </CapColoredTag>
1920
- </CapRow>
1921
- )}
1947
+ <span className="template-card-title">
1948
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
1949
+ <CapLabel.CapLabelInline type="label1" title={template?.name} className="template-card-name">
1950
+ {template?.name}
1951
+ {currentChannel === INAPP && (
1952
+ <CapRow>
1953
+ <CapColoredTag
1954
+ tagColor={INAPP_LAYOUT_DETAILS[inappBodyType]?.tagColor}
1955
+ tagTextColor={
1956
+ INAPP_LAYOUT_DETAILS[inappBodyType]?.tagTextColor
1957
+ }
1958
+ tagHeight="1.25rem"
1959
+ tagFontSize="0.857rem"
1960
+ >
1961
+ {INAPP_LAYOUT_DETAILS[inappBodyType]?.text}
1962
+ </CapColoredTag>
1963
+ </CapRow>
1964
+ )}
1965
+ </CapLabel.CapLabelInline>
1922
1966
  </span>
1923
1967
  ),
1924
- extra: [
1968
+ extra: isArchivedMode ? [] : [
1925
1969
  // Hide preview icon for channels that support Test and Preview
1926
1970
  // Show preview icon only for channels that don't support Test and Preview
1927
1971
  (() => {
1928
1972
  // Channels that have Test and Preview integrated
1929
1973
  const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO];
1930
1974
  const isTestAndPreviewSupported = testAndPreviewChannels.includes(currentChannel.toUpperCase());
1931
-
1975
+
1932
1976
  // Don't show preview icon if channel supports Test and Preview
1933
1977
  if (isTestAndPreviewSupported) {
1934
1978
  return null;
1935
1979
  }
1936
-
1980
+
1937
1981
  // Show preview icon for other channels (e.g., WeChat, Line, Facebook, Ebill)
1938
1982
  if (currentChannel === ZALO && isZaloPreviewLoading) {
1939
1983
  return (
1940
1984
  <CapSpin style={{ marginRight: "16px", position: "static", display: "inline" }} spinning />
1941
1985
  );
1942
1986
  }
1943
-
1987
+
1944
1988
  return this.getHoverComponent(
1945
1989
  <CapIcon
1946
1990
  className={`view-${channelLowerCase}`}
@@ -1960,7 +2004,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1960
2004
  );
1961
2005
  })()
1962
2006
  ],
1963
- hoverOption: (
2007
+ hoverOption: isArchivedMode ? null : (
1964
2008
  <CapButton
1965
2009
  className={
1966
2010
  this.props.isFullMode
@@ -2000,8 +2044,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2000
2044
  <CapDropdown
2001
2045
  overlay={
2002
2046
  <CapMenu>
2003
- {/* Phase 16: Test and Preview menu item - Show for supported channels */}
2004
- {(this.isTestAndPreviewSupported() ||
2047
+ {/* Phase 16: Test and Preview menu item - Show for supported channels, hide in archived mode */}
2048
+ {!isArchivedMode && (this.isTestAndPreviewSupported() ||
2005
2049
  (this.state.channel.toUpperCase() === WHATSAPP &&
2006
2050
  status === WHATSAPP_STATUSES.approved) || (this.state.channel.toUpperCase() === RCS &&
2007
2051
  rcsStatus === RCS_STATUSES.approved)) && (
@@ -2024,7 +2068,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2024
2068
  {![WECHAT, WHATSAPP, ZALO].includes(
2025
2069
  this.state.channel.toUpperCase()
2026
2070
  ) &&
2027
- !isTraiDltFeature && (
2071
+ !isTraiDltFeature && !template.isArchived && (
2028
2072
  <CapMenu.Item
2029
2073
  className={`duplicate-${channelLowerCase}`}
2030
2074
  onClick={() => this.duplicateTemplate(template)}
@@ -2032,6 +2076,26 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2032
2076
  <FormattedMessage {...messages.duplicateButton} />
2033
2077
  </CapMenu.Item>
2034
2078
  )}
2079
+ {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2080
+ {(() => {
2081
+ const channelUp = this.state.channel.toUpperCase();
2082
+ if (!this.isChannelArchiveEligible(channelUp, status, rcsStatus)) return null;
2083
+ return !template?.isArchived ? (
2084
+ <CapMenu.Item
2085
+ className={`archive-${channelLowerCase}`}
2086
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
2087
+ >
2088
+ <FormattedMessage {...messages.archiveButton} />
2089
+ </CapMenu.Item>
2090
+ ) : (
2091
+ <CapMenu.Item
2092
+ className={`unarchive-${channelLowerCase}`}
2093
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
2094
+ >
2095
+ <FormattedMessage {...messages.unarchiveButton} />
2096
+ </CapMenu.Item>
2097
+ );
2098
+ })()}
2035
2099
  {/* Delete/Unmap menu item */}
2036
2100
  {(!(
2037
2101
  this.state.channel.toUpperCase() === WHATSAPP &&
@@ -2312,7 +2376,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2312
2376
  case WHATSAPP: {
2313
2377
  const { whatsappImageSrc = '', templateMsg, docPreview, whatsappVideoPreviewImg = '', templateHeaderPreview, templateFooterPreview, carouselData = [] } = getWhatsappContent(template);
2314
2378
  templateData.title = (
2315
- <CapRow>
2379
+ <CapRow type="flex" align="middle">
2380
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2316
2381
  <CapLabel className="whatsapp-rcs-template-name">{template?.name}</CapLabel>
2317
2382
  <WhatsappStatusContainer template={template} />
2318
2383
  </CapRow>
@@ -2405,7 +2470,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2405
2470
  const name = get(template, "name", "");
2406
2471
  const statusDisplay=getRcsStatusType(status);
2407
2472
  templateData.title = (
2408
- <CapRow>
2473
+ <CapRow type="flex" align="middle">
2474
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2409
2475
  <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2410
2476
  <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2411
2477
  <CapStatus
@@ -2529,7 +2595,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2529
2595
 
2530
2596
  //no templates available
2531
2597
  const showIllustrationForChannel = (forChannel) => {
2532
- return forChannel === channelLowerCase && isEmpty(templates) && isEmpty(this.state.searchText) && !isLoading;
2598
+ return forChannel === channelLowerCase && isEmpty(templates) && isEmpty(this.state.searchText) && !isLoading && !get(this.props, 'Templates.isArchivedMode', false);
2533
2599
  }
2534
2600
  //when filters applied not matching templates
2535
2601
  const filteredEmptyAndFullModeAs = (fullModeValue) => {
@@ -2547,6 +2613,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2547
2613
  const accountId = get(this.props, 'Templates.selectedWeChatAccount.uuid');
2548
2614
  const accounts = get(this.props, 'Templates.weCrmAccounts');
2549
2615
  const getAllTemplatesInProgress = get(this.props, 'Templates.getAllTemplatesInProgress');
2616
+ const archiveListingRefreshType = get(this.props, 'Templates.archiveListingRefreshType', null);
2617
+ const isArchiveOperationInProgress = get(this.props, 'Templates.archiveInProgress', false)
2618
+ || get(this.props, 'Templates.unarchiveInProgress', false)
2619
+ || get(this.props, 'Templates.bulkArchiveInProgress', false)
2620
+ || get(this.props, 'Templates.bulkUnarchiveInProgress', false);
2550
2621
 
2551
2622
  const noWhatsappZaloTemplates = this.isFullMode() && isEmpty(templates) || !this.state.hostName;
2552
2623
  const noApprovedWhatsappZaloTemplates = filteredEmptyAndFullModeAs(false) && !isEmpty(this.state?.hostName);
@@ -2555,9 +2626,36 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2555
2626
 
2556
2627
  const noLoaderAndSearchText = isEmpty(this.state.searchText) && !isLoading;
2557
2628
 
2558
- return (<div>
2629
+ const isArchivedModeLocal = get(this.props, 'Templates.isArchivedMode', false);
2630
+ const selectedIdsLocal = get(this.props, 'Templates.selectedTemplateIds', []);
2631
+ const selectedIdsArrayLocal = selectedIdsLocal && typeof selectedIdsLocal.toJS === 'function' ? selectedIdsLocal.toJS() : (Array.isArray(selectedIdsLocal) ? selectedIdsLocal : []);
2632
+ const selectedCountLocal = selectedIdsArrayLocal.length;
2633
+ const hasSelectionLocal = this.props.isFullMode && selectedCountLocal > 0;
2634
+
2635
+ return (<div>
2559
2636
  {[WECHAT, MOBILE_PUSH, WEBPUSH, INAPP, WHATSAPP, ZALO, RCS].includes(currentChannel) && this.showAccountName()}
2560
- {filterContent}
2637
+ <CapRow type="flex" align="middle" justify="space-between" className="filter-row">
2638
+ <div className="filter-row-content">{filterContent}</div>
2639
+ {hasSelectionLocal && (
2640
+ <CapRow type="flex" align="middle" className="bulk-selection-bar">
2641
+ <CapLabel type="label2">
2642
+ {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2643
+ </CapLabel>
2644
+ <CapButton
2645
+ type="primary"
2646
+ prefix={<CapIcon type="archive" size="l" />}
2647
+ onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2648
+ >
2649
+ <span className="archive-btn-label">
2650
+ <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2651
+ </span>
2652
+ </CapButton>
2653
+ <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2654
+ <FormattedMessage {...messages.archiveConfirmCancel} />
2655
+ </CapButton>
2656
+ </CapRow>
2657
+ )}
2658
+ </CapRow>
2561
2659
  {[WHATSAPP, ZALO, INAPP,RCS].includes(currentChannel) && this.selectedFilters()}
2562
2660
  {<div>
2563
2661
  {!isEmpty(filteredTemplates) || !isEmpty(this.state.searchText) || !isEmpty(this.props.Templates.templateError) ? (
@@ -2570,7 +2668,7 @@ return (<div>
2570
2668
  fbType={"list"}
2571
2669
  />
2572
2670
  </div>)
2573
- : [SMS_LOWERCASE, EMAIL_LOWERCASE].includes(this.state.channel.toLowerCase()) && !isLoading && this.getSmsEmailIllustration()
2671
+ : [SMS_LOWERCASE, EMAIL_LOWERCASE].includes(this.state.channel.toLowerCase()) && !isLoading && !get(this.props, 'Templates.isArchivedMode', false) && this.getSmsEmailIllustration()
2574
2672
  }
2575
2673
 
2576
2674
  {(this.state.selectedAccount === '' && isEmpty(this.props.Templates.selectedWeChatAccount)) && ([WECHAT_LOWERCASE, MOBILE_PUSH_LOWERCASE, INAPP_LOWERCASE].includes(this.state.channel.toLowerCase())) &&
@@ -2594,7 +2692,7 @@ return (<div>
2594
2692
  </div>
2595
2693
  )
2596
2694
  }
2597
- {(showWhatsappIllustration || showZaloIllustration) && (
2695
+ {(showWhatsappIllustration || showZaloIllustration) && !get(this.props, 'Templates.isArchivedMode', false) && (
2598
2696
  noLoaderAndSearchText &&
2599
2697
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2600
2698
  {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
@@ -2635,8 +2733,27 @@ return (<div>
2635
2733
  <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2636
2734
  </div>
2637
2735
  }
2736
+ {get(this.props, 'Templates.isArchivedMode', false) && isEmpty(templates) && !isLoading && !getAllTemplatesInProgress && isEmpty(this.state.searchText) && (
2737
+ <CapRow className={this.isFullMode() ? 'illustration-scroll-wrapper' : ''}>
2738
+ <ChannelTypeIllustration
2739
+ isFullMode={this.props.isFullMode}
2740
+ createTemplate={this.createTemplate}
2741
+ currentChannel={currentChannel}
2742
+ isArchivedMode
2743
+ />
2744
+ </CapRow>
2745
+ )}
2638
2746
  {<CapCustomSkeleton loader={isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
2639
- {<CapPageSpinner spinning={!isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
2747
+ {!isInitialLoading && getAllTemplatesInProgress && archiveListingRefreshType ? (
2748
+ <div className="archive-listing-spinner">
2749
+ <CapSpin spinning />
2750
+ <CapLabel.CapLabelInline type="label1">
2751
+ {archiveListingRefreshType === ARCHIVE_REFRESH_TYPE_ARCHIVE ? this.props.intl.formatMessage(messages.archivalInProgress) : this.props.intl.formatMessage(messages.unarchivalInProgress)}
2752
+ </CapLabel.CapLabelInline>
2753
+ </div>
2754
+ ) : (
2755
+ <CapPageSpinner spinning={!isInitialLoading && (isLoading || getAllTemplatesInProgress)} />
2756
+ )}
2640
2757
  </div>
2641
2758
  }
2642
2759
  </div>);
@@ -3278,6 +3395,10 @@ return (<div>
3278
3395
  };
3279
3396
 
3280
3397
  handleEditClick(e, template, modeType, path, options) {
3398
+ if (template && template.isArchived) {
3399
+ CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3400
+ return;
3401
+ }
3281
3402
  if (modeType && modeType !== undefined) {
3282
3403
  this.setState({modeType});
3283
3404
  }
@@ -3459,6 +3580,87 @@ return (<div>
3459
3580
  this.setState({showModal: false});
3460
3581
  }
3461
3582
 
3583
+ renderCardSelectionCheckbox = ({ templateId, selectedIds, isDisabled }) => {
3584
+ if (!this.props.isFullMode || this.props.location.query.type === EMBEDDED) return null;
3585
+ return (
3586
+ <CapCheckbox
3587
+ checked={selectedIds.includes(templateId)}
3588
+ onChange={() => !isDisabled && this.props.actions.toggleTemplateSelection(templateId)}
3589
+ onClick={(e) => e.stopPropagation()}
3590
+ disabled={isDisabled}
3591
+ />
3592
+ );
3593
+ }
3594
+
3595
+ handleBulkArchiveAction = ({ ids, count, isUnarchive = false }) => {
3596
+ const { intl, actions } = this.props;
3597
+ const { channel } = this.state;
3598
+ const title = isUnarchive
3599
+ ? intl.formatMessage(messages.unarchiveTemplates)
3600
+ : intl.formatMessage(messages.archiveTemplates);
3601
+ const action = isUnarchive ? actions.bulkUnarchiveTemplates : actions.bulkArchiveTemplates;
3602
+ const successMessage = (c) => intl.formatMessage(
3603
+ isUnarchive ? messages.bulkUnarchiveSuccess : messages.bulkArchiveSuccess,
3604
+ { count: c }
3605
+ );
3606
+ this.showArchiveConfirm({
3607
+ title,
3608
+ count,
3609
+ isUnarchive,
3610
+ onConfirm: () => action(channel, ids, successMessage),
3611
+ });
3612
+ };
3613
+
3614
+ handleTemplateArchiveAction = ({ templateId, templateName, isUnarchive = false }) => {
3615
+ const { intl, actions } = this.props;
3616
+ const { channel } = this.state;
3617
+ const title = isUnarchive
3618
+ ? intl.formatMessage(messages.unarchiveTemplates)
3619
+ : intl.formatMessage(messages.archiveTemplates);
3620
+ const successMessage = isUnarchive
3621
+ ? intl.formatMessage(messages.unarchiveTemplateSuccess)
3622
+ : intl.formatMessage(messages.archiveTemplateSuccess);
3623
+ const action = isUnarchive ? actions.unarchiveTemplate : actions.archiveTemplate;
3624
+ this.showArchiveConfirm({
3625
+ title,
3626
+ count: 1,
3627
+ isUnarchive,
3628
+ onConfirm: () => action(
3629
+ channel,
3630
+ templateId,
3631
+ successMessage,
3632
+ buildTemplateNameDescription(intl.formatMessage(messages.templateNameLabel), templateName)
3633
+ ),
3634
+ });
3635
+ };
3636
+
3637
+ // Shared helper for archive/unarchive confirm modals:
3638
+ // - no icon, lighter overlay, Confirm button on left (primary), Cancel on right
3639
+ showArchiveConfirm = ({ title, content, onConfirm, count = 1, isUnarchive = false }) => {
3640
+ const { intl } = this.props;
3641
+ const confirmText = intl.formatMessage(messages.archiveConfirmOk);
3642
+ const cancelText = intl.formatMessage(messages.archiveConfirmCancel);
3643
+ // Derive content from count if not explicitly provided
3644
+ const resolvedContent = content || (isUnarchive
3645
+ ? intl.formatMessage(count > 1 ? messages.unarchiveTemplateContent : messages.unarchiveTemplateSingleContent)
3646
+ : intl.formatMessage(count > 1 ? messages.archiveTemplateContent : messages.archiveTemplateSingleContent));
3647
+ // AntD v3 footer order is [cancelButton][okButton]. Swap text+handler so
3648
+ // "Confirm" (primary) appears on the left and "Cancel" (default) on the right.
3649
+ CapModal.confirm({
3650
+ title,
3651
+ content: resolvedContent,
3652
+ icon: ' ',
3653
+ className: 'archive-confirm-modal',
3654
+ maskStyle: { backgroundColor: 'rgba(0, 0, 0, 0.25)' },
3655
+ cancelText: confirmText,
3656
+ okText: cancelText,
3657
+ cancelButtonProps: { type: 'primary' },
3658
+ okButtonProps: { type: 'default' },
3659
+ onCancel: (close) => { onConfirm(); close(); },
3660
+ // onOk (the right "Cancel" button) just closes the modal — default behaviour
3661
+ });
3662
+ }
3663
+
3462
3664
  populateTemplatesList = (data, blankTemplateRequired, layoutSelection) => {
3463
3665
  if (!data) {
3464
3666
  return [];
@@ -3608,22 +3810,71 @@ return (<div>
3608
3810
  deleteOption = this.props.intl.formatMessage(messages.unMapButton);
3609
3811
  }
3610
3812
  if (!layoutSelection) {
3813
+ // Determine archive eligibility for this template:
3814
+ // - Zalo: never eligible
3815
+ // - WhatsApp: not eligible when status is awaitingApproval / pending / unsubmitted
3816
+ // - RCS: not eligible when status is awaitingApproval / pending
3817
+ const templateWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
3818
+ const templateRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
3819
+ const channelUpCase = this.state.channel.toUpperCase();
3820
+ const isTemplateArchiveEligible = this.isChannelArchiveEligible(channelUpCase, templateWhatsappStatus, templateRcsStatus);
3821
+
3822
+ // Checkbox on card header (full mode only, only for archive-eligible templates)
3823
+ if (this.props.isFullMode && this.props.location.query.type !== EMBEDDED && isTemplateArchiveEligible) {
3824
+ const selectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
3825
+ const selectedIdsArray = selectedIds.toJS ? selectedIds.toJS() : selectedIds;
3826
+ temp.cardTop = (
3827
+ <CapRow type="flex" align="middle" justify="space-between" className="template-card-top-bar">
3828
+ <CapCheckbox
3829
+ checked={selectedIdsArray.includes(template._id)}
3830
+ onChange={() => this.props.actions.toggleTemplateSelection(template._id)}
3831
+ onClick={(e) => e.stopPropagation()}
3832
+ />
3833
+ </CapRow>
3834
+ );
3835
+ }
3836
+
3611
3837
  temp.footer = (
3612
3838
  <div className="footer-container">
3613
3839
  <div className="card-title">
3614
3840
  <span className="template-name" style={{ fontWeight: `${this.state.channel.toLowerCase() === 'wechat' ? '400' : '600'}` }}>
3615
3841
  { 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>}
3616
3842
  {template?.name}
3843
+ {template.isArchived && <CapColoredTag tagColor={CAP_G08} tagTextColor={CAP_G05} className="archived-tag">{this.props.intl.formatMessage(messages.archivedTag)}</CapColoredTag>}
3617
3844
  </span>
3618
- {this.props.location.query.type !== 'embedded' && <CapPopover
3845
+ {this.props.location.query.type !== EMBEDDED && <CapPopover
3619
3846
  trigger="click"
3620
3847
  content={
3621
3848
  <div className="popover-content">
3622
- {this.state.channel !== 'wechat' && <div className="popover-action-container">
3623
- <span onClick={() => this.duplicateTemplate(template)} className="popover-action" style={{cursor: 'pointer', padding: '8px 0px'}}>{this.props.intl.formatMessage(messages.duplicateButton)}</span>
3849
+ {this.state.channel !== 'wechat' && !template.isArchived && <div className="popover-action-container">
3850
+ <CapButton type="link" onClick={() => this.duplicateTemplate(template)} className="popover-action">
3851
+ {this.props.intl.formatMessage(messages.duplicateButton)}
3852
+ </CapButton>
3853
+ </div>}
3854
+ {this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
3855
+ <CapButton
3856
+ type="link"
3857
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
3858
+ className="popover-action popover-archive-action"
3859
+ >
3860
+ <CapIcon type="archive" size="s" />
3861
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.archiveButton)}</CapLabel.CapLabelInline>
3862
+ </CapButton>
3863
+ </div>}
3864
+ {this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
3865
+ <CapButton
3866
+ type="link"
3867
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
3868
+ className="popover-action popover-archive-action"
3869
+ >
3870
+ <CapIcon type="archive" size="s" />
3871
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.unarchiveButton)}</CapLabel.CapLabelInline>
3872
+ </CapButton>
3624
3873
  </div>}
3625
3874
  <div className="popover-action-container">
3626
- <span onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action" style={{cursor: 'pointer', padding: '8px 0px'}}>{deleteOption}</span>
3875
+ <CapButton type="link" onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action">
3876
+ {deleteOption}
3877
+ </CapButton>
3627
3878
  </div>
3628
3879
  </div>
3629
3880
  }
@@ -3676,7 +3927,20 @@ return (<div>
3676
3927
  return false;
3677
3928
  }
3678
3929
  }
3679
- isFullMode = () => this.props.location.query.type !== "embedded" || this.props.isFullMode
3930
+ isFullMode = () => this.props.location.query.type !== EMBEDDED || this.props.isFullMode
3931
+
3932
+ isChannelArchiveEligible = (channel, whatsappStatus, rcsStatus) => (
3933
+ channel !== ZALO &&
3934
+ !(channel === WHATSAPP && [WHATSAPP_STATUSES.awaitingApproval, WHATSAPP_STATUSES.pending, WHATSAPP_STATUSES.unsubmitted].includes(whatsappStatus)) &&
3935
+ !(channel === RCS && [RCS_STATUSES.awaitingApproval, RCS_STATUSES.pending].includes(rcsStatus))
3936
+ )
3937
+
3938
+ setArchivedMode = (isArchived) => {
3939
+ this.props.actions.setArchivedMode(isArchived);
3940
+ this.setState({ searchText: '', page: 1 }, () => {
3941
+ this.getAllTemplates({ params: { name: '', sortBy: this.state.sortBy, archiveStatus: isArchived ? ARCHIVE_STATUS_ARCHIVED : ARCHIVE_STATUS_ACTIVE }, resetPage: true }, true);
3942
+ });
3943
+ }
3680
3944
  isCreateDisabled = () => {
3681
3945
  let isDisabled = this.isLoading();
3682
3946
  const channel = this.state.channel.toUpperCase();
@@ -4214,11 +4478,16 @@ return (<div>
4214
4478
  if (([WHATSAPP_LOWERCASE, ZALO_LOWERCASE, RCS_LOWERCASE].includes(this.state?.channel?.toLocaleLowerCase()) && isEmpty(this.state?.hostName))) {
4215
4479
  isfilterContentVisisble = false;
4216
4480
  }
4481
+ const _isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
4482
+ const _renderSelectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
4483
+ const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4484
+ const _renderHasSelection = this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4485
+
4217
4486
  const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4218
4487
  {isfilterContentVisisble && <CapInput.Search
4219
4488
  className="search-text"
4220
4489
  style={{width: '210px'}}
4221
- placeholder={this.props.intl.formatMessage(messages.searchText)}
4490
+ placeholder={_isArchivedMode ? this.props.intl.formatMessage(messages.searchArchivedTemplates) : this.props.intl.formatMessage(messages.searchText)}
4222
4491
  value={this.state.searchText}
4223
4492
  onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4224
4493
  disabled={this.checkSearchDisabled()}
@@ -4370,9 +4639,9 @@ return (<div>
4370
4639
  </div>
4371
4640
  )
4372
4641
  }
4373
- <div style={{display: "flex", justifyContent: "space-between", alignItems: 'center'}}>
4374
- {
4375
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded)? (
4642
+ <div className="template-listing-header-actions">
4643
+ {!_isArchivedMode && !_renderHasSelection && (
4644
+ this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4376
4645
  <CapTooltip title={whatsappCountExceedText}>
4377
4646
  <div className="button-disabled-tooltip-wrapper">
4378
4647
  {createButton}
@@ -4380,9 +4649,29 @@ return (<div>
4380
4649
  </CapTooltip>
4381
4650
  )
4382
4651
  : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4383
- }
4652
+ )}
4653
+ {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active */}
4654
+ {!_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4655
+ <CapDropdown
4656
+ trigger={['click']}
4657
+ overlay={
4658
+ <CapMenu>
4659
+ <CapMenu.Item
4660
+ key="archived"
4661
+ onClick={() => this.setArchivedMode(true)}
4662
+ >
4663
+ <FormattedMessage {...messages.archivedTemplates} />
4664
+ </CapMenu.Item>
4665
+ </CapMenu>
4666
+ }
4667
+ placement="bottomRight"
4668
+ >
4669
+ <CapButton type="flat" className="template-listing-more-btn">
4670
+ <CapIcon type="more" />
4671
+ </CapButton>
4672
+ </CapDropdown>
4673
+ )}
4384
4674
  </div>
4385
-
4386
4675
  </div>);
4387
4676
  let htmlPreviewContent = "";
4388
4677
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4431,6 +4720,18 @@ return (<div>
4431
4720
  }
4432
4721
  />
4433
4722
 
4723
+ {/* Archived mode header with back arrow (full mode only) */}
4724
+ {this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
4725
+ <CapRow type="flex" align="middle" className="archived-mode-header">
4726
+ <CapIcon
4727
+ type="back"
4728
+ className="archived-mode-back-icon"
4729
+ onClick={() => this.setArchivedMode(false)}
4730
+ />
4731
+ <CapHeading type="h3"><FormattedMessage {...messages.archivedTemplates} /></CapHeading>
4732
+ </CapRow>
4733
+ )}
4734
+
4434
4735
  {channel.toLowerCase() === WHATSAPP_LOWERCASE &&
4435
4736
  showWhatsappCountWarning ? (
4436
4737
  <CapAlert message={whatsappCountExceedText} type="info" />