@capillarytech/creatives-library 9.0.36-alpha.0 → 9.0.37-alpha.0

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 (50) hide show
  1. package/package.json +1 -1
  2. package/translations/en.json +2 -2
  3. package/utils/permissions.js +59 -0
  4. package/utils/tests/permissions.test.js +116 -0
  5. package/v2Components/AccessForbidden/_accessForbidden.scss +14 -0
  6. package/v2Components/AccessForbidden/index.js +5 -3
  7. package/v2Components/AccessForbidden/messages.js +3 -2
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +15 -108
  9. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +1 -139
  10. package/v2Components/CommonTestAndPreview/index.js +26 -240
  11. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +0 -364
  12. package/v2Components/NoPermissionWrapper/_noPermissionWrapper.scss +12 -0
  13. package/v2Components/NoPermissionWrapper/index.js +26 -0
  14. package/v2Containers/App/constants.js +5 -0
  15. package/v2Containers/Assets/Gallery/_gallery.scss +6 -0
  16. package/v2Containers/Assets/Gallery/index.js +34 -10
  17. package/v2Containers/Cap/index.js +5 -2
  18. package/v2Containers/Cap/messages.js +4 -0
  19. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
  20. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
  21. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
  22. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
  23. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
  24. package/v2Containers/Rcs/carouselUtils.js +73 -46
  25. package/v2Containers/Rcs/components/CarouselCard.js +9 -6
  26. package/v2Containers/Rcs/constants.js +7 -2
  27. package/v2Containers/Rcs/index.js +126 -72
  28. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +43 -5
  29. package/v2Containers/Rcs/tests/CarouselCard.test.js +2 -1
  30. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +118 -118
  31. package/v2Containers/Rcs/tests/carouselUtils.test.js +15 -15
  32. package/v2Containers/Rcs/tests/index.test.js +232 -10
  33. package/v2Containers/Rcs/tests/utils.test.js +17 -0
  34. package/v2Containers/Rcs/utils.js +51 -12
  35. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
  36. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -18
  37. package/v2Containers/Templates/ChannelTypeIllustration.js +10 -1
  38. package/v2Containers/Templates/_templates.scss +6 -114
  39. package/v2Containers/Templates/index.js +151 -190
  40. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +7 -0
  41. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +173 -120
  42. package/v2Containers/TemplatesV2/index.js +16 -5
  43. package/v2Containers/Viber/constants.js +0 -21
  44. package/v2Containers/Viber/index.js +49 -719
  45. package/v2Containers/Viber/index.scss +0 -175
  46. package/v2Containers/Viber/messages.js +0 -121
  47. package/v2Containers/Viber/tests/index.test.js +0 -80
  48. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +268 -268
  49. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +0 -132
  50. package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +0 -131
@@ -73,7 +73,6 @@ import * as ebillActions from '../Ebill/actions';
73
73
  import * as emailActions from '../Email/actions';
74
74
  import * as lineActions from '../Line/Container/actions';
75
75
  import * as viberActions from '../Viber/actions';
76
- import { VIBER_MEDIA_TYPES } from '../Viber/constants';
77
76
  import * as facebookActions from '../Facebook/actions';
78
77
  import * as whatsappActions from '../Whatsapp/actions';
79
78
  import * as rcsActions from '../Rcs/actions';
@@ -98,6 +97,9 @@ import WhatsappStatusContainer from '../../v2Components/WhatsappStatusContainer'
98
97
  import WechatRichmediaTemplatePreview from '../../v2Components/TemplatePreview/WechatRichmediaTemplatePreview';
99
98
  import CapCustomSkeleton from '../../v2Components/CapCustomSkeleton';
100
99
  import ChannelTypeIllustration from './ChannelTypeIllustration';
100
+ import NoPermissionWrapper from '../../v2Components/NoPermissionWrapper';
101
+ import { getUserAccess, hasPermission, PERMISSIONS } from '../../utils/permissions';
102
+
101
103
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
102
104
  import creativesMessages from '../CreativesContainer/messages';
