@capillarytech/creatives-library 8.0.348 → 8.0.350

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 (40) hide show
  1. package/constants/unified.js +1 -0
  2. package/package.json +1 -1
  3. package/services/api.js +20 -0
  4. package/services/tests/api.test.js +59 -0
  5. package/utils/common.js +6 -0
  6. package/utils/tests/v2Common.test.js +46 -1
  7. package/utils/v2common.js +18 -0
  8. package/v2Components/CapCustomSkeleton/index.js +1 -1
  9. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  10. package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +6 -18
  11. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +0 -27
  12. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WhatsAppPreviewContent.test.js +0 -48
  13. package/v2Components/TemplatePreview/_templatePreview.scss +0 -21
  14. package/v2Components/TemplatePreview/index.js +6 -18
  15. package/v2Components/TemplatePreview/tests/__snapshots__/index.test.js.snap +0 -1
  16. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +9 -0
  17. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
  18. package/v2Containers/CreativesContainer/index.js +6 -9
  19. package/v2Containers/CreativesContainer/messages.js +4 -0
  20. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +3 -0
  21. package/v2Containers/Templates/ChannelTypeIllustration.js +23 -6
  22. package/v2Containers/Templates/_templates.scss +180 -24
  23. package/v2Containers/Templates/actions.js +44 -0
  24. package/v2Containers/Templates/constants.js +31 -0
  25. package/v2Containers/Templates/index.js +364 -60
  26. package/v2Containers/Templates/messages.js +96 -0
  27. package/v2Containers/Templates/reducer.js +84 -1
  28. package/v2Containers/Templates/sagas.js +64 -0
  29. package/v2Containers/Templates/selectors.js +12 -0
  30. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +12 -0
  31. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1176 -1122
  32. package/v2Containers/Templates/tests/index.test.js +6 -0
  33. package/v2Containers/Templates/tests/reducer.test.js +178 -0
  34. package/v2Containers/Templates/tests/sagas.test.js +390 -8
  35. package/v2Containers/Templates/tests/selector.test.js +32 -0
  36. package/v2Containers/TemplatesV2/TemplatesV2.style.js +1 -1
  37. package/v2Containers/Whatsapp/constants.js +0 -8
  38. package/v2Containers/Whatsapp/index.js +5 -142
  39. package/v2Containers/Whatsapp/index.scss +0 -8
  40. 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,59 @@ 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 isArchivalEnabled = commonUtil.hasCreativesArchivalEnabled();
1942
+ const isCardArchiveEligible = isArchivalEnabled && this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
1943
+ const isArchivedMode = isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
1944
+ const isAnyArchiveInProgress = isArchivalEnabled && !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
1903
1945
  const templateData = {
1904
1946
  key: `${currentChannel}-card-${template?.name}`,
1905
1947
  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
- )}
1948
+ <span className="template-card-title">
1949
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
1950
+ <CapLabel.CapLabelInline type="label1" title={template?.name} className="template-card-name">
1951
+ {template?.name}
1952
+ {currentChannel === INAPP && (
1953
+ <CapRow>
1954
+ <CapColoredTag
1955
+ tagColor={INAPP_LAYOUT_DETAILS[inappBodyType]?.tagColor}
1956
+ tagTextColor={
1957
+ INAPP_LAYOUT_DETAILS[inappBodyType]?.tagTextColor
1958
+ }
1959
+ tagHeight="1.25rem"
1960
+ tagFontSize="0.857rem"
1961
+ >
1962
+ {INAPP_LAYOUT_DETAILS[inappBodyType]?.text}
1963
+ </CapColoredTag>
1964
+ </CapRow>
1965
+ )}
1966
+ </CapLabel.CapLabelInline>
1922
1967
  </span>
1923
1968
  ),
