@capillarytech/creatives-library 8.0.345-alpha.8 → 8.0.345

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 (27) 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/v2Components/CapCustomSkeleton/index.js +1 -1
  5. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  6. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +9 -0
  7. package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -20
  8. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
  9. package/v2Containers/CreativesContainer/index.js +6 -4
  10. package/v2Containers/CreativesContainer/messages.js +4 -0
  11. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +3 -0
  12. package/v2Containers/Email/index.js +0 -17
  13. package/v2Containers/Templates/ChannelTypeIllustration.js +23 -6
  14. package/v2Containers/Templates/_templates.scss +130 -24
  15. package/v2Containers/Templates/actions.js +36 -0
  16. package/v2Containers/Templates/constants.js +23 -0
  17. package/v2Containers/Templates/index.js +286 -30
  18. package/v2Containers/Templates/messages.js +68 -0
  19. package/v2Containers/Templates/reducer.js +68 -0
  20. package/v2Containers/Templates/sagas.js +89 -1
  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 +1300 -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 +314 -8
  27. package/v2Containers/Templates/tests/selector.test.js +32 -0
@@ -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';
@@ -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();
@@ -1595,6 +1587,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1595
1587
  let queryParams = params || {};
1596
1588
  let page = this.state.page;
1597
1589
  const { activeMode } = this.state;
1590
+ // Archive filter — use explicit param if provided (props may not have updated yet due to async dispatch)
1591
+ if (!queryParams.archiveStatus) {
1592
+ const archiveFilter = get(this.props, 'Templates.archiveFilter', 'active');
1593
+ queryParams.archiveStatus = archiveFilter;
1594
+ }
1598
1595
  if (activeMode === ACCOUNT_SELECTION_MODE) {
1599
1596
  this.setTemplatesMode();
1600
1597
  }
@@ -1861,6 +1858,9 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1861
1858
  const currentChannel = channel.toUpperCase();
1862
1859
  const {channel: stateChannel} = this.state;
1863
1860
  const channelLowerCase = stateChannel.toLowerCase();
1861
+ const _selectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
1862
+ const _selectedIdsArray = _selectedIds && typeof _selectedIds.toJS === 'function' ? _selectedIds.toJS() : (Array.isArray(_selectedIds) ? _selectedIds : []);
1863
+ const hasSelection = this.props.isFullMode && _selectedIdsArray.length > 0;
1864
1864
  let filteredTemplates = templates;
1865
1865
  let isTraiDltFeature = false;