103
105
  import {
@@ -1397,11 +1399,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1397
1399
  const image = viberContent.image || {};
1398
1400
  const video = viberContent.video || {};
1399
1401
  const button = viberContent.button || {};
1400
- const messageType = viberContent.type || '';
1401
- const cardsRaw = viberContent.cards;
1402
- const cards = Array.isArray(cardsRaw) ? cardsRaw : [];
1403
- const isCarousel =
1404
- messageType === VIBER_MEDIA_TYPES.CAROUSEL || cards.length > 0;
1405
1402
 
1406
1403
  const viberPreview = {
1407
1404
  messageContent: text,
@@ -1419,39 +1416,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1419
1416
  };
1420
1417
  }
1421
1418
 
1422
- if (isCarousel) {
1423
- viberPreview.type = VIBER_MEDIA_TYPES.CAROUSEL;
1424
- viberPreview.cards = cards.map((card) => ({
1425
- text: card?.text || '',
1426
- mediaUrl: card?.mediaUrl || '',
1427
- buttons: (card?.buttons || []).map((btn) => ({
1428
- title: btn?.title || '',
1429
- action: btn?.action || '',
1430
- })),
1431
- }));
1432
- viberPreview.showCarouselEditorPreview = true;
1433
- }
1434
-
1435
1419
  // Extract account name
1436
1420
  const accountName = get(template, 'definition.accountName', '');
1437
1421
 
1438
1422
  // Return Viber content object (same as Viber/index.js getTemplateContent)
1439
1423
  return {
1440
1424
  viberPreviewContent: viberPreview,
1441
- accountName: accountName || '',
1442
- brandName: accountName || '',
1443
- messageContent: text,
1444
- ...(isCarousel && {
1445
- type: VIBER_MEDIA_TYPES.CAROUSEL,
1446
- cards: viberPreview.cards,
1447
- }),
1448
- ...(!isCarousel && !isEmpty(button) && button.text && (button.url || viberContent.buttonURL) && {
1449
- button: {
1450
- text: button.text,
1451
- url: button.url || viberContent.buttonURL || '',
1452
- },
1453
- }),
1454
- buttonURL: button.url || viberContent.buttonURL || '',
1425
+ accountName: accountName ? [accountName] : [],
1426
+ brandName: accountName ? [accountName] : [],
1455
1427
  };
1456
1428
  }
1457
1429
 
@@ -2002,7 +1974,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2002
1974
  }
2003
1975
  />
2004
1976
  ) : (
2005
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={this.state?.channel?.toUpperCase()} hostName={this.state?.hostName}/>
1977
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={this.state?.channel?.toUpperCase()} hostName={this.state?.hostName}/>
2006
1978
  );
2007
1979
  }
2008
1980
 
@@ -2136,7 +2108,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2136
2108
  );
2137
2109
  })()
2138
2110
  ],