1924
- extra: [
1969
+ extra: isArchivedMode ? [] : [
1925
1970
  // Hide preview icon for channels that support Test and Preview
1926
1971
  // Show preview icon only for channels that don't support Test and Preview
1927
1972
  (() => {
1928
1973
  // Channels that have Test and Preview integrated
1929
1974
  const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO];
1930
1975
  const isTestAndPreviewSupported = testAndPreviewChannels.includes(currentChannel.toUpperCase());
1931
-
1976
+
1932
1977
  // Don't show preview icon if channel supports Test and Preview
1933
1978
  if (isTestAndPreviewSupported) {
1934
1979
  return null;
1935
1980
  }
1936
-
1981
+
1937
1982
  // Show preview icon for other channels (e.g., WeChat, Line, Facebook, Ebill)
1938
1983
  if (currentChannel === ZALO && isZaloPreviewLoading) {
1939
1984
  return (
1940
1985
  <CapSpin style={{ marginRight: "16px", position: "static", display: "inline" }} spinning />
1941
1986
  );
1942
1987
  }
1943
-
1988
+
1944
1989
  return this.getHoverComponent(
1945
1990
  <CapIcon
1946
1991
  className={`view-${channelLowerCase}`}
@@ -1960,7 +2005,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1960
2005
  );
1961
2006
  })()
1962
2007
  ],
