@capillarytech/creatives-library 8.0.346 → 8.0.347

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 (28) 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/v2Containers/Assets/images/archive_Empty_Illustration.svg +9 -0
  9. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
  10. package/v2Containers/CreativesContainer/index.js +5 -0
  11. package/v2Containers/CreativesContainer/messages.js +4 -0
  12. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +3 -0
  13. package/v2Containers/Templates/ChannelTypeIllustration.js +23 -6
  14. package/v2Containers/Templates/_templates.scss +179 -24
  15. package/v2Containers/Templates/actions.js +44 -0
  16. package/v2Containers/Templates/constants.js +23 -0
  17. package/v2Containers/Templates/index.js +378 -58
  18. package/v2Containers/Templates/messages.js +88 -0
  19. package/v2Containers/Templates/reducer.js +84 -1
  20. package/v2Containers/Templates/sagas.js +64 -0
  21. package/v2Containers/Templates/selectors.js +12 -0
  22. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +12 -0
  23. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1345 -1122
  24. package/v2Containers/Templates/tests/index.test.js +6 -0
  25. package/v2Containers/Templates/tests/reducer.test.js +178 -0
  26. package/v2Containers/Templates/tests/sagas.test.js +390 -8
  27. package/v2Containers/Templates/tests/selector.test.js +32 -0
  28. package/v2Containers/TemplatesV2/TemplatesV2.style.js +1 -1
@@ -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';
@@ -108,7 +106,7 @@ import {
108
106
  CREATE,
109
107
  } from '../App/constants';