1866
1866
  switch (currentChannel) {
@@ -1900,10 +1900,26 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1900
1900
  const iosBodyType = get(template, 'versions.base.content.IOS.bodyType');
1901
1901
  const inappBodyType = androidBodyType || iosBodyType;
1902
1902
  const isZaloPreviewLoading = previewTemplateId === template?._id;
1903
+ const selectedIdsForCard = get(this.props, 'Templates.selectedTemplateIds', []);
1904
+ const selectedIdsArrayForCard = selectedIdsForCard.toJS ? selectedIdsForCard.toJS() : selectedIdsForCard;
1905
+ // Archive eligibility per template: Zalo never; WhatsApp/RCS not when pending/awaiting
1906
+ const cardWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
1907
+ const cardRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
1908
+ const isCardArchiveEligible = currentChannel !== ZALO &&
1909
+ !(currentChannel === WHATSAPP && [WHATSAPP_STATUSES.awaitingApproval, WHATSAPP_STATUSES.pending, WHATSAPP_STATUSES.unsubmitted].includes(cardWhatsappStatus)) &&
1910
+ !(currentChannel === RCS && [RCS_STATUSES.awaitingApproval, RCS_STATUSES.pending].includes(cardRcsStatus));
1903
1911
  const templateData = {
1904
1912
  key: `${currentChannel}-card-${template?.name}`,
1905
1913
  title: (
1906
- <span title={template?.name}>
1914
+ <span title={template?.name} style={{ display: 'flex', alignItems: 'center' }}>
1915
+ {this.props.isFullMode && this.props.location.query.type !== 'embedded' && isCardArchiveEligible && (
1916
+ <CapCheckbox
1917
+ checked={selectedIdsArrayForCard.includes(template._id)}
1918
+ onChange={() => this.props.actions.toggleTemplateSelection(template._id)}
1919
+ onClick={(e) => e.stopPropagation()}
1920
+ style={{ marginRight: CAP_SPACE_08, flexShrink: 0 }}
1921
+ />
1922
+ )}
1907
1923
  {template?.name}
1908
1924
  {currentChannel === INAPP && (
1909
1925
  <CapRow>
@@ -2024,7 +2040,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2024
2040
  {![WECHAT, WHATSAPP, ZALO].includes(
2025
2041
  this.state.channel.toUpperCase()
2026
2042
  ) &&
2027
- !isTraiDltFeature && (
2043
+ !isTraiDltFeature && !template.isArchived && (
2028
2044
  <CapMenu.Item
2029
2045
  className={`duplicate-${channelLowerCase}`}
2030
2046
  onClick={() => this.duplicateTemplate(template)}
@@ -2032,6 +2048,42 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2032
2048
  <FormattedMessage {...messages.duplicateButton} />
2033
2049
  </CapMenu.Item>
2034
2050
  )}
2051
+ {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2052
+ {(() => {
2053
+ const channelUp = this.state.channel.toUpperCase();
2054
+ const isArchiveEligible = channelUp !== ZALO &&
2055
+ !(channelUp === WHATSAPP && [WHATSAPP_STATUSES.awaitingApproval, WHATSAPP_STATUSES.pending, WHATSAPP_STATUSES.unsubmitted].includes(status)) &&
2056
+ !(channelUp === RCS && [RCS_STATUSES.awaitingApproval, RCS_STATUSES.pending].includes(rcsStatus));
2057
+ if (!isArchiveEligible) return null;
2058
+ return !template.isArchived ? (
2059
+ <CapMenu.Item
2060
+ className={`archive-${channelLowerCase}`}
2061
+ onClick={() => {
2062
+ this.showArchiveConfirm({
2063
+ title: this.props.intl.formatMessage(messages.archiveTemplates),
2064
+ onConfirm: () => this.props.actions.archiveTemplate(this.state.channel, template._id, template.name),
2065
+ count: 1,
2066
+ });
2067
+ }}
2068
+ >
2069
+ <FormattedMessage {...messages.archiveButton} />
2070
+ </CapMenu.Item>
2071
+ ) : (
2072
+ <CapMenu.Item
2073
+ className={`unarchive-${channelLowerCase}`}
2074
+ onClick={() => {
2075
+ this.showArchiveConfirm({
2076
+ title: this.props.intl.formatMessage(messages.unarchiveTemplates),
2077
+ onConfirm: () => this.props.actions.unarchiveTemplate(this.state.channel, template._id, template.name),
2078
+ count: 1,
2079
+ isUnarchive: true,
2080
+ });
2081
+ }}
2082
+ >
2083
+ <FormattedMessage {...messages.unarchiveButton} />
2084
+ </CapMenu.Item>
2085
+ );
2086
+ })()}
2035
2087
  {/* Delete/Unmap menu item */}
2036
2088
  {(!(
2037
2089
  this.state.channel.toUpperCase() === WHATSAPP &&
@@ -2312,7 +2364,15 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2312
2364
  case WHATSAPP: {
2313
2365
  const { whatsappImageSrc = '', templateMsg, docPreview, whatsappVideoPreviewImg = '', templateHeaderPreview, templateFooterPreview, carouselData = [] } = getWhatsappContent(template);
2314
2366
  templateData.title = (
2315
- <CapRow>
2367
+ <CapRow type="flex" align="middle">
2368
+ {this.props.isFullMode && this.props.location.query.type !== 'embedded' && isCardArchiveEligible && (
2369
+ <CapCheckbox
2370
+ checked={selectedIdsArrayForCard.includes(template._id)}
2371
+ onChange={() => this.props.actions.toggleTemplateSelection(template._id)}
2372
+ onClick={(e) => e.stopPropagation()}
2373
+ style={{ marginRight: CAP_SPACE_08, flexShrink: 0 }}
2374
+ />
2375
+ )}
2316
2376
  <CapLabel className="whatsapp-rcs-template-name">{template?.name}</CapLabel>
2317
2377
  <WhatsappStatusContainer template={template} />
2318
2378
  </CapRow>
@@ -2405,7 +2465,15 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2405
2465
  const name = get(template, "name", "");
2406
2466
  const statusDisplay=getRcsStatusType(status);
2407
2467
  templateData.title = (
2408
- <CapRow>
2468
+ <CapRow type="flex" align="middle">
2469
+ {this.props.isFullMode && this.props.location.query.type !== 'embedded' && isCardArchiveEligible && (
2470
+ <CapCheckbox
2471
+ checked={selectedIdsArrayForCard.includes(template._id)}
2472
+ onChange={() => this.props.actions.toggleTemplateSelection(template._id)}
2473
+ onClick={(e) => e.stopPropagation()}
2474
+ style={{ marginRight: CAP_SPACE_08, flexShrink: 0 }}
2475
+ />
2476
+ )}
2409
2477
  <CapLabel className="whatsapp-rcs-template-name">{name}</CapLabel>
2410
2478
  <CapRow type="flex" align="middle" className="rcs-status-container zalo-status-color">
2411
2479
  <CapStatus
@@ -2529,7 +2597,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2529
2597
 
2530
2598
  //no templates available
2531
2599
  const showIllustrationForChannel = (forChannel) => {
2532
- return forChannel === channelLowerCase && isEmpty(templates) && isEmpty(this.state.searchText) && !isLoading;
2600
+ return forChannel === channelLowerCase && isEmpty(templates) && isEmpty(this.state.searchText) && !isLoading && !get(this.props, 'Templates.isArchivedMode', false);
2533
2601
  }
2534
2602
  //when filters applied not matching templates
2535
2603
  const filteredEmptyAndFullModeAs = (fullModeValue) => {
@@ -2555,9 +2623,49 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2555
2623
 
2556
2624
  const noLoaderAndSearchText = isEmpty(this.state.searchText) && !isLoading;
2557
2625
 
2558
- return (<div>
2626
+ const isArchivedModeLocal = get(this.props, 'Templates.isArchivedMode', false);
2627
+ const selectedIdsLocal = get(this.props, 'Templates.selectedTemplateIds', []);
2628
+ const selectedIdsArrayLocal = selectedIdsLocal && typeof selectedIdsLocal.toJS === 'function' ? selectedIdsLocal.toJS() : (Array.isArray(selectedIdsLocal) ? selectedIdsLocal : []);
2629
+ const selectedCountLocal = selectedIdsArrayLocal.length;
2630
+ const hasSelectionLocal = this.props.isFullMode && selectedCountLocal > 0;
2631
+
2632
+ return (<div>
2559
2633
  {[WECHAT, MOBILE_PUSH, WEBPUSH, INAPP, WHATSAPP, ZALO, RCS].includes(currentChannel) && this.showAccountName()}
2560
- {filterContent}
2634
+ <CapRow type="flex" align="middle" justify="space-between" className="filter-row">
2635
+ <div className="filter-row-content">{filterContent}</div>
2636
+ {hasSelectionLocal && (
2637
+ <CapRow type="flex" align="middle" className="bulk-selection-bar">
2638
+ <CapLabel type="label2">
2639
+ {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2640
+ </CapLabel>
2641
+ <CapButton
2642
+ type="primary"
2643
+ prefix={<CapIcon type="archive" size="l" />}
2644
+ onClick={() => {
2645
+ this.showArchiveConfirm({
2646
+ title: this.props.intl.formatMessage(isArchivedModeLocal ? messages.unarchiveTemplates : messages.archiveTemplates),
2647
+ onConfirm: () => {
2648
+ if (isArchivedModeLocal) {
2649
+ this.props.actions.bulkUnarchiveTemplates(this.state.channel, selectedIdsArrayLocal);
2650
+ } else {
2651
+ this.props.actions.bulkArchiveTemplates(this.state.channel, selectedIdsArrayLocal);
2652
+ }
2653
+ },
2654
+ count: selectedCountLocal,
2655
+ isUnarchive: isArchivedModeLocal,
2656
+ });
2657
+ }}
2658
+ >
2659
+ <span className="archive-btn-label">
2660
+ <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2661
+ </span>
2662
+ </CapButton>
2663
+ <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2664
+ <FormattedMessage {...messages.archiveConfirmCancel} />
2665
+ </CapButton>
2666
+ </CapRow>
2667
+ )}
2668
+ </CapRow>
2561
2669
  {[WHATSAPP, ZALO, INAPP,RCS].includes(currentChannel) && this.selectedFilters()}
2562
2670
  {<div>
2563
2671
  {!isEmpty(filteredTemplates) || !isEmpty(this.state.searchText) || !isEmpty(this.props.Templates.templateError) ? (
@@ -2570,7 +2678,7 @@ return (<div>
2570
2678
  fbType={"list"}
2571
2679
  />
2572
2680
  </div>)
2573
- : [SMS_LOWERCASE, EMAIL_LOWERCASE].includes(this.state.channel.toLowerCase()) && !isLoading && this.getSmsEmailIllustration()
2681
+ : [SMS_LOWERCASE, EMAIL_LOWERCASE].includes(this.state.channel.toLowerCase()) && !isLoading && !get(this.props, 'Templates.isArchivedMode', false) && this.getSmsEmailIllustration()
2574
2682
  }
2575
2683
 
2576
2684
  {(this.state.selectedAccount === '' && isEmpty(this.props.Templates.selectedWeChatAccount)) && ([WECHAT_LOWERCASE, MOBILE_PUSH_LOWERCASE, INAPP_LOWERCASE].includes(this.state.channel.toLowerCase())) &&
@@ -2594,7 +2702,7 @@ return (<div>
2594
2702
  </div>
2595
2703
  )
2596
2704
  }
2597
- {(showWhatsappIllustration || showZaloIllustration) && (
2705
+ {(showWhatsappIllustration || showZaloIllustration) && !get(this.props, 'Templates.isArchivedMode', false) && (
2598
2706
  noLoaderAndSearchText &&
2599
2707
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2600
2708
  {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
@@ -2635,6 +2743,16 @@ return (<div>
2635
2743
  <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2636
2744
  </div>
2637
2745
  }
2746
+ {get(this.props, 'Templates.isArchivedMode', false) && isEmpty(templates) && !isLoading && !getAllTemplatesInProgress && isEmpty(this.state.searchText) && (
2747
+ <div className={this.isFullMode() ? 'illustration-scroll-wrapper' : ''}>
2748
+ <ChannelTypeIllustration
2749
+ isFullMode={this.props.isFullMode}
2750
+ createTemplate={this.createTemplate}
2751
+ currentChannel={currentChannel}
2752
+ isArchivedMode
2753
+ />
2754
+ </div>
2755
+ )}
2638
2756
  {<CapCustomSkeleton loader={isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
2639
2757
  {<CapPageSpinner spinning={!isInitialLoading && (isLoading || getAllTemplatesInProgress)} />}
2640
2758
  </div>
@@ -3278,6 +3396,10 @@ return (<div>
3278
3396
  };
3279
3397
 
3280
3398
  handleEditClick(e, template, modeType, path, options) {
3399
+ if (template && template.isArchived) {
3400
+ CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3401
+ return;
3402
+ }
3281
3403
  if (modeType && modeType !== undefined) {
3282
3404
  this.setState({modeType});
3283
3405
  }
@@ -3459,6 +3581,33 @@ return (<div>
3459
3581
  this.setState({showModal: false});
3460
3582
  }
3461
3583
 
3584
+ // Shared helper for archive/unarchive confirm modals:
3585
+ // - no icon, lighter overlay, Confirm button on left (primary), Cancel on right
3586
+ showArchiveConfirm = ({ title, content, onConfirm, count = 1, isUnarchive = false }) => {
3587
+ const { intl } = this.props;
3588
+ const confirmText = intl.formatMessage(messages.archiveConfirmOk);
3589
+ const cancelText = intl.formatMessage(messages.archiveConfirmCancel);
3590
+ // Derive content from count if not explicitly provided
3591
+ const resolvedContent = content || (isUnarchive
3592
+ ? intl.formatMessage(count > 1 ? messages.unarchiveTemplateContent : messages.unarchiveTemplateSingleContent)
3593
+ : intl.formatMessage(count > 1 ? messages.archiveTemplateContent : messages.archiveTemplateSingleContent));
3594
+ // AntD v3 footer order is [cancelButton][okButton]. Swap text+handler so
3595
+ // "Confirm" (primary) appears on the left and "Cancel" (default) on the right.
3596
+ CapModal.confirm({
3597
+ title,
3598
+ content: resolvedContent,
3599
+ icon: ' ',
3600
+ className: 'archive-confirm-modal',
3601
+ maskStyle: { backgroundColor: 'rgba(0, 0, 0, 0.25)' },
3602
+ cancelText: confirmText,
3603
+ okText: cancelText,
3604
+ cancelButtonProps: { type: 'primary' },
3605
+ okButtonProps: { type: 'default' },
3606
+ onCancel: (close) => { onConfirm(); close(); },
3607
+ // onOk (the right "Cancel" button) just closes the modal — default behaviour
3608
+ });
3609
+ }
3610
+
3462
3611
  populateTemplatesList = (data, blankTemplateRequired, layoutSelection) => {
3463
3612
  if (!data) {
3464
3613
  return [];
@@ -3608,20 +3757,78 @@ return (<div>
3608
3757
  deleteOption = this.props.intl.formatMessage(messages.unMapButton);
3609
3758
  }
3610
3759
  if (!layoutSelection) {
3760
+ // Determine archive eligibility for this template:
3761
+ // - Zalo: never eligible
3762
+ // - WhatsApp: not eligible when status is awaitingApproval / pending / unsubmitted
3763
+ // - RCS: not eligible when status is awaitingApproval / pending
3764
+ const templateWhatsappStatus = get(template, `versions.base.content.${WHATSAPP_LOWERCASE}.status`, '');
3765
+ const templateRcsStatus = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0].Status', '');
3766
+ const channelUpCase = this.state.channel.toUpperCase();
3767
+ const isTemplateArchiveEligible = channelUpCase !== ZALO &&
3768
+ !(channelUpCase === WHATSAPP && [WHATSAPP_STATUSES.awaitingApproval, WHATSAPP_STATUSES.pending, WHATSAPP_STATUSES.unsubmitted].includes(templateWhatsappStatus)) &&
3769
+ !(channelUpCase === RCS && [RCS_STATUSES.awaitingApproval, RCS_STATUSES.pending].includes(templateRcsStatus));
3770
+
3771
+ // Checkbox on card header (full mode only, only for archive-eligible templates)
3772
+ if (this.props.isFullMode && this.props.location.query.type !== 'embedded' && isTemplateArchiveEligible) {
3773
+ const selectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
3774
+ const selectedIdsArray = selectedIds.toJS ? selectedIds.toJS() : selectedIds;
3775
+ temp.cardTop = (
3776
+ <div className="template-card-top-bar">
3777
+ <CapCheckbox
3778
+ checked={selectedIdsArray.includes(template._id)}
3779
+ onChange={() => this.props.actions.toggleTemplateSelection(template._id)}
3780
+ onClick={(e) => e.stopPropagation()}
3781
+ />
3782
+ </div>
3783
+ );
3784
+ }
3785
+
3611
3786
  temp.footer = (
3612
3787
  <div className="footer-container">
3613
3788
  <div className="card-title">
3614
3789
  <span className="template-name" style={{ fontWeight: `${this.state.channel.toLowerCase() === 'wechat' ? '400' : '600'}` }}>
3615
3790
  { 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
3791
  {template?.name}
3792
+ {template.isArchived && <CapColoredTag tagColor={CAP_G08} tagTextColor={CAP_G05} className="archived-tag">{this.props.intl.formatMessage(messages.archivedTag)}</CapColoredTag>}
3617
3793
  </span>
3618
3794
  {this.props.location.query.type !== 'embedded' && <CapPopover
3619
3795
  trigger="click"
3620
3796
  content={
3621
3797
  <div className="popover-content">
3622
- {this.state.channel !== 'wechat' && <div className="popover-action-container">
3798
+ {this.state.channel !== 'wechat' && !template.isArchived && <div className="popover-action-container">
3623
3799
  <span onClick={() => this.duplicateTemplate(template)} className="popover-action" style={{cursor: 'pointer', padding: '8px 0px'}}>{this.props.intl.formatMessage(messages.duplicateButton)}</span>
3624
3800
  </div>}
3801
+ {this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
3802
+ <span
3803
+ onClick={() => {
3804
+ this.showArchiveConfirm({
3805
+ title: this.props.intl.formatMessage(messages.archiveTemplates),
3806
+ onConfirm: () => this.props.actions.archiveTemplate(this.state.channel, template._id, template.name),
3807
+ count: 1,
3808
+ });
3809
+ }}
3810
+ className="popover-action popover-archive-action"
3811
+ >
3812
+ <CapIcon type="archive" size="l" />
3813
+ {this.props.intl.formatMessage(messages.archiveButton)}
3814
+ </span>
3815
+ </div>}
3816
+ {this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
3817
+ <span
3818
+ onClick={() => {
3819
+ this.showArchiveConfirm({
3820
+ title: this.props.intl.formatMessage(messages.unarchiveTemplates),
3821
+ onConfirm: () => this.props.actions.unarchiveTemplate(this.state.channel, template._id, template.name),
3822
+ count: 1,
3823
+ isUnarchive: true,
3824
+ });
3825
+ }}
3826
+ className="popover-action popover-archive-action"
3827
+ >
3828
+ <CapIcon type="archive" size="l" />
3829
+ {this.props.intl.formatMessage(messages.unarchiveButton)}
3830
+ </span>
3831
+ </div>}
3625
3832
  <div className="popover-action-container">
3626
3833
  <span onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action" style={{cursor: 'pointer', padding: '8px 0px'}}>{deleteOption}</span>
3627
3834
  </div>
@@ -4214,11 +4421,16 @@ return (<div>
4214
4421
  if (([WHATSAPP_LOWERCASE, ZALO_LOWERCASE, RCS_LOWERCASE].includes(this.state?.channel?.toLocaleLowerCase()) && isEmpty(this.state?.hostName))) {
4215
4422
  isfilterContentVisisble = false;
4216
4423
  }
4424
+ const _isArchivedMode = get(this.props, 'Templates.isArchivedMode', false);
4425
+ const _renderSelectedIds = get(this.props, 'Templates.selectedTemplateIds', []);
4426
+ const _renderSelectedIdsArray = _renderSelectedIds && typeof _renderSelectedIds.toJS === 'function' ? _renderSelectedIds.toJS() : (Array.isArray(_renderSelectedIds) ? _renderSelectedIds : []);
4427
+ const _renderHasSelection = this.props.isFullMode && _renderSelectedIdsArray.length > 0;
4428
+
4217
4429
  const filterContent = (( isfilterContentVisisble || [WECHAT, MOBILE_PUSH, INAPP].includes(this.state.channel.toUpperCase())) && <div className="action-container">
4218
4430
  {isfilterContentVisisble && <CapInput.Search
4219
4431
  className="search-text"
4220
4432
  style={{width: '210px'}}
4221
- placeholder={this.props.intl.formatMessage(messages.searchText)}
4433
+ placeholder={_isArchivedMode ? this.props.intl.formatMessage(messages.searchArchivedTemplates) : this.props.intl.formatMessage(messages.searchText)}
4222
4434
  value={this.state.searchText}
4223
4435
  onChange={(e) => this.searchTemplate(e.target.value, this.state.channel)}
4224
4436
  disabled={this.checkSearchDisabled()}
@@ -4370,9 +4582,9 @@ return (<div>
4370
4582
  </div>
4371
4583
  )
4372
4584
  }
4373
- <div style={{display: "flex", justifyContent: "space-between", alignItems: 'center'}}>
4374
- {
4375
- this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded)? (
4585
+ <div className="template-listing-header-actions">
4586
+ {!_isArchivedMode && !_renderHasSelection && (
4587
+ this.state?.channel?.toLowerCase() === WHATSAPP_LOWERCASE && (isWhatsappCountExeeded) ? (
4376
4588
  <CapTooltip title={whatsappCountExceedText}>
4377
4589
  <div className="button-disabled-tooltip-wrapper">
4378
4590
  {createButton}
@@ -4380,9 +4592,35 @@ return (<div>
4380
4592
  </CapTooltip>
4381
4593
  )
4382
4594
  : isfilterContentVisisble && !isWechatEmbedded && !this.props.isDltFromRcs && createButton
4383
- }
4595
+ )}
4596
+ {/* More (⋯) menu: full mode only, not archived mode, not Zalo (no archive support), not when selection active */}
4597
+ {!_isArchivedMode && !_renderHasSelection && this.props.isFullMode && this.props.location.query.type !== 'embedded' && channelLowerCase !== ZALO_LOWERCASE && (
4598
+ <CapDropdown
4599
+ trigger={['click']}
4600
+ overlay={
4601
+ <CapMenu>
4602
+ <CapMenu.Item
4603
+ key="archived"
4604
+ onClick={() => {
4605
+ this.props.actions.setArchivedMode(true);
4606
+ this.setState({ searchText: '', page: 1 }, () => {
4607
+ const params = { name: '', sortBy: this.state.sortBy, archiveStatus: 'archived' };
4608
+ this.getAllTemplates({ params, resetPage: true }, true);
4609
+ });
4610
+ }}
4611
+ >
4612
+ <FormattedMessage {...messages.archivedTemplates} />
4613
+ </CapMenu.Item>
4614
+ </CapMenu>
4615
+ }
4616
+ placement="bottomRight"
4617
+ >
4618
+ <CapButton type="flat" className="template-listing-more-btn">
4619
+ <CapIcon type="more" />
4620
+ </CapButton>
4621
+ </CapDropdown>
4622
+ )}
4384
4623
  </div>
4385
-
4386
4624
  </div>);
4387
4625
  let htmlPreviewContent = "";
4388
4626
  if (this.state.channel.toLowerCase() === 'ebill') {
@@ -4431,6 +4669,24 @@ return (<div>
4431
4669
  }
4432
4670
  />
4433
4671
 
4672
+ {/* Archived mode header with back arrow (full mode only) */}
4673
+ {this.props.isFullMode && get(this.props, 'Templates.isArchivedMode', false) && (
4674
+ <CapRow type="flex" align="middle" className="archived-mode-header">
4675
+ <CapIcon
4676
+ type="back"
4677
+ className="archived-mode-back-icon"
4678
+ onClick={() => {
4679
+ this.props.actions.setArchivedMode(false);
4680
+ this.setState({ searchText: '', page: 1 }, () => {
4681
+ const params = { name: '', sortBy: this.state.sortBy, archiveStatus: 'active' };
4682
+ this.getAllTemplates({ params, resetPage: true }, true);
4683
+ });
4684
+ }}
4685
+ />
4686
+ <CapHeading type="h3"><FormattedMessage {...messages.archivedTemplates} /></CapHeading>
4687
+ </CapRow>
4688
+ )}
4689
+
4434
4690
  {channel.toLowerCase() === WHATSAPP_LOWERCASE &&
4435
4691
  showWhatsappCountWarning ? (
4436
4692
  <CapAlert message={whatsappCountExceedText} type="info" />
@@ -618,4 +618,72 @@ export default defineMessages({
618
618
  id: `${scope}.templateUpdateSuccess`,
619
619
  defaultMessage: 'Template updated successfully',
620
620
  },
621
+ "archiveTemplates": {
622
+ id: `${scope}.archiveTemplates`,
623
+ defaultMessage: 'Archive templates',
624
+ },
625
+ "archiveTemplateContent": {
626
+ id: `${scope}.archiveTemplateContent`,
627
+ defaultMessage: 'These templates will be archived and unavailable for use. You can restore them anytime.',
628
+ },
629
+ "archiveTemplateSingleContent": {
630
+ id: `${scope}.archiveTemplateSingleContent`,
631
+ defaultMessage: 'This template will be archived and unavailable for use. You can restore it anytime.',
632
+ },
633
+ "unarchiveTemplates": {
634
+ id: `${scope}.unarchiveTemplates`,
635
+ defaultMessage: 'Unarchive templates',
636
+ },
637
+ "unarchiveTemplateContent": {
638
+ id: `${scope}.unarchiveTemplateContent`,
639
+ defaultMessage: 'These templates will be unarchived and available for use again.',
640
+ },
641
+ "unarchiveTemplateSingleContent": {
642
+ id: `${scope}.unarchiveTemplateSingleContent`,
643
+ defaultMessage: 'This template will be unarchived and available for use again.',
644
+ },
645
+ "archiveConfirmOk": {
646
+ id: `${scope}.archiveConfirmOk`,
647
+ defaultMessage: 'Confirm',
648
+ },
649
+ "archiveConfirmCancel": {
650
+ id: `${scope}.archiveConfirmCancel`,
651
+ defaultMessage: 'Cancel',
652
+ },
653
+ "archiveButton": {
654
+ id: `${scope}.archiveButton`,
655
+ defaultMessage: 'Archive',
656
+ },
657
+ "unarchiveButton": {
658
+ id: `${scope}.unarchiveButton`,
659
+ defaultMessage: 'Unarchive',
660
+ },
661
+ "archivedTag": {
662
+ id: `${scope}.archivedTag`,
663
+ defaultMessage: 'Archived',
664
+ },
665
+ "archivedTemplates": {
666
+ id: `${scope}.archivedTemplates`,
667
+ defaultMessage: 'Archived templates',
668
+ },
669
+ "searchArchivedTemplates": {
670
+ id: `${scope}.searchArchivedTemplates`,
671
+ defaultMessage: 'Search archived templates...',
672
+ },
673
+ "templatesSelected": {
674
+ id: `${scope}.templatesSelected`,
675
+ defaultMessage: '{count} {count, plural, one {template} other {templates}} selected',
676
+ },
677
+ "cannotEditArchivedTemplate": {
678
+ id: `${scope}.cannotEditArchivedTemplate`,
679
+ defaultMessage: 'Cannot edit an archived template. Please unarchive it first.',
680
+ },
681
+ "noArchivedCreatives": {
682
+ id: `${scope}.noArchivedCreatives`,
683
+ defaultMessage: 'No archived creatives',
684
+ },
685
+ "noArchivedCreativesDesc": {
686
+ id: `${scope}.noArchivedCreativesDesc`,
687
+ defaultMessage: 'Creatives you archive will appear here',
688
+ },
621
689
  });
@@ -26,6 +26,13 @@ export const initialState = fromJS({
26
26
  reportsSettings: {},
27
27
  },
28
28
  senderDetails: {},
29
+ archiveFilter: 'active',
30
+ isArchivedMode: false,
31
+ selectedTemplateIds: fromJS([]),
32
+ archiveInProgress: false,
33
+ unarchiveInProgress: false,
34
+ bulkArchiveInProgress: false,
35
+ bulkUnarchiveInProgress: false,
29
36
  });
30
37
 
31
38
  function templatesReducer(state = initialState, action) {
@@ -231,6 +238,67 @@ function templatesReducer(state = initialState, action) {
231
238
  hostName: '',
232
239
  errors: action.payload,
233
240
  });
241
+ case types.SET_ARCHIVE_FILTER:
242
+ return state
243
+ .set('archiveFilter', action.filter)
244
+ .set('templates', [])
245
+ .set('selectedTemplateIds', fromJS([]));
246
+ case types.SET_ARCHIVED_MODE:
247
+ return state
248
+ .set('isArchivedMode', action.isArchived)
249
+ .set('archiveFilter', action.isArchived ? 'archived' : 'active')
250
+ .set('templates', [])
251
+ .set('selectedTemplateIds', fromJS([]));
252
+ case types.TOGGLE_TEMPLATE_SELECTION: {
253
+ const rawSelected = state.get('selectedTemplateIds');
254
+ // Defensive: handle undefined (stale persisted state) or plain array (pre-fromJS migration)
255
+ const currentSelected = rawSelected && typeof rawSelected.toJS === 'function'
256
+ ? rawSelected.toJS()
257
+ : (Array.isArray(rawSelected) ? rawSelected : []);
258
+ const idx = currentSelected.indexOf(action.id);
259
+ const newSelected = idx === -1
260
+ ? [...currentSelected, action.id]
261
+ : currentSelected.filter((id) => id !== action.id);
262
+ return state.set('selectedTemplateIds', fromJS(newSelected));
263
+ }
264
+ case types.SELECT_ALL_TEMPLATES:
265
+ return state.set('selectedTemplateIds', fromJS(action.ids));
266
+ case types.CLEAR_TEMPLATE_SELECTION:
267
+ return state.set('selectedTemplateIds', fromJS([]));
268
+ case types.ARCHIVE_TEMPLATE_REQUEST:
269
+ return state.set('archiveInProgress', true);
270
+ case types.ARCHIVE_TEMPLATE_SUCCESS: {
271
+ const afterArchive = state.get('selectedTemplateIds');
272
+ const archiveSelected = afterArchive && typeof afterArchive.toJS === 'function' ? afterArchive.toJS() : [];
273
+ return state
274
+ .set('archiveInProgress', false)
275
+ .set('selectedTemplateIds', fromJS(archiveSelected.filter((sid) => sid !== action.id)));
276
+ }
277
+ case types.ARCHIVE_TEMPLATE_FAILURE:
278
+ return state.set('archiveInProgress', false);
279
+ case types.UNARCHIVE_TEMPLATE_REQUEST:
280
+ return state.set('unarchiveInProgress', true);
281
+ case types.UNARCHIVE_TEMPLATE_SUCCESS: {
282
+ const afterUnarchive = state.get('selectedTemplateIds');
283
+ const unarchiveSelected = afterUnarchive && typeof afterUnarchive.toJS === 'function' ? afterUnarchive.toJS() : [];
284
+ return state
285
+ .set('unarchiveInProgress', false)
286
+ .set('selectedTemplateIds', fromJS(unarchiveSelected.filter((sid) => sid !== action.id)));
287
+ }
288
+ case types.UNARCHIVE_TEMPLATE_FAILURE:
289
+ return state.set('unarchiveInProgress', false);
290
+ case types.BULK_ARCHIVE_REQUEST:
291
+ return state.set('bulkArchiveInProgress', true);
292
+ case types.BULK_ARCHIVE_SUCCESS:
293
+ return state.set('bulkArchiveInProgress', false).set('selectedTemplateIds', fromJS([]));
294
+ case types.BULK_ARCHIVE_FAILURE:
295
+ return state.set('bulkArchiveInProgress', false);
296
+ case types.BULK_UNARCHIVE_REQUEST:
297
+ return state.set('bulkUnarchiveInProgress', true);
298
+ case types.BULK_UNARCHIVE_SUCCESS:
299
+ return state.set('bulkUnarchiveInProgress', false).set('selectedTemplateIds', fromJS([]));
300
+ case types.BULK_UNARCHIVE_FAILURE:
301
+ return state.set('bulkUnarchiveInProgress', false);
234
302
  default:
235
303
  return state;
236
304
  }