2139
- hoverOption: isArchivedMode ? null : (
2111
+ hoverOption: isArchivedMode || !this.canPerform(PERMISSIONS.CREATIVE_EDIT) ? null : (
2140
2112
  <CapButton
2141
2113
  className={
2142
2114
  this.props.isFullMode
@@ -2200,56 +2172,47 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2200
2172
  {![WECHAT, WHATSAPP, ZALO].includes(
2201
2173
  this.state.channel.toUpperCase()
2202
2174
  ) &&
2203
- !isTraiDltFeature && !template.isArchived && (
2204
- <CapMenu.Item
2205
- className={`duplicate-${channelLowerCase}`}
2206
- onClick={() => this.duplicateTemplate(template)}
2207
- >
2208
- <FormattedMessage {...messages.duplicateButton} />
2209
- </CapMenu.Item>
2210
- )}
2175
+ !isTraiDltFeature && !template.isArchived && this.gatedMenuItem({
2176
+ allowed: this.canPerform(PERMISSIONS.CREATIVE_CREATE),
2177
+ className: `duplicate-${channelLowerCase}`,
2178
+ onClick: () => this.duplicateTemplate(template),
2179
+ label: <FormattedMessage {...messages.duplicateButton} />,
2180
+ })}
2211
2181
  {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2212
2182
  {commonUtil.hasCreativesArchivalEnabled() && (() => {
2213
2183
  const channelUp = this.state.channel.toUpperCase();
2214
2184
  if (!this.isChannelArchiveEligible(channelUp, status, rcsStatus)) return null;
2215
- return !template?.isArchived ? (
2216
- <CapMenu.Item
2217
- className={`archive-${channelLowerCase}`}
2218
- onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
2219
- >
2220
- <FormattedMessage {...messages.archiveButton} />
2221
- </CapMenu.Item>
2222
- ) : (
2223
- <CapMenu.Item
2224
- className={`unarchive-${channelLowerCase}`}
2225
- onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
2226
- >
2227
- <FormattedMessage {...messages.unarchiveButton} />
2228
- </CapMenu.Item>
2229
- );
2185
+ const canArchive = this.canPerform(PERMISSIONS.CREATIVE_DELETE);
2186
+ return !template?.isArchived
2187
+ ? this.gatedMenuItem({
2188
+ allowed: canArchive,
2189
+ className: `archive-${channelLowerCase}`,
2190
+ onClick: () => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name }),
2191
+ label: <FormattedMessage {...messages.archiveButton} />,
2192
+ })
2193
+ : this.gatedMenuItem({
2194
+ allowed: canArchive,
2195
+ className: `unarchive-${channelLowerCase}`,
2196
+ onClick: () => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true }),
2197
+ label: <FormattedMessage {...messages.unarchiveButton} />,
2198
+ });
2230
2199
  })()}
2231
2200
  {/* Delete/Unmap menu item */}
2232
2201
  {(!(
2233
2202
  this.state.channel.toUpperCase() === WHATSAPP &&
2234
2203
  status === WHATSAPP_STATUSES.approved
2235
- ) && ![ZALO].includes(this.state.channel.toUpperCase())) && (
2236
- <CapMenu.Item
2237
- className={
2238
- this.state.channel.toUpperCase() === WECHAT &&
2239
- !template.definition
2240
- ? `unmap-${channelLowerCase}`
2241
- : `delete-${channelLowerCase}`
2242
- }
2243
- onClick={() => this.toggleDeleteTemplateModal(template)}
2244
- >
2245
- {this.state.channel.toUpperCase() === WECHAT &&
2246
- !template.definition ? (
2247
- <FormattedMessage {...messages.unMapButton} />
2248
- ) : (
2249
- <FormattedMessage {...messages.deleteButton} />
2250
- )}
2251
- </CapMenu.Item>
2252
- )}
2204
+ ) && ![ZALO].includes(this.state.channel.toUpperCase())) && this.gatedMenuItem({
2205
+ allowed: this.canPerform(PERMISSIONS.CREATIVE_DELETE),
2206
+ className:
2207
+ this.state.channel.toUpperCase() === WECHAT && !template.definition
2208
+ ? `unmap-${channelLowerCase}`
2209
+ : `delete-${channelLowerCase}`,
2210
+ onClick: () => this.toggleDeleteTemplateModal(template),
2211
+ label:
2212
+ this.state.channel.toUpperCase() === WECHAT && !template.definition
2213
+ ? <FormattedMessage {...messages.unMapButton} />
2214
+ : <FormattedMessage {...messages.deleteButton} />,
2215
+ })}
2253
2216
  </CapMenu>
2254
2217
  }
2255
2218
  >
@@ -2472,52 +2435,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2472
2435
  image = {},
2473
2436
  button = {},
2474
2437
  video = {},
2475
- type = '',
2476
- cards = [],
2477
2438
  } = {},
2478
2439
  } = {},
2479
2440
  } = template.versions.base;
2480
- const isViberCarousel = type === VIBER_MEDIA_TYPES.CAROUSEL;
2481
2441
  templateData.content = text;