110
108
  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';
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,41 @@ 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 { successMessage, count } = nextProps.Templates.bulkArchiveSuccessPayload || {};
843
+ const msg = successMessage ? successMessage(count) : `${count} templates archived successfully`;
844
+ CapNotification.success({ message: msg });
845
+ this.getAllTemplates({ params, resetPage: true });
846
+ }
847
+
848
+ const wasBulkUnarchiving = this.props.Templates.bulkUnarchiveInProgress;
849
+ const isBulkUnarchiveDone = !nextProps.Templates.bulkUnarchiveInProgress && wasBulkUnarchiving;
850
+ if (isBulkUnarchiveDone && !nextProps.Templates.bulkUnarchiveError) {
851
+ const { successMessage, count } = nextProps.Templates.bulkUnarchiveSuccessPayload || {};
852
+ const msg = successMessage ? successMessage(count) : `${count} templates unarchived successfully`;
853
+ CapNotification.success({ message: msg });
854
+ this.getAllTemplates({ params, resetPage: true });
855
+ }
856
+
830
857
  if (!nextProps.Templates.sendingFile && !isEqual(this.props.Templates.sendingFile, nextProps.Templates.sendingFile) && !nextProps.Templates.errorSendingFile) {
831
858
  const module = this.props.location.query.module ? this.props.location.query.module : 'default';
832
859
  const isLanguageSupport = (this.props.location.query.isLanguageSupport) ? this.props.location.query.isLanguageSupport : true;
@@ -1595,6 +1622,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1595
1622
  let queryParams = params || {};
1596
1623
  let page = this.state.page;
1597
1624
  const { activeMode } = this.state;
1625
+ // Archive filter — use explicit param if provided (props may not have updated yet due to async dispatch)
1626
+ if (!queryParams.archiveStatus) {
1627
+ const archiveFilter = get(this.props, 'Templates.archiveFilter', 'active');
1628
+ queryParams.archiveStatus = archiveFilter;
1629
+ }
1598
1630
  if (activeMode === ACCOUNT_SELECTION_MODE) {
1599
1631
  this.setTemplatesMode();
1600
1632
  }
@@ -1861,6 +1893,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1861
1893
  const currentChannel = channel.toUpperCase();
1862
1894
  const {channel: stateChannel} = this.state;
1863
1895
  const channelLowerCase = stateChannel.toLowerCase();
1896
+ const _selectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
1897
+ const _selectedIdsArray = _selectedIds && typeof _selectedIds.toJS === 'function' ? _selectedIds.toJS() : (Array.isArray(_selectedIds) ? _selectedIds : []);
1898
+ const hasSelection = this.props.isFullMode && _selectedIdsArray.length > 0;
1864
1899
  let filteredTemplates = templates;
1865
1900
  let isTraiDltFeature = false;
1866
1901
  switch (currentChannel) {
@@ -1900,47 +1935,65 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1900
1935
  const iosBodyType = get(template, 'versions.base.content.IOS.bodyType');
1901
1936
  const inappBodyType = androidBodyType || iosBodyType;
1902
1937
  const isZaloPreviewLoading = previewTemplateId === template?._id;
1938
+ const selectedIdsForCard = get(this.props, 'Templates.selectedTemplateIds', []);
1939
+ const selectedIdsArrayForCard = selectedIdsForCard.toJS ? selectedIdsForCard.toJS() : selectedIdsForCard;
1940
+ // Archive eligibility per template: Zalo never; WhatsApp/RCS not when pending/awaiting
1941
+ const cardWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
1942
+ const cardRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
1943
+ const isCardArchiveEligible = this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
1944
+ const isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
1945
+ const isAnyArchiveInProgress = !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
1903
1946
  const templateData = {
1904
1947
  key: `${currentChannel}-card-${template?.name}`,
1905
1948
  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>
1949
+ <span className="template-card-title">
1950
+ {this.props.isFullMode && this.props.location.query.type !== EMBEDDED && isCardArchiveEligible && (
1951
+ <CapCheckbox
1952
+ checked={selectedIdsArrayForCard.includes(template._id)}
1953
+ onChange={() => !isAnyArchiveInProgress && this.props.actions.toggleTemplateSelection(template._id)}
1954
+ onClick={(e) => e.stopPropagation()}
1955
+ disabled={isAnyArchiveInProgress}
1956
+ />
1921
1957
  )}
1958
+ <CapLabel.CapLabelInline type="label1" title={template?.name} className="template-card-name">
1959
+ {template?.name}
1960
+ {currentChannel === INAPP && (
1961
+ <CapRow>
1962
+ <CapColoredTag
1963
+ tagColor={INAPP_LAYOUT_DETAILS[inappBodyType]?.tagColor}
1964
+ tagTextColor={
1965
+ INAPP_LAYOUT_DETAILS[inappBodyType]?.tagTextColor
1966
+ }
1967
+ tagHeight="1.25rem"
1968
+ tagFontSize="12px"
1969
+ >
1970
+ {INAPP_LAYOUT_DETAILS[inappBodyType]?.text}
1971
+ </CapColoredTag>
1972
+ </CapRow>
1973
+ )}
1974
+ </CapLabel.CapLabelInline>
1922
1975
  </span>
1923
1976
  ),
1924
- extra: [
1977
+ extra: isArchivedMode ? [] : [
1925
1978
  // Hide preview icon for channels that support Test and Preview
1926
1979
  // Show preview icon only for channels that don't support Test and Preview
1927
1980
  (() => {
1928
1981
  // Channels that have Test and Preview integrated
1929
1982
  const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO];
1930
1983
  const isTestAndPreviewSupported = testAndPreviewChannels.includes(currentChannel.toUpperCase());
1931
-
1984
+
1932
1985
  // Don't show preview icon if channel supports Test and Preview
1933
1986
  if (isTestAndPreviewSupported) {
1934
1987
  return null;
1935
1988
  }
1936
-
1989
+
1937
1990
  // Show preview icon for other channels (e.g., WeChat, Line, Facebook, Ebill)
1938
1991
  if (currentChannel === ZALO && isZaloPreviewLoading) {
1939
1992
  return (
1940
1993
  <CapSpin style={{ marginRight: "16px", position: "static", display: "inline" }} spinning />
1941
1994
  );
1942
1995
  }
1943
-
1996
+
1944
1997
  return this.getHoverComponent(
1945
1998
  <CapIcon
1946
1999
  className={`view-${channelLowerCase}`}
@@ -1960,7 +2013,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1960
2013
  );
1961
2014
  })()
1962
2015
  ],
1963
- hoverOption: (
2016
+ hoverOption: isArchivedMode ? null : (
1964
2017
  <CapButton
1965
2018
  className={
1966
2019
  this.props.isFullMode
@@ -2000,8 +2053,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2000
2053
  <CapDropdown
2001
2054
  overlay={
2002
2055
  <CapMenu>
2003
- {/* Phase 16: Test and Preview menu item - Show for supported channels */}
2004
- {(this.isTestAndPreviewSupported() ||
2056
+ {/* Phase 16: Test and Preview menu item - Show for supported channels, hide in archived mode */}
2057
+ {!isArchivedMode && (this.isTestAndPreviewSupported() ||
2005
2058
  (this.state.channel.toUpperCase() === WHATSAPP &&
2006
2059
  status === WHATSAPP_STATUSES.approved) || (this.state.channel.toUpperCase() === RCS &&
2007
2060
  rcsStatus === RCS_STATUSES.approved)) && (
@@ -2024,7 +2077,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2024
2077
  {![WECHAT, WHATSAPP, ZALO].includes(
2025
2078
  this.state.channel.toUpperCase()
2026
2079
  ) &&
2027
- !isTraiDltFeature && (
2080
+ !isTraiDltFeature && !template.isArchived && (
2028
2081
  <CapMenu.Item
2029
2082
  className={`duplicate-${channelLowerCase}`}
2030
2083
  onClick={() => this.duplicateTemplate(template)}
@@ -2032,6 +2085,26 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2032
2085
  <FormattedMessage {...messages.duplicateButton} />
2033
2086
  </CapMenu.Item>
2034
2087
  )}
2088
+ {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2089
+ {(() => {
2090
+ const channelUp = this.state.channel.toUpperCase();
2091
+ if (!this.isChannelArchiveEligible(channelUp, status, rcsStatus)) return null;
2092
+ return !template.isArchived ? (
2093
+ <CapMenu.Item
2094
+ className={`archive-${channelLowerCase}`}
2095
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
2096
+ >
2097
+ <FormattedMessage {...messages.archiveButton} />
2098
+ </CapMenu.Item>
2099
+ ) : (
2100
+ <CapMenu.Item
2101
+ className={`unarchive-${channelLowerCase}`}
2102
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
2103
+ >
2104
+ <FormattedMessage {...messages.unarchiveButton} />
2105
+ </CapMenu.Item>
2106
+ );
2107
+ })()}
2035
2108
  {/* Delete/Unmap menu item */}
2036
2109
  {(!(
2037
2110
  this.state.channel.toUpperCase() === WHATSAPP &&
@@ -2312,7 +2385,15 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2312
2385
  case WHATSAPP: {
2313
2386
  const { whatsappImageSrc = '', templateMsg, docPreview, whatsappVideoPreviewImg = '', templateHeaderPreview, templateFooterPreview, carouselData = [] } = getWhatsappContent(template);
2314
2387
  templateData.title = (
2315
- <CapRow>
2388
+ <CapRow type="flex" align="middle">
2389
+ {this.props.isFullMode && this.props.location.query.type !== EMBEDDED && isCardArchiveEligible && (
2390
+ <CapCheckbox
2391
+ checked={selectedIdsArrayForCard.includes(template._id)}
2392
+ onChange={() => !isAnyArchiveInProgress && this.props.actions.toggleTemplateSelection(template._id)}
2393
+ onClick={(e) => e.stopPropagation()}
2394
+ disabled={isAnyArchiveInProgress}
2395
+ />
2396
+ )}
2316
2397
  <CapLabel className="whatsapp-rcs-template-name">{template?.name}</CapLabel>
2317
2398
  <WhatsappStatusContainer template={template} />
2318
2399
  </CapRow>
@@ -2405,7 +2486,15 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2405
2486
  const name = get(template, "name", "");
2406
2487
  const statusDisplay=getRcsStatusType(status);
2407
2488
  templateData.title = (
2408
- <CapRow>
2489
+ <CapRow type="flex" align="middle">
2490
+ {this.props.isFullMode && this.props.location.query.type !== EMBEDDED && isCardArchiveEligible && (
2491
+ <CapCheckbox
2492
+ checked={selectedIdsArrayForCard.includes(template._id)}
2493
+ onChange={() => !isAnyArchiveInProgress && this.props.actions.toggleTemplateSelection(template._id)}
2494
+ onClick={(e) => e.stopPropagation()}
2495
+ disabled={isAnyArchiveInProgress}
2496
+ />
2497
+ )}
2409
2498
  <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2410
2499
  <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2411
2500
  <CapStatus
@@ -2529,7 +2618,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2529
2618
 
2530
2619
  //no templates available
2531
2620
  const showIllustrationForChannel = (forChannel) => {
2532
- return forChannel === channelLowerCase && isEmpty(templates) && isEmpty(this.state.searchText) && !isLoading;
2621
+ return forChannel === channelLowerCase && isEmpty(templates) && isEmpty(this.state.searchText) && !isLoading && !get(this.props, 'Templates.isArchivedMode', false);
2533
2622
  }
2534
2623
  //when filters applied not matching templates
2535
2624
  const filteredEmptyAndFullModeAs = (fullModeValue) => {
@@ -2547,6 +2636,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2547
2636
  const accountId = get(this.props, 'Templates.selectedWeChatAccount.uuid');
2548
2637
  const accounts = get(this.props, 'Templates.weCrmAccounts');
2549
2638
  const getAllTemplatesInProgress = get(this.props, 'Templates.getAllTemplatesInProgress');
2639
+ const archiveListingRefreshType = get(this.props, 'Templates.archiveListingRefreshType', null);
2640
+ const isArchiveOperationInProgress = get(this.props, 'Templates.archiveInProgress', false)
2641
+ || get(this.props, 'Templates.unarchiveInProgress', false)
2642
+ || get(this.props, 'Templates.bulkArchiveInProgress', false)
2643
+ || get(this.props, 'Templates.bulkUnarchiveInProgress', false);
2550
2644
 
2551
2645
  const noWhatsappZaloTemplates = this.isFullMode() && isEmpty(templates) || !this.state.hostName;
2552
2646
  const noApprovedWhatsappZaloTemplates = filteredEmptyAndFullModeAs(false) && !isEmpty(this.state?.hostName);
@@ -2555,9 +2649,36 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2555
2649
 
2556
2650
  const noLoaderAndSearchText = isEmpty(this.state.searchText) && !isLoading;
2557
2651
 
2558
- return (<div>
2652
+ const isArchivedModeLocal = get(this.props, 'Templates.isArchivedMode', false);
2653
+ const selectedIdsLocal = get(this.props, 'Templates.selectedTemplateIds', []);
2654
+ const selectedIdsArrayLocal = selectedIdsLocal && typeof selectedIdsLocal.toJS === 'function' ? selectedIdsLocal.toJS() : (Array.isArray(selectedIdsLocal) ? selectedIdsLocal : []);
2655
+ const selectedCountLocal = selectedIdsArrayLocal.length;
2656
+ const hasSelectionLocal = this.props.isFullMode && selectedCountLocal > 0;
2657
+
2658
+ return (<div>
2559
2659
  {[WECHAT, MOBILE_PUSH, WEBPUSH, INAPP, WHATSAPP, ZALO, RCS].includes(currentChannel) && this.showAccountName()}
2560
- {filterContent}
2660
+ <CapRow type="flex" align="middle" justify="space-between" className="filter-row">
2661
+ <div className="filter-row-content">{filterContent}</div>
2662
+ {hasSelectionLocal && (
2663
+ <CapRow type="flex" align="middle" className="bulk-selection-bar">
2664
+ <CapLabel type="label2">
2665
+ {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2666
+ </CapLabel>
2667
+ <CapButton
2668
+ type="primary"
2669
+ prefix={<CapIcon type="archive" size="l" />}
2670
+ onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2671
+ >
2672
+ <span className="archive-btn-label">
2673
+ <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2674
+ </span>
2675
+ </CapButton>
2676
+ <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2677
+ <FormattedMessage {...messages.archiveConfirmCancel} />
2678
+ </CapButton>
2679
+ </CapRow>
2680
+ )}
2681
+ </CapRow>
2561
2682
  {[WHATSAPP, ZALO, INAPP,RCS].includes(currentChannel) && this.selectedFilters()}
2562
2683
  {<div>
2563
2684
  {!isEmpty(filteredTemplates) || !isEmpty(this.state.searchText) || !isEmpty(this.props.Templates.templateError) ? (
@@ -2570,7 +2691,7 @@ return (<div>
2570
2691
  fbType={"list"}
2571
2692
  />
2572
2693
  </div>)
2573
- : [SMS_LOWERCASE, EMAIL_LOWERCASE].includes(this.state.channel.toLowerCase()) && !isLoading && this.getSmsEmailIllustration()
2694
+ : [SMS_LOWERCASE, EMAIL_LOWERCASE].includes(this.state.channel.toLowerCase()) && !isLoading && !get(this.props, 'Templates.isArchivedMode', false) && this.getSmsEmailIllustration()
2574
2695
  }
2575
2696
 
2576
2697
  {(this.state.selectedAccount === '' && isEmpty(this.props.Templates.selectedWeChatAccount)) && ([WECHAT_LOWERCASE, MOBILE_PUSH_LOWERCASE, INAPP_LOWERCASE].includes(this.state.channel.toLowerCase())) &&
@@ -2594,7 +2715,7 @@ return (<div>
2594
2715
  </div>
2595
2716
  )
2596
2717
  }
2597
- {(showWhatsappIllustration || showZaloIllustration) && (
2718
+ {(showWhatsappIllustration || showZaloIllustration) && !get(this.props, 'Templates.isArchivedMode', false) && (
2598
2719
  noLoaderAndSearchText &&
2599
2720
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2600
2721
  {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
@@ -2635,8 +2756,27 @@ return (<div>
2635
2756
  <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2636
2757
  </div>
2637
2758
  }
2759
+ {get(this.props, 'Templates.isArchivedMode', false) && isEmpty(templates) && !isLoading && !getAllTemplatesInProgress && isEmpty(this.state.searchText) && (
2760
+ <CapRow className={this.isFullMode() ? 'illustration-scroll-wrapper' : ''}>
2761
+ <ChannelTypeIllustration
2762
+ isFullMode={this.props.isFullMode}
2763
+ createTemplate={this.createTemplate}
2764
+ currentChannel={currentChannel}
2765
+ isArchivedMode
2766
+ />
2767
+ </CapRow>
2768
+ )}
2638
2769
  {<CapCustomSkeleton loader={isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
2639
- {<CapPageSpinner spinning={!isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
2770
+ {!isInitialLoading && getAllTemplatesInProgress && archiveListingRefreshType ? (
2771
+ <div className="archive-listing-spinner">
2772
+ <CapSpin spinning />
2773
+ <CapLabel.CapLabelInline type="label1">
2774
+ {archiveListingRefreshType === 'ARCHIVE' ? 'Archival in progress' : 'Unarchival in progress'}
2775
+ </CapLabel.CapLabelInline>
2776
+ </div>
2777
+ ) : (
2778
+ <CapPageSpinner spinning={!isInitialLoading && (isLoading || getAllTemplatesInProgress)} />
2779
+ )}
2640
2780
  </div>
2641
2781
  }
2642
2782
  </div>);
@@ -3278,6 +3418,10 @@ return (<div>
3278
3418
  };
3279
3419
 
3280
3420
  handleEditClick(e, template, modeType, path, options) {
3421
+ if (template && template.isArchived) {
3422
+ CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3423
+ return;
3424
+ }
3281
3425
  if (modeType && modeType !== undefined) {
3282
3426
  this.setState({modeType});
3283
3427
  }
@@ -3459,6 +3603,75 @@ return (<div>
3459
3603
  this.setState({showModal: false});
3460
3604
  }
3461
3605
 
3606
+ handleBulkArchiveAction = ({ ids, count, isUnarchive = false }) => {
3607
+ const { intl, actions } = this.props;
3608
+ const { channel } = this.state;
3609
+ const title = isUnarchive
3610
+ ? intl.formatMessage(messages.unarchiveTemplates)
3611
+ : intl.formatMessage(messages.archiveTemplates);
3612
+ const action = isUnarchive ? actions.bulkUnarchiveTemplates : actions.bulkArchiveTemplates;
3613
+ const successMessage = (c) => intl.formatMessage(
3614
+ isUnarchive ? messages.bulkUnarchiveSuccess : messages.bulkArchiveSuccess,
3615
+ { count: c }
3616
+ );
3617
+ this.showArchiveConfirm({
3618
+ title,
3619
+ count,
3620
+ isUnarchive,
3621
+ onConfirm: () => action(channel, ids, successMessage),
3622
+ });
3623
+ };
3624
+
3625
+ handleTemplateArchiveAction = ({ templateId, templateName, isUnarchive = false }) => {
3626
+ const { intl, actions } = this.props;
3627
+ const { channel } = this.state;
3628
+ const title = isUnarchive
3629
+ ? intl.formatMessage(messages.unarchiveTemplates)
3630
+ : intl.formatMessage(messages.archiveTemplates);
3631
+ const successMessage = isUnarchive
3632
+ ? intl.formatMessage(messages.unarchiveTemplateSuccess)
3633
+ : intl.formatMessage(messages.archiveTemplateSuccess);
3634
+ const action = isUnarchive ? actions.unarchiveTemplate : actions.archiveTemplate;
3635
+ this.showArchiveConfirm({
3636
+ title,
3637
+ count: 1,
3638
+ isUnarchive,
3639
+ onConfirm: () => action(
3640
+ channel,
3641
+ templateId,
3642
+ successMessage,
3643
+ buildTemplateNameDescription(intl.formatMessage(messages.templateNameLabel), templateName)
3644
+ ),
3645
+ });
3646
+ };
3647
+
3648
+ // Shared helper for archive/unarchive confirm modals:
3649
+ // - no icon, lighter overlay, Confirm button on left (primary), Cancel on right
3650
+ showArchiveConfirm = ({ title, content, onConfirm, count = 1, isUnarchive = false }) => {
3651
+ const { intl } = this.props;
3652
+ const confirmText = intl.formatMessage(messages.archiveConfirmOk);
3653
+ const cancelText = intl.formatMessage(messages.archiveConfirmCancel);
3654
+ // Derive content from count if not explicitly provided
3655
+ const resolvedContent = content || (isUnarchive
3656
+ ? intl.formatMessage(count > 1 ? messages.unarchiveTemplateContent : messages.unarchiveTemplateSingleContent)
3657
+ : intl.formatMessage(count > 1 ? messages.archiveTemplateContent : messages.archiveTemplateSingleContent));
3658
+ // AntD v3 footer order is [cancelButton][okButton]. Swap text+handler so
3659
+ // "Confirm" (primary) appears on the left and "Cancel" (default) on the right.
3660
+ CapModal.confirm({
3661
+ title,
3662
+ content: resolvedContent,
3663
+ icon: ' ',
3664
+ className: 'archive-confirm-modal',
3665
+ maskStyle: { backgroundColor: 'rgba(0, 0, 0, 0.25)' },
3666
+ cancelText: confirmText,
3667
+ okText: cancelText,
3668
+ cancelButtonProps: { type: 'primary' },
3669
+ okButtonProps: { type: 'default' },
3670
+ onCancel: (close) => { onConfirm(); close(); },
3671
+ // onOk (the right "Cancel" button) just closes the modal — default behaviour
3672
+ });
3673
+ }
3674
+
3462
3675
  populateTemplatesList = (data, blankTemplateRequired, layoutSelection) => {
3463
3676
  if (!data) {
3464
3677
  return [];
@@ -3608,22 +3821,71 @@ return (<div>
3608
3821
  deleteOption = this.props.intl.formatMessage(messages.unMapButton);
3609
3822
  }
3610
3823
  if (!layoutSelection) {
3824
+ // Determine archive eligibility for this template:
3825
+ // - Zalo: never eligible
3826
+ // - WhatsApp: not eligible when status is awaitingApproval / pending / unsubmitted
3827
+ // - RCS: not eligible when status is awaitingApproval / pending
3828
+ const templateWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
3829
+ const templateRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
3830
+ const channelUpCase = this.state.channel.toUpperCase();
3831
+ const isTemplateArchiveEligible = this.isChannelArchiveEligible(channelUpCase, templateWhatsappStatus, templateRcsStatus);
3832
+
3833
+ // Checkbox on card header (full mode only, only for archive-eligible templates)
3834
+ if (this.props.isFullMode && this.props.location.query.type !== EMBEDDED && isTemplateArchiveEligible) {
3835
+ const selectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
3836
+ const selectedIdsArray = selectedIds.toJS ? selectedIds.toJS() : selectedIds;
3837
+ temp.cardTop = (
3838
+ <CapRow type="flex" align="middle" justify="space-between" className="template-card-top-bar">
3839
+ <CapCheckbox
3840
+ checked={selectedIdsArray.includes(template._id)}
3841
+ onChange={() => this.props.actions.toggleTemplateSelection(template._id)}
3842
+ onClick={(e) => e.stopPropagation()}
3843
+ />
3844
+ </CapRow>
3845
+ );
3846
+ }
3847
+
3611
3848
  temp.footer = (
3612
3849
  <div className="footer-container">
3613
3850
  <div className="card-title">
3614
3851
  <span className="template-name" style={{ fontWeight: `${this.state.channel.toLowerCase() === 'wechat' ? '400' : '600'}` }}>
3615
3852
  { 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
3853
  {template?.name}
3854
+ {template.isArchived && <CapColoredTag tagColor={CAP_G08} tagTextColor={CAP_G05} className="archived-tag">{this.props.intl.formatMessage(messages.archivedTag)}</CapColoredTag>}
3617
3855
  </span>
3618
- {this.props.location.query.type !== 'embedded' && <CapPopover
3856
+ {this.props.location.query.type !== EMBEDDED && <CapPopover
3619
3857
  trigger="click"
3620
3858
  content={
3621
3859
  <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>
3860
+ {this.state.channel !== 'wechat' && !template.isArchived && <div className="popover-action-container">
3861
+ <CapButton type="link" onClick={() => this.duplicateTemplate(template)} className="popover-action">
3862
+ {this.props.intl.formatMessage(messages.duplicateButton)}
3863
+ </CapButton>
3864
+ </div>}
3865
+ {this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
3866
+ <CapButton
3867
+ type="link"
3868
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
3869
+ className="popover-action popover-archive-action"
3870
+ >
3871
+ <CapIcon type="archive" size="s" />
3872
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.archiveButton)}</CapLabel.CapLabelInline>
3873
+ </CapButton>
3874
+ </div>}
3875
+ {this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
3876
+ <CapButton
3877
+ type="link"
3878
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
3879
+ className="popover-action popover-archive-action"
3880
+ >
3881
+ <CapIcon type="archive" size="s" />
3882
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.unarchiveButton)}</CapLabel.CapLabelInline>
3883
+ </CapButton>
3624
3884
  </div>}
3625
3885
  <div className="popover-action-container">
3626
- <span onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action" style={{cursor: 'pointer', padding: '8px 0px'}}>{deleteOption}</span>
3886
+ <CapButton type="link" onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action">
3887
+ {deleteOption}
3888
+ </CapButton>
3627
3889
  </div>
3628
3890
  </div>
3629
3891
  }
@@ -3676,7 +3938,27 @@ return (<div>
3676
3938
  return false;
3677
3939
  }
3678
3940
  }
3679
- isFullMode = () => this.props.location.query.type !== "embedded" || this.props.isFullMode
3941
+ isFullMode = () => this.props.location.query.type !== EMBEDDED || this.props.isFullMode
3942
+
3943
+ isChannelArchiveEligible = (channel, whatsappStatus, rcsStatus) => (
3944
+ channel !== ZALO &&
3945
+ !(channel === WHATSAPP && [WHATSAPP_STATUSES.awaitingApproval, WHATSAPP_STATUSES.pending, WHATSAPP_STATUSES.unsubmitted].includes(whatsappStatus)) &&
3946
+ !(channel === RCS && [RCS_STATUSES.awaitingApproval, RCS_STATUSES.pending].includes(rcsStatus))
3947
+ )
3948
+
3949
+ enterArchivedMode = () => {
3950
+ this.props.actions.setArchivedMode(true);
3951
+ this.setState({ searchText: '', page: 1 }, () => {
3952
+ this.getAllTemplates({ params: { name: '', sortBy: this.state.sortBy, archiveStatus: 'archived' }, resetPage: true }, true);
3953
+ });
3954
+ }
3955
+
3956
+ exitArchivedMode = () => {
3957
+ this.props.actions.setArchivedMode(false);
3958
+ this.setState({ searchText: '', page: 1 }, () => {
3959
+ this.getAllTemplates({ params: { name: '', sortBy: this.state.sortBy, archiveStatus: 'active' }, resetPage: true }, true);
3960
+ });
3961
+ }
3680
3962
  isCreateDisabled = () => {
3681
3963
  let isDisabled = this.isLoading();
3682
3964
  const channel = this.state.channel.toUpperCase();
@@ -4214,11 +4496,16 @@ return (<div>
4214
4496
  if (([WHATSAPP_LOWERCASE, ZALO_LOWERCASE, RCS_LOWERCASE].includes(this.state?.channel?.toLocaleLowerCase()) && isEmpty(this.state?.hostName))) {
4215
4497
  isfilterContentVisisble = false;
4216
4498
  }
4499
+ const _isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
4500
+ const _renderSelectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
4501
+ const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4502
+ const _renderHasSelection = this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4503
+
4217
4504
  const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4218
4505
  {isfilterContentVisisble && <CapInput.Search
4219
4506
  className="search-text"
4220
4507
  style={{width: '210px'}}
4221
- placeholder={this.props.intl.formatMessage(messages.searchText)}
4508
+ placeholder={_isArchivedMode ? this.props.intl.formatMessage(messages.searchArchivedTemplates) : this.props.intl.formatMessage(messages.searchText)}
4222
4509
  value={this.state.searchText}
4223
4510
  onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4224
4511
  disabled={this.checkSearchDisabled()}
@@ -4370,9 +4657,9 @@ return (<div>
4370
4657
  </div>
4371
4658
  )
4372
4659
  }
4373
- <div style={{display: "flex", justifyContent: "space-between", alignItems: 'center'}}>
4374
- {
4375
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded)? (
4660
+ <div className="template-listing-header-actions">
4661
+ {!_isArchivedMode && !_renderHasSelection && (
4662
+ this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4376
4663
  <CapTooltip title={whatsappCountExceedText}>
4377
4664
  <div className="button-disabled-tooltip-wrapper">
4378
4665
  {createButton}
@@ -4380,9 +4667,29 @@ return (<div>
4380
4667
  </CapTooltip>
4381
4668
  )
4382
4669
  : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4383
- }
4670
+ )}
4671
+ {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active */}
4672
+ {!_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4673
+ <CapDropdown
4674
+ trigger={['click']}
4675
+ overlay={
4676
+ <CapMenu>
4677
+ <CapMenu.Item
4678
+ key="archived"
4679
+ onClick={this.enterArchivedMode}
4680
+ >
4681
+ <FormattedMessage {...messages.archivedTemplates} />
4682
+ </CapMenu.Item>
4683
+ </CapMenu>
4684
+ }
4685
+ placement="bottomRight"
4686
+ >
4687
+ <CapButton type="flat" className="template-listing-more-btn">
4688
+ <CapIcon type="more" />
4689
+ </CapButton>
4690
+ </CapDropdown>
4691
+ )}
4384
4692
  </div>
4385
-
4386
4693
  </div>);
4387
4694
  let htmlPreviewContent = "";
4388
4695
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4420,6 +4727,7 @@ return (<div>
4420
4727
  className={`creatives-templates-list ${
4421
4728
  this.props.isFullMode ? "full-mode" : "library-mode"
4422
4729
  }`}
4730
+ style={{ position: 'relative' }}
4423
4731
  >
4424
4732
  <input
4425
4733
  type="file"
@@ -4431,6 +4739,18 @@ return (<div>
4431
4739
  }
4432
4740
  />
4433
4741
 
4742
+ {/* Archived mode header with back arrow (full mode only) */}
4743
+ {this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
4744
+ <CapRow type="flex" align="middle" className="archived-mode-header">
4745
+ <CapIcon
4746
+ type="back"
4747
+ className="archived-mode-back-icon"
4748
+ onClick={this.exitArchivedMode}
4749
+ />
4750
+ <CapHeading type="h3"><FormattedMessage {...messages.archivedTemplates} /></CapHeading>
4751
+ </CapRow>
4752
+ )}
4753
+
4434
4754
  {channel.toLowerCase() === WHATSAPP_LOWERCASE &&
4435
4755
  showWhatsappCountWarning ? (
4436
4756
  <CapAlert message={whatsappCountExceedText} type="info" />