1963
- hoverOption: (
2008
+ hoverOption: isArchivedMode ? null : (
1964
2009
  <CapButton
1965
2010
  className={
1966
2011
  this.props.isFullMode
@@ -2000,8 +2045,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2000
2045
  <CapDropdown
2001
2046
  overlay={
2002
2047
  <CapMenu>
2003
- {/* Phase 16: Test and Preview menu item - Show for supported channels */}
2004
- {(this.isTestAndPreviewSupported() ||
2048
+ {/* Phase 16: Test and Preview menu item - Show for supported channels, hide in archived mode */}
2049
+ {!isArchivedMode && (this.isTestAndPreviewSupported() ||
2005
2050
  (this.state.channel.toUpperCase() === WHATSAPP &&
2006
2051
  status === WHATSAPP_STATUSES.approved) || (this.state.channel.toUpperCase() === RCS &&
2007
2052
  rcsStatus === RCS_STATUSES.approved)) && (
@@ -2024,7 +2069,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2024
2069
  {![WECHAT, WHATSAPP, ZALO].includes(
2025
2070
  this.state.channel.toUpperCase()
2026
2071
  ) &&
2027
- !isTraiDltFeature && (
2072
+ !isTraiDltFeature && !template.isArchived && (
2028
2073
  <CapMenu.Item
2029
2074
  className={`duplicate-${channelLowerCase}`}
2030
2075
  onClick={() => this.duplicateTemplate(template)}
@@ -2032,6 +2077,26 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2032
2077
  <FormattedMessage {...messages.duplicateButton} />
2033
2078
  </CapMenu.Item>
2034
2079
  )}
2080
+ {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2081
+ {commonUtil.hasCreativesArchivalEnabled() && (() => {
2082
+ const channelUp = this.state.channel.toUpperCase();
2083
+ if (!this.isChannelArchiveEligible(channelUp, status, rcsStatus)) return null;
2084
+ return !template?.isArchived ? (
2085
+ <CapMenu.Item
2086
+ className={`archive-${channelLowerCase}`}
2087
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
2088
+ >
2089
+ <FormattedMessage {...messages.archiveButton} />
2090
+ </CapMenu.Item>
2091
+ ) : (
2092
+ <CapMenu.Item
2093
+ className={`unarchive-${channelLowerCase}`}
2094
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
2095
+ >
2096
+ <FormattedMessage {...messages.unarchiveButton} />
2097
+ </CapMenu.Item>
2098
+ );
2099
+ })()}
2035
2100
  {/* Delete/Unmap menu item */}
2036
2101
  {(!(
2037
2102
  this.state.channel.toUpperCase() === WHATSAPP &&
@@ -2312,7 +2377,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2312
2377
  case WHATSAPP: {
2313
2378
  const { whatsappImageSrc = '', templateMsg, docPreview, whatsappVideoPreviewImg = '', templateHeaderPreview, templateFooterPreview, carouselData = [] } = getWhatsappContent(template);
2314
2379
  templateData.title = (
2315
- <CapRow>
2380
+ <CapRow type="flex" align="middle">
2381
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2316
2382
  <CapLabel className="whatsapp-rcs-template-name">{template?.name}</CapLabel>
2317
2383
  <WhatsappStatusContainer template={template} />
2318
2384
  </CapRow>
@@ -2405,7 +2471,8 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2405
2471
  const name = get(template, "name", "");
2406
2472
  const statusDisplay=getRcsStatusType(status);
2407
2473
  templateData.title = (
2408
- <CapRow>
2474
+ <CapRow type="flex" align="middle">
2475
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2409
2476
  <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2410
2477
  <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2411
2478
  <CapStatus
@@ -2529,7 +2596,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2529
2596
 
2530
2597
  //no templates available
2531
2598
  const showIllustrationForChannel = (forChannel) => {
2532
- return forChannel === channelLowerCase && isEmpty(templates) && isEmpty(this.state.searchText) && !isLoading;
2599
+ return forChannel === channelLowerCase && isEmpty(templates) && isEmpty(this.state.searchText) && !isLoading && !get(this.props, 'Templates.isArchivedMode', false);
2533
2600
  }
2534
2601
  //when filters applied not matching templates
2535
2602
  const filteredEmptyAndFullModeAs = (fullModeValue) => {
@@ -2547,6 +2614,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2547
2614
  const accountId = get(this.props, 'Templates.selectedWeChatAccount.uuid');
2548
2615
  const accounts = get(this.props, 'Templates.weCrmAccounts');
2549
2616
  const getAllTemplatesInProgress = get(this.props, 'Templates.getAllTemplatesInProgress');
2617
+ const archiveListingRefreshType = get(this.props, 'Templates.archiveListingRefreshType', null);
2618
+ const isArchiveOperationInProgress = get(this.props, 'Templates.archiveInProgress', false)
2619
+ || get(this.props, 'Templates.unarchiveInProgress', false)
2620
+ || get(this.props, 'Templates.bulkArchiveInProgress', false)
2621
+ || get(this.props, 'Templates.bulkUnarchiveInProgress', false);
2550
2622
 
2551
2623
  const noWhatsappZaloTemplates = this.isFullMode() && isEmpty(templates) || !this.state.hostName;
2552
2624
  const noApprovedWhatsappZaloTemplates = filteredEmptyAndFullModeAs(false) && !isEmpty(this.state?.hostName);
@@ -2555,9 +2627,37 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2555
2627
 
2556
2628
  const noLoaderAndSearchText = isEmpty(this.state.searchText) && !isLoading;
2557
2629
 
2558
- return (<div>
2630
+ const isArchivalEnabledLocal = commonUtil.hasCreativesArchivalEnabled();
2631
+ const isArchivedModeLocal = isArchivalEnabledLocal && get(this.props, 'Templates.isArchivedMode', false);
2632
+ const selectedIdsLocal = get(this.props, 'Templates.selectedTemplateIds', []);
2633
+ const selectedIdsArrayLocal = selectedIdsLocal && typeof selectedIdsLocal.toJS === 'function' ? selectedIdsLocal.toJS() : (Array.isArray(selectedIdsLocal) ? selectedIdsLocal : []);
2634
+ const selectedCountLocal = selectedIdsArrayLocal.length;
2635
+ const hasSelectionLocal = isArchivalEnabledLocal && this.props.isFullMode && selectedCountLocal > 0;
2636
+
2637
+ return (<div>
2559
2638
  {[WECHAT, MOBILE_PUSH, WEBPUSH, INAPP, WHATSAPP, ZALO, RCS].includes(currentChannel) && this.showAccountName()}
2560
- {filterContent}
2639
+ <CapRow type="flex" align="middle" justify="space-between" className="filter-row">
2640
+ <div className="filter-row-content">{filterContent}</div>
2641
+ {hasSelectionLocal && (
2642
+ <CapRow type="flex" align="middle" className="bulk-selection-bar">
2643
+ <CapLabel type="label2">
2644
+ {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2645
+ </CapLabel>
2646
+ <CapButton
2647
+ type="primary"
2648
+ prefix={<CapIcon type="archive" size="l" />}
2649
+ onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2650
+ >
2651
+ <span className="archive-btn-label">
2652
+ <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2653
+ </span>
2654
+ </CapButton>
2655
+ <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2656
+ <FormattedMessage {...messages.archiveConfirmCancel} />
2657
+ </CapButton>
2658
+ </CapRow>
2659
+ )}
2660
+ </CapRow>
2561
2661
  {[WHATSAPP, ZALO, INAPP,RCS].includes(currentChannel) && this.selectedFilters()}
2562
2662
  {<div>
2563
2663
  {!isEmpty(filteredTemplates) || !isEmpty(this.state.searchText) || !isEmpty(this.props.Templates.templateError) ? (
@@ -2570,7 +2670,7 @@ return (<div>
2570
2670
  fbType={"list"}
2571
2671
  />
2572
2672
  </div>)
2573
- : [SMS_LOWERCASE, EMAIL_LOWERCASE].includes(this.state.channel.toLowerCase()) && !isLoading && this.getSmsEmailIllustration()
2673
+ : [SMS_LOWERCASE, EMAIL_LOWERCASE].includes(this.state.channel.toLowerCase()) && !isLoading && !get(this.props, 'Templates.isArchivedMode', false) && this.getSmsEmailIllustration()
2574
2674
  }
2575
2675
 
2576
2676
  {(this.state.selectedAccount === '' && isEmpty(this.props.Templates.selectedWeChatAccount)) && ([WECHAT_LOWERCASE, MOBILE_PUSH_LOWERCASE, INAPP_LOWERCASE].includes(this.state.channel.toLowerCase())) &&
@@ -2594,7 +2694,7 @@ return (<div>
2594
2694
  </div>
2595
2695
  )
2596
2696
  }
2597
- {(showWhatsappIllustration || showZaloIllustration) && (
2697
+ {(showWhatsappIllustration || showZaloIllustration) && !get(this.props, 'Templates.isArchivedMode', false) && (
2598
2698
  noLoaderAndSearchText &&
2599
2699
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2600
2700
  {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
@@ -2635,8 +2735,27 @@ return (<div>
2635
2735
  <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2636
2736
  </div>
2637
2737
  }
2738
+ {get(this.props, 'Templates.isArchivedMode', false) && isEmpty(templates) && !isLoading && !getAllTemplatesInProgress && isEmpty(this.state.searchText) && (
2739
+ <CapRow className={this.isFullMode() ? 'illustration-scroll-wrapper' : ''}>
2740
+ <ChannelTypeIllustration
2741
+ isFullMode={this.props.isFullMode}
2742
+ createTemplate={this.createTemplate}
2743
+ currentChannel={currentChannel}
2744
+ isArchivedMode
2745
+ />
2746
+ </CapRow>
2747
+ )}
2638
2748
  {<CapCustomSkeleton loader={isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
2639
- {<CapPageSpinner spinning={!isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
2749
+ {!isInitialLoading && getAllTemplatesInProgress && archiveListingRefreshType ? (
2750
+ <div className="archive-listing-spinner">
2751
+ <CapSpin spinning />
2752
+ <CapLabel.CapLabelInline type="label1">
2753
+ {archiveListingRefreshType === ARCHIVE_REFRESH_TYPE_ARCHIVE ? this.props.intl.formatMessage(messages.archivalInProgress) : this.props.intl.formatMessage(messages.unarchivalInProgress)}
2754
+ </CapLabel.CapLabelInline>
2755
+ </div>
2756
+ ) : (
2757
+ <CapPageSpinner spinning={!isInitialLoading && (isLoading || getAllTemplatesInProgress)} />
2758
+ )}
2640
2759
  </div>
2641
2760
  }
2642
2761
  </div>);
@@ -3278,6 +3397,10 @@ return (<div>
3278
3397
  };
3279
3398
 
3280
3399
  handleEditClick(e, template, modeType, path, options) {
3400
+ if (template && template.isArchived) {
3401
+ CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3402
+ return;
3403
+ }
3281
3404
  if (modeType && modeType !== undefined) {
3282
3405
  this.setState({modeType});
3283
3406
  }
@@ -3459,6 +3582,87 @@ return (<div>
3459
3582
  this.setState({showModal: false});
3460
3583
  }
3461
3584
 
3585
+ renderCardSelectionCheckbox = ({ templateId, selectedIds, isDisabled }) => {
3586
+ if (!this.props.isFullMode || this.props.location.query.type === EMBEDDED) return null;
3587
+ return (
3588
+ <CapCheckbox
3589
+ checked={selectedIds.includes(templateId)}
3590
+ onChange={() => !isDisabled && this.props.actions.toggleTemplateSelection(templateId)}
3591
+ onClick={(e) => e.stopPropagation()}
3592
+ disabled={isDisabled}
3593
+ />
3594
+ );
3595
+ }
3596
+
3597
+ handleBulkArchiveAction = ({ ids, count, isUnarchive = false }) => {
3598
+ const { intl, actions } = this.props;
3599
+ const { channel } = this.state;
3600
+ const title = isUnarchive
3601
+ ? intl.formatMessage(messages.unarchiveTemplates)
3602
+ : intl.formatMessage(messages.archiveTemplates);
3603
+ const action = isUnarchive ? actions.bulkUnarchiveTemplates : actions.bulkArchiveTemplates;
3604
+ const successMessage = (c) => intl.formatMessage(
3605
+ isUnarchive ? messages.bulkUnarchiveSuccess : messages.bulkArchiveSuccess,
3606
+ { count: c }
3607
+ );
3608
+ this.showArchiveConfirm({
3609
+ title,
3610
+ count,
3611
+ isUnarchive,
3612
+ onConfirm: () => action(channel, ids, successMessage),
3613
+ });
3614
+ };
3615
+
3616
+ handleTemplateArchiveAction = ({ templateId, templateName, isUnarchive = false }) => {
3617
+ const { intl, actions } = this.props;
3618
+ const { channel } = this.state;
3619
+ const title = isUnarchive
3620
+ ? intl.formatMessage(messages.unarchiveTemplates)
3621
+ : intl.formatMessage(messages.archiveTemplates);
3622
+ const successMessage = isUnarchive
3623
+ ? intl.formatMessage(messages.unarchiveTemplateSuccess)
3624
+ : intl.formatMessage(messages.archiveTemplateSuccess);
3625
+ const action = isUnarchive ? actions.unarchiveTemplate : actions.archiveTemplate;
3626
+ this.showArchiveConfirm({
3627
+ title,
3628
+ count: 1,
3629
+ isUnarchive,
3630
+ onConfirm: () => action(
3631
+ channel,
3632
+ templateId,
3633
+ successMessage,
3634
+ buildTemplateNameDescription(intl.formatMessage(messages.templateNameLabel), templateName)
3635
+ ),
3636
+ });
3637
+ };
3638
+
3639
+ // Shared helper for archive/unarchive confirm modals:
3640
+ // - no icon, lighter overlay, Confirm button on left (primary), Cancel on right
3641
+ showArchiveConfirm = ({ title, content, onConfirm, count = 1, isUnarchive = false }) => {
3642
+ const { intl } = this.props;
3643
+ const confirmText = intl.formatMessage(messages.archiveConfirmOk);
3644
+ const cancelText = intl.formatMessage(messages.archiveConfirmCancel);
3645
+ // Derive content from count if not explicitly provided
3646
+ const resolvedContent = content || (isUnarchive
3647
+ ? intl.formatMessage(count > 1 ? messages.unarchiveTemplateContent : messages.unarchiveTemplateSingleContent)
3648
+ : intl.formatMessage(count > 1 ? messages.archiveTemplateContent : messages.archiveTemplateSingleContent));
3649
+ // AntD v3 footer order is [cancelButton][okButton]. Swap text+handler so
3650
+ // "Confirm" (primary) appears on the left and "Cancel" (default) on the right.
3651
+ CapModal.confirm({
3652
+ title,
3653
+ content: resolvedContent,
3654
+ icon: ' ',
3655
+ className: 'archive-confirm-modal',
3656
+ maskStyle: { backgroundColor: 'rgba(0, 0, 0, 0.25)' },
3657
+ cancelText: confirmText,
3658
+ okText: cancelText,
3659
+ cancelButtonProps: { type: 'primary' },
3660
+ okButtonProps: { type: 'default' },
3661
+ onCancel: (close) => { onConfirm(); close(); },
3662
+ // onOk (the right "Cancel" button) just closes the modal — default behaviour
3663
+ });
3664
+ }
3665
+
3462
3666
  populateTemplatesList = (data, blankTemplateRequired, layoutSelection) => {
3463
3667
  if (!data) {
3464
3668
  return [];
@@ -3608,22 +3812,71 @@ return (<div>
3608
3812
  deleteOption = this.props.intl.formatMessage(messages.unMapButton);
3609
3813
  }
3610
3814
  if (!layoutSelection) {
3815
+ // Determine archive eligibility for this template:
3816
+ // - Zalo: never eligible
3817
+ // - WhatsApp: not eligible when status is awaitingApproval / pending / unsubmitted
3818
+ // - RCS: not eligible when status is awaitingApproval / pending
3819
+ const templateWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
3820
+ const templateRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
3821
+ const channelUpCase = this.state.channel.toUpperCase();
3822
+ const isTemplateArchiveEligible = this.isChannelArchiveEligible(channelUpCase, templateWhatsappStatus, templateRcsStatus);
3823
+
3824
+ // Checkbox on card header (full mode only, only for archive-eligible templates)
3825
+ if (this.props.isFullMode && this.props.location.query.type !== EMBEDDED && isTemplateArchiveEligible) {
3826
+ const selectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
3827
+ const selectedIdsArray = selectedIds.toJS ? selectedIds.toJS() : selectedIds;
3828
+ temp.cardTop = (
3829
+ <CapRow type="flex" align="middle" justify="space-between" className="template-card-top-bar">
3830
+ <CapCheckbox
3831
+ checked={selectedIdsArray.includes(template._id)}
3832
+ onChange={() => this.props.actions.toggleTemplateSelection(template._id)}
3833
+ onClick={(e) => e.stopPropagation()}
3834
+ />
3835
+ </CapRow>
3836
+ );
3837
+ }
3838
+
3611
3839
  temp.footer = (
3612
3840
  <div className="footer-container">
3613
3841
  <div className="card-title">
3614
3842
  <span className="template-name" style={{ fontWeight: `${this.state.channel.toLowerCase() === 'wechat' ? '400' : '600'}` }}>
3615
3843
  { 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
3844
  {template?.name}
3845
+ {commonUtil.hasCreativesArchivalEnabled() && template.isArchived && <CapColoredTag tagColor={CAP_G08} tagTextColor={CAP_G05} className="archived-tag">{this.props.intl.formatMessage(messages.archivedTag)}</CapColoredTag>}
3617
3846
  </span>
3618
- {this.props.location.query.type !== 'embedded' && <CapPopover
3847
+ {this.props.location.query.type !== EMBEDDED && <CapPopover
3619
3848
  trigger="click"
3620
3849
  content={
3621
3850
  <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>
3851
+ {this.state.channel !== 'wechat' && !template.isArchived && <div className="popover-action-container">
3852
+ <CapButton type="link" onClick={() => this.duplicateTemplate(template)} className="popover-action">
3853
+ {this.props.intl.formatMessage(messages.duplicateButton)}
3854
+ </CapButton>
3855
+ </div>}
3856
+ {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
3857
+ <CapButton
3858
+ type="link"
3859
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
3860
+ className="popover-action popover-archive-action"
3861
+ >
3862
+ <CapIcon type="archive" size="s" />
3863
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.archiveButton)}</CapLabel.CapLabelInline>
3864
+ </CapButton>
3865
+ </div>}
3866
+ {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
3867
+ <CapButton
3868
+ type="link"
3869
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
3870
+ className="popover-action popover-archive-action"
3871
+ >
3872
+ <CapIcon type="archive" size="s" />
3873
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.unarchiveButton)}</CapLabel.CapLabelInline>
3874
+ </CapButton>
3624
3875
  </div>}
3625
3876
  <div className="popover-action-container">
3626
- <span onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action" style={{cursor: 'pointer', padding: '8px 0px'}}>{deleteOption}</span>
3877
+ <CapButton type="link" onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action">
3878
+ {deleteOption}
3879
+ </CapButton>
3627
3880
  </div>
3628
3881
  </div>
3629
3882
  }
@@ -3676,7 +3929,20 @@ return (<div>
3676
3929
  return false;
3677
3930
  }
3678
3931
  }
3679
- isFullMode = () => this.props.location.query.type !== "embedded" || this.props.isFullMode
3932
+ isFullMode = () => this.props.location.query.type !== EMBEDDED || this.props.isFullMode
3933
+
3934
+ isChannelArchiveEligible = (channel, whatsappStatus, rcsStatus) => (
3935
+ channel !== ZALO &&
3936
+ !(channel === WHATSAPP && [WHATSAPP_STATUSES.awaitingApproval, WHATSAPP_STATUSES.pending, WHATSAPP_STATUSES.unsubmitted].includes(whatsappStatus)) &&
3937
+ !(channel === RCS && [RCS_STATUSES.awaitingApproval, RCS_STATUSES.pending].includes(rcsStatus))
3938
+ )
3939
+
3940
+ setArchivedMode = (isArchived) => {
3941
+ this.props.actions.setArchivedMode(isArchived);
3942
+ this.setState({ searchText: '', page: 1 }, () => {
3943
+ this.getAllTemplates({ params: { name: '', sortBy: this.state.sortBy, archiveStatus: isArchived ? ARCHIVE_STATUS_ARCHIVED : ARCHIVE_STATUS_ACTIVE }, resetPage: true }, true);
3944
+ });
3945
+ }
3680
3946
  isCreateDisabled = () => {
3681
3947
  let isDisabled = this.isLoading();
3682
3948
  const channel = this.state.channel.toUpperCase();
@@ -4214,11 +4480,17 @@ return (<div>
4214
4480
  if (([WHATSAPP_LOWERCASE, ZALO_LOWERCASE, RCS_LOWERCASE].includes(this.state?.channel?.toLocaleLowerCase()) && isEmpty(this.state?.hostName))) {
4215
4481
  isfilterContentVisisble = false;
4216
4482
  }
4483
+ const _isArchivalEnabled = commonUtil.hasCreativesArchivalEnabled();
4484
+ const _isArchivedMode = _isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
4485
+ const _renderSelectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
4486
+ const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4487
+ const _renderHasSelection = _isArchivalEnabled && this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4488
+
4217
4489
  const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4218
4490
  {isfilterContentVisisble && <CapInput.Search
4219
4491
  className="search-text"
4220
4492
  style={{width: '210px'}}
4221
- placeholder={this.props.intl.formatMessage(messages.searchText)}
4493
+ placeholder={_isArchivedMode ? this.props.intl.formatMessage(messages.searchArchivedTemplates) : this.props.intl.formatMessage(messages.searchText)}
4222
4494
  value={this.state.searchText}
4223
4495
  onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4224
4496
  disabled={this.checkSearchDisabled()}
@@ -4370,9 +4642,9 @@ return (<div>
4370
4642
  </div>
4371
4643
  )
4372
4644
  }
4373
- <div style={{display: "flex", justifyContent: "space-between", alignItems: 'center'}}>
4374
- {
4375
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded)? (
4645
+ <div className="template-listing-header-actions">
4646
+ {!_isArchivedMode && !_renderHasSelection && (
4647
+ this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4376
4648
  <CapTooltip title={whatsappCountExceedText}>
4377
4649
  <div className="button-disabled-tooltip-wrapper">
4378
4650
  {createButton}
@@ -4380,9 +4652,29 @@ return (<div>
4380
4652
  </CapTooltip>
4381
4653
  )
4382
4654
  : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4383
- }
4655
+ )}
4656
+ {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active, archive flag enabled */}
4657
+ {commonUtil.hasCreativesArchivalEnabled() && !_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== EMBEDDED && channelLowerCase !== ZALO_LOWERCASE && (
4658
+ <CapDropdown
4659
+ trigger={['click']}
4660
+ overlay={
4661
+ <CapMenu>
4662
+ <CapMenu.Item
4663
+ key="archived"
4664
+ onClick={() => this.setArchivedMode(true)}
4665
+ >
4666
+ <FormattedMessage {...messages.archivedTemplates} />
4667
+ </CapMenu.Item>
4668
+ </CapMenu>
4669
+ }
4670
+ placement="bottomRight"
4671
+ >
4672
+ <CapButton type="flat" className="template-listing-more-btn">
4673
+ <CapIcon type="more" />
4674
+ </CapButton>
4675
+ </CapDropdown>
4676
+ )}
4384
4677
  </div>
4385
-
4386
4678
  </div>);
4387
4679
  let htmlPreviewContent = "";
4388
4680
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4431,6 +4723,18 @@ return (<div>
4431
4723
  }
4432
4724
  />
4433
4725
 
4726
+ {/* Archived mode header with back arrow (full mode only, archive flag enabled) */}
4727
+ {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
4728
+ <CapRow type="flex" align="middle" className="archived-mode-header">
4729
+ <CapIcon
4730
+ type="back"
4731
+ className="archived-mode-back-icon"
4732
+ onClick={() => this.setArchivedMode(false)}
4733
+ />
4734
+ <CapHeading type="h3"><FormattedMessage {...messages.archivedTemplates} /></CapHeading>
4735
+ </CapRow>
4736
+ )}
4737
+
4434
4738
  {channel.toLowerCase() === WHATSAPP_LOWERCASE &&
4435
4739
  showWhatsappCountWarning ? (
4436
4740
  <CapAlert message={whatsappCountExceedText} type="info" />