2482
- if (isViberCarousel) {
2483
- const previewCards = Array.isArray(cards) ? cards.slice(0, 1) : [];
2484
- templateData.className = 'viber-carousel-static';
2485
- templateData.content = (
2486
- <CapRow className="viber-carousel-static-content">
2487
- <CapRow className="viber-carousel-static-message-box">
2488
- <CapLabel type="label1" className="viber-carousel-static-message">
2489
- {text}
2490
- </CapLabel>
2491
- </CapRow>
2492
- <CapRow className="viber-carousel-static-cards">
2493
- {previewCards.map((card, cardIdx) => (
2494
- <CapRow className="viber-carousel-static-card" key={`viber-static-card-${cardIdx}`}>
2495
- {card?.mediaUrl ? (
2496
- <CapImage src={card.mediaUrl} className="viber-carousel-static-image" />
2497
- ) : (
2498
- <CapRow className="viber-carousel-static-image-placeholder" />
2499
- )}
2500
- <CapLabel type="label1" className="viber-carousel-static-text">
2501
- {card?.text}
2502
- </CapLabel>
2503
- <CapRow className="viber-carousel-static-buttons">
2504
- {(Array.isArray(card?.buttons) && card.buttons.length ? card.buttons : [{}, {}]).slice(0, 2).map((carouselButton, btnIdx) => (
2505
- <CapLabel
2506
- key={`viber-static-btn-${cardIdx}-${btnIdx}`}
2507
- type="label1"
2508
- className={`viber-carousel-static-button ${btnIdx === 1 ? 'viber-carousel-static-button-secondary' : ''}`}
2509
- >
2510
- {(carouselButton?.title || '').trim()}
2511
- </CapLabel>
2512
- ))}
2513
- </CapRow>
2514
- </CapRow>
2515
- ))}
2516
- </CapRow>
2517
- </CapRow>
2518
- );
2519
- break;
2520
- }
2521
2442
  if (!isEmpty(image)) {
2522
2443
  const { url = '' } = image;
2523
2444
  templateData.url = url;
@@ -2820,16 +2741,18 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2820
2741
  <CapLabel type="label2">
2821
2742
  {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2822
2743
  </CapLabel>
2823
- <CapButton
2824
- type="primary"
2825
- className="archive-template-button"
2826
- prefix={<CapIcon type="archive" size="s" />}
2827
- onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2828
- >
2829
- <span className="archive-btn-label">
2830
- <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2831
- </span>
2832
- </CapButton>
2744
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
2745
+ <CapButton
2746
+ type="primary"
2747
+ className="archive-template-button"
2748
+ prefix={<CapIcon type="archive" size="s" />}
2749
+ onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2750
+ >
2751
+ <span className="archive-btn-label">
2752
+ <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2753
+ </span>
2754
+ </CapButton>
2755
+ </NoPermissionWrapper>
2833
2756
  <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2834
2757
  <FormattedMessage {...messages.archiveConfirmCancel} />
2835
2758
  </CapButton>
@@ -2868,14 +2791,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2868
2791
  </div> : (
2869
2792
  showIllustrationForChannel(MOBILE_PUSH_LOWERCASE) &&
2870
2793
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto'} : {}}>
2871
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2794
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2872
2795
  </div>
2873
2796
  )
2874
2797
  }
2875
2798
  {(showWhatsappIllustration || showZaloIllustration) && !get(this.props, 'Templates.isArchivedMode', false) && (
2876
2799
  noLoaderAndSearchText &&
2877
2800
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2878
- {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
2801
+ {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
2879
2802
  {filteredEmptyAndFullModeAs(true) && !isEmpty(templates) && this.state?.hostName && this.whatsappZaloIllustrationText(noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc)}
2880
2803
  {noApprovedWhatsappZaloTemplates && this.whatsappZaloIllustrationText(noApprovedWhatsappZaloTemplatesTitle, showWhatsappIllustration ? noApprovedWhatsappTemplatesDesc : zaloDescIllustration)}
2881
2804
  </div>
@@ -2883,41 +2806,41 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2883
2806
  }
2884
2807
  {showIllustrationForChannel(RCS_LOWERCASE) &&
2885
2808
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2886
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
2809
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel}/>
2887
2810
  {filteredEmptyAndFullModeAs(false) && this.whatsappZaloIllustrationText(noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc)}
2888
2811
  </div>
2889
2812
  }
2890
2813
  {showIllustrationForChannel(INAPP_LOWERCASE) &&
2891
2814
  <div className={`${this.isFullMode() ? 'inapp-illustration-parent' : ''}`}>
2892
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} />
2815
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} />
2893
2816
  </div>
2894
2817
  }
2895
2818
 
2896
2819
  {showIllustrationForChannel(VIBER_LOWERCASE) &&
2897
2820
  <div className={`${this.isFullMode() ? 'viber-illustration-parent' : ''}`}>
2898
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
2821
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel}/>
2899
2822
  </div>
2900
2823
  }
2901
2824
  {showIllustrationForChannel(FB_LOWERCASE) &&
2902
2825
  <div className={`${this.isFullMode() ? 'fb-illustration-parent' : ''}`}>
2903
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
2826
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel}/>
2904
2827
  </div>
2905
2828
  }
2906
2829
  {showIllustrationForChannel(LINE_LOWERCASE) &&
2907
2830
  <div className={`${this.isFullMode() ? 'line-illustration-parent' : ''}`}>
2908
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
2831
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel}/>
2909
2832
  </div>
2910
2833
  }
2911
2834
  {showIllustrationForChannel(WEBPUSH_LOWERCASE) &&
2912
2835
  <div style={this.isFullMode() ? { height: "calc(100vh - 20.3125rem)", overflow: 'auto' } : {}}>
2913
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2836
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2914
2837
  </div>
2915
2838
  }
2916
2839
  {get(this.props, 'Templates.isArchivedMode', false) && isEmpty(templates) && !isLoading && !getAllTemplatesInProgress && isEmpty(this.state.searchText) && (
2917
2840
  <CapRow className={this.isFullMode() ? 'illustration-scroll-wrapper' : ''}>
2918
2841
  <ChannelTypeIllustration
2919
2842
  isFullMode={this.props.isFullMode}
2920
- createTemplate={this.createTemplate}
2843
+ createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}
2921
2844
  currentChannel={currentChannel}
2922
2845
  isArchivedMode
2923
2846
  />
@@ -3226,7 +3149,36 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3226
3149
  }
3227
3150
  };
3228
3151
 
3152
+ canPerform(code) {
3153
+ return hasPermission(getUserAccess(this.props), code);
3154
+ }
3155
+
3156
+ // RBAC: render a dropdown menu item that stays visible but is grayed out,
3157
+ // unclickable, and shows a permission tooltip when the user lacks access
3158
+ // (instead of hiding it, which leaves an empty/no-op 3-dots menu).
3159
+ gatedMenuItem = ({ allowed, className, onClick, label }) => (
3160
+ <CapMenu.Item
3161
+ className={`${className || ''} ${allowed ? '' : 'rbac-disabled'}`.trim()}
3162
+ onClick={() => { if (allowed) onClick(); }}
3163
+ >
3164
+ {allowed ? label : (
3165
+ <CapTooltip title={<FormattedMessage {...globalMessages.noPermissionAction} />}>
3166
+ <span>{label}</span>
3167
+ </CapTooltip>
3168
+ )}
3169
+ </CapMenu.Item>
3170
+ );
3171
+
3172
+ guardWrite(code) {
3173
+ if (this.canPerform(code)) return true;
3174
+ CapNotification.error({
3175
+ message: this.props.intl.formatMessage(globalMessages.noPermissionAction),
3176
+ });
3177
+ return false;
3178
+ }
3179
+
3229
3180
  createTemplate(ev) {
3181
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_CREATE)) return;
3230
3182
  const type = this.props.location.query.type;
3231
3183
  const module = this.props.location.query.module ? this.props.location.query.module : 'default';
3232
3184
  const channel = this.state.channel.toLowerCase();
@@ -3470,6 +3422,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3470
3422
  }
3471
3423
 
3472
3424
  duplicateTemplate(template) {
3425
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_CREATE)) return;
3473
3426
  const duplicateObj = cloneDeep(template);
3474
3427
  duplicateObj.name = `${COPY_OF} ${template?.name} ${moment().format('MM-DD-YYYY HH:mm:ss')}`;
3475
3428
  delete duplicateObj._id;
@@ -3576,6 +3529,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3576
3529
  };
3577
3530
 
3578
3531
  handleEditClick(e, template, modeType, path, options) {
3532
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_EDIT)) return;
3579
3533
  if (template && template.isArchived) {
3580
3534
  CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3581
3535
  return;
@@ -3751,6 +3705,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3751
3705
  }
3752
3706
 
3753
3707
  toggleDeleteTemplateModal = (template) => {
3708
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3754
3709
  this.setState({actionTemplate: template}, () => {
3755
3710
  this.setState({showModal: true});
3756
3711
  });
@@ -3784,6 +3739,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3784
3739
  }
3785
3740
 
3786
3741
  handleBulkArchiveAction = ({ ids, count, isUnarchive = false }) => {
3742
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3787
3743
  const { intl, actions } = this.props;
3788
3744
  const { channel } = this.state;
3789
3745
  const title = isUnarchive
@@ -3803,6 +3759,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3803
3759
  };
3804
3760
 
3805
3761
  handleTemplateArchiveAction = ({ templateId, templateName, isUnarchive = false }) => {
3762
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3806
3763
  const { intl, actions } = this.props;
3807
3764
  const { channel } = this.state;
3808
3765
  const title = isUnarchive
@@ -4035,35 +3992,43 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4035
3992
  trigger="click"
4036
3993
  content={
4037
3994
  <div className="popover-content">
4038
- {this.state.channel !== 'wechat' && !template.isArchived && <div className="popover-action-container">
4039
- <CapButton type="link" onClick={() => this.duplicateTemplate(template)} className="popover-action">
4040
- {this.props.intl.formatMessage(messages.duplicateButton)}
4041
- </CapButton>
3995
+ {this.state.channel !== 'wechat' && !template.isArchived && <div className="popover-action-container">
3996
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}>
3997
+ <CapButton type="link" onClick={() => this.duplicateTemplate(template)} className="popover-action">
3998
+ {this.props.intl.formatMessage(messages.duplicateButton)}
3999
+ </CapButton>
4000
+ </NoPermissionWrapper>
4042
4001
  </div>}
4043
4002
  {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
4044
- <CapButton
4045
- type="link"
4046
- onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
4047
- className="popover-action popover-archive-action"
4048
- >
4049
- <CapIcon type="archive" size="s" />
4050
- <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.archiveButton)}</CapLabel.CapLabelInline>
4051
- </CapButton>
4003
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4004
+ <CapButton
4005
+ type="link"
4006
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
4007
+ className="popover-action popover-archive-action"
4008
+ >
4009
+ <CapIcon type="archive" size="s" />
4010
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.archiveButton)}</CapLabel.CapLabelInline>
4011
+ </CapButton>
4012
+ </NoPermissionWrapper>
4052
4013
  </div>}
4053
4014
  {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
4054
- <CapButton
4055
- type="link"
4056
- onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
4057
- className="popover-action popover-archive-action"
4058
- >
4059
- <CapIcon type="archive" size="s" />
4060
- <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.unarchiveButton)}</CapLabel.CapLabelInline>
4061
- </CapButton>
4015
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4016
+ <CapButton
4017
+ type="link"
4018
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
4019
+ className="popover-action popover-archive-action"
4020
+ >
4021
+ <CapIcon type="archive" size="s" />
4022
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.unarchiveButton)}</CapLabel.CapLabelInline>
4023
+ </CapButton>
4024
+ </NoPermissionWrapper>
4062
4025
  </div>}
4063
4026
  <div className="popover-action-container">
4064
- <CapButton type="link" onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action">
4065
- {deleteOption}
4066
- </CapButton>
4027
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4028
+ <CapButton type="link" onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action">
4029
+ {deleteOption}
4030
+ </CapButton>
4031
+ </NoPermissionWrapper>
4067
4032
  </div>
4068
4033
  </div>
4069
4034
  }
@@ -4665,23 +4630,26 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4665
4630
  && !this.props.isFullMode
4666
4631
  )
4667
4632
  ? (
4668
- <CapLink
4669
- onClick={this.openCreativesFullMode}
4670
- title={formatMessage(messages.createNewTemplateLink)}
4671
- suffix={<CapIcon type="open-in-new" size="s" />}
4672
- className="create-new-link"
4673
- />
4633
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}>
4634
+ <CapLink
4635
+ onClick={this.openCreativesFullMode}
4636
+ title={formatMessage(messages.createNewTemplateLink)}
4637
+ suffix={<CapIcon type="open-in-new" size="s" />}
4638
+ className="create-new-link"
4639
+ />
4640
+ </NoPermissionWrapper>
4674
4641
  )
4675
4642
  : this.state?.channel?.toLowerCase() === ZALO_LOWERCASE || (channelLowerCase === WHATSAPP_LOWERCASE && isEmpty(this.state?.hostName)) ? <></> : (
4676
- <CapButton
4677
- className={`create-new-${channelLowerCase} margin-l-8 margin-b-12`}
4678
- type={"primary"}
4679
- isAddBtn
4680
- disabled={this.isCreateDisabled()}
4681
- onClick={this.createTemplate}
4682
- >
4683
- { this.props.intl.formatMessage((isTraiDltFeature && channel.toUpperCase() === SMS ) ? messages.uploadTemplate : messages.createNewActionButton) }
4684
- </CapButton>
4643
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}>
4644
+ <CapButton
4645
+ className={`create-new-${channelLowerCase} margin-l-8 margin-b-12`}
4646
+ type={"primary"}
4647
+ disabled={this.isCreateDisabled()}
4648
+ onClick={this.createTemplate}
4649
+ >
4650
+ { this.props.intl.formatMessage((isTraiDltFeature && channel.toUpperCase() === SMS ) ? messages.uploadTemplate : messages.createNewActionButton) }
4651
+ </CapButton>
4652
+ </NoPermissionWrapper>
4685
4653
  );
4686
4654
 
4687
4655
  const isWhatsappCountExeeded = templatesCount >= MAX_WHATSAPP_TEMPLATES;
@@ -5138,22 +5106,15 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
5138
5106
  ? this.props.Templates.selectedWhatsappAccount.name
5139
5107
  : null
5140
5108
  }
5141
- // Pass formData for tag extraction (EMAIL/SMS use nested formData; VIBER uses content object)
5109
+ // Pass formData for EMAIL and SMS channels for tag extraction
5142
5110
  formData={
5143
- (() => {
5144
- const previewContent = this.state.testAndPreviewContent;
5145
- const channelUpper = this.state.channel?.toUpperCase();
5146
- if (!previewContent || typeof previewContent !== 'object') {
5147
- return null;
5148
- }
5149
- if ([EMAIL, SMS].includes(channelUpper)) {
5150
- return previewContent.formData || null;
5151
- }
5152
- if (channelUpper === VIBER) {
5153
- return previewContent;
5154
- }
5155
- return null;
5156
- })()
5111
+ this.state.testAndPreviewContent &&
5112
+ (this.state.channel?.toUpperCase() === EMAIL ||
5113
+ this.state.channel?.toUpperCase() === SMS) &&
5114
+ typeof this.state.testAndPreviewContent === 'object' &&
5115
+ this.state.testAndPreviewContent?.formData
5116
+ ? this.state.testAndPreviewContent.formData
5117
+ : null
5157
5118
  }
5158
5119
  />
5159
5120
  )}
@@ -3,6 +3,13 @@ jest.mock('@capillarytech/cap-ui-library', () => ({
3
3
  CapIllustration: ({ children, ...props }) => <div data-testid="cap-illustration" {...props}>{children}</div>
4
4
  }));
5
5
 
6
+ // The component uses useIntl() (context); this shallow test supplies intl via props, so
7
+ // stub useIntl. FormattedMessage stays real.
8
+ jest.mock('react-intl', () => ({
9
+ ...jest.requireActual('react-intl'),
10
+ useIntl: () => ({ formatMessage: m => (m && m.defaultMessage) || '' }),
11
+ }));
12
+
6
13
  // Mock all image imports
7
14
  jest.mock('../../Assets/images/emailIllustration.svg', () => 'test-file-stub');
8
15
  jest.mock('../../Assets/images/smsIllustration.svg', () => 'test-file-stub');