@capillarytech/creatives-library 9.0.35 → 9.0.36-alpha.1

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/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/ViberCarouselPreviewCards.js +132 -0
  9. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +108 -15
  10. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +139 -1
  11. package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +131 -0
  12. package/v2Components/CommonTestAndPreview/index.js +240 -26
  13. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +364 -0
  14. package/v2Components/NoPermissionWrapper/_noPermissionWrapper.scss +12 -0
  15. package/v2Components/NoPermissionWrapper/index.js +26 -0
  16. package/v2Containers/App/constants.js +5 -0
  17. package/v2Containers/Assets/Gallery/_gallery.scss +6 -0
  18. package/v2Containers/Assets/Gallery/index.js +34 -10
  19. package/v2Containers/Cap/index.js +5 -2
  20. package/v2Containers/Cap/messages.js +4 -0
  21. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
  22. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
  23. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
  24. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
  25. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
  26. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +118 -118
  27. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
  28. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -18
  29. package/v2Containers/Templates/ChannelTypeIllustration.js +13 -2
  30. package/v2Containers/Templates/_templates.scss +201 -0
  31. package/v2Containers/Templates/index.js +244 -110
  32. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +1 -1
  33. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +174 -121
  34. package/v2Containers/TemplatesV2/index.js +16 -5
  35. package/v2Containers/Viber/constants.js +21 -0
  36. package/v2Containers/Viber/index.js +719 -49
  37. package/v2Containers/Viber/index.scss +175 -0
  38. package/v2Containers/Viber/messages.js +121 -0
  39. package/v2Containers/Viber/tests/index.test.js +80 -0
  40. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +268 -268
@@ -73,6 +73,7 @@ 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';
76
77
  import * as facebookActions from '../Facebook/actions';
77
78
  import * as whatsappActions from '../Whatsapp/actions';
78
79
  import * as rcsActions from '../Rcs/actions';
@@ -97,6 +98,9 @@ import WhatsappStatusContainer from '../../v2Components/WhatsappStatusContainer'
97
98
  import WechatRichmediaTemplatePreview from '../../v2Components/TemplatePreview/WechatRichmediaTemplatePreview';
98
99
  import CapCustomSkeleton from '../../v2Components/CapCustomSkeleton';
99
100
  import ChannelTypeIllustration from './ChannelTypeIllustration';
101
+ import NoPermissionWrapper from '../../v2Components/NoPermissionWrapper';
102
+ import { getUserAccess, hasPermission, PERMISSIONS } from '../../utils/permissions';
103
+
100
104
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
101
105
  import creativesMessages from '../CreativesContainer/messages';
102
106
  import {
@@ -1396,6 +1400,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1396
1400
  const image = viberContent.image || {};
1397
1401
  const video = viberContent.video || {};
1398
1402
  const button = viberContent.button || {};
1403
+ const messageType = viberContent.type || '';
1404
+ const cardsRaw = viberContent.cards;
1405
+ const cards = Array.isArray(cardsRaw) ? cardsRaw : [];
1406
+ const isCarousel =
1407
+ messageType === VIBER_MEDIA_TYPES.CAROUSEL || cards.length > 0;
1399
1408
 
1400
1409
  const viberPreview = {
1401
1410
  messageContent: text,
@@ -1413,14 +1422,39 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1413
1422
  };
1414
1423
  }
1415
1424
 
1425
+ if (isCarousel) {
1426
+ viberPreview.type = VIBER_MEDIA_TYPES.CAROUSEL;
1427
+ viberPreview.cards = cards.map((card) => ({
1428
+ text: card?.text || '',
1429
+ mediaUrl: card?.mediaUrl || '',
1430
+ buttons: (card?.buttons || []).map((btn) => ({
1431
+ title: btn?.title || '',
1432
+ action: btn?.action || '',
1433
+ })),
1434
+ }));
1435
+ viberPreview.showCarouselEditorPreview = true;
1436
+ }
1437
+
1416
1438
  // Extract account name
1417
1439
  const accountName = get(template, 'definition.accountName', '');
1418
1440
 
1419
1441
  // Return Viber content object (same as Viber/index.js getTemplateContent)
1420
1442
  return {
1421
1443
  viberPreviewContent: viberPreview,
1422
- accountName: accountName ? [accountName] : [],
1423
- brandName: accountName ? [accountName] : [],
1444
+ accountName: accountName || '',
1445
+ brandName: accountName || '',
1446
+ messageContent: text,
1447
+ ...(isCarousel && {
1448
+ type: VIBER_MEDIA_TYPES.CAROUSEL,
1449
+ cards: viberPreview.cards,
1450
+ }),
1451
+ ...(!isCarousel && !isEmpty(button) && button.text && (button.url || viberContent.buttonURL) && {
1452
+ button: {
1453
+ text: button.text,
1454
+ url: button.url || viberContent.buttonURL || '',
1455
+ },
1456
+ }),
1457
+ buttonURL: button.url || viberContent.buttonURL || '',
1424
1458
  };
1425
1459
  }
1426
1460
 
@@ -1971,7 +2005,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1971
2005
  }
1972
2006
  />
1973
2007
  ) : (
1974
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={this.state?.channel?.toUpperCase()} hostName={this.state?.hostName}/>
2008
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={this.state?.channel?.toUpperCase()} hostName={this.state?.hostName}/>
1975
2009
  );
1976
2010
  }
1977
2011
 
@@ -2105,7 +2139,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2105
2139
  );
2106
2140
  })()
2107
2141
  ],
2108
- hoverOption: isArchivedMode ? null : (
2142
+ hoverOption: isArchivedMode || !this.canPerform(PERMISSIONS.CREATIVE_EDIT) ? null : (
2109
2143
  <CapButton
2110
2144
  className={
2111
2145
  this.props.isFullMode
@@ -2169,56 +2203,47 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2169
2203
  {![WECHAT, WHATSAPP, ZALO].includes(
2170
2204
  this.state.channel.toUpperCase()
2171
2205
  ) &&
2172
- !isTraiDltFeature && !template.isArchived && (
2173
- <CapMenu.Item
2174
- className={`duplicate-${channelLowerCase}`}
2175
- onClick={() => this.duplicateTemplate(template)}
2176
- >
2177
- <FormattedMessage {...messages.duplicateButton} />
2178
- </CapMenu.Item>
2179
- )}
2206
+ !isTraiDltFeature && !template.isArchived && this.gatedMenuItem({
2207
+ allowed: this.canPerform(PERMISSIONS.CREATIVE_CREATE),
2208
+ className: `duplicate-${channelLowerCase}`,
2209
+ onClick: () => this.duplicateTemplate(template),
2210
+ label: <FormattedMessage {...messages.duplicateButton} />,
2211
+ })}
2180
2212
  {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2181
2213
  {commonUtil.hasCreativesArchivalEnabled() && (() => {
2182
2214
  const channelUp = this.state.channel.toUpperCase();
2183
2215
  if (!this.isChannelArchiveEligible(channelUp, status, rcsStatus)) return null;
2184
- return !template?.isArchived ? (
2185
- <CapMenu.Item
2186
- className={`archive-${channelLowerCase}`}
2187
- onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
2188
- >
2189
- <FormattedMessage {...messages.archiveButton} />
2190
- </CapMenu.Item>
2191
- ) : (
2192
- <CapMenu.Item
2193
- className={`unarchive-${channelLowerCase}`}
2194
- onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
2195
- >
2196
- <FormattedMessage {...messages.unarchiveButton} />
2197
- </CapMenu.Item>
2198
- );
2216
+ const canArchive = this.canPerform(PERMISSIONS.CREATIVE_DELETE);
2217
+ return !template?.isArchived
2218
+ ? this.gatedMenuItem({
2219
+ allowed: canArchive,
2220
+ className: `archive-${channelLowerCase}`,
2221
+ onClick: () => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name }),
2222
+ label: <FormattedMessage {...messages.archiveButton} />,
2223
+ })
2224
+ : this.gatedMenuItem({
2225
+ allowed: canArchive,
2226
+ className: `unarchive-${channelLowerCase}`,
2227
+ onClick: () => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true }),
2228
+ label: <FormattedMessage {...messages.unarchiveButton} />,
2229
+ });
2199
2230
  })()}
2200
2231
  {/* Delete/Unmap menu item */}
2201
2232
  {(!(
2202
2233
  this.state.channel.toUpperCase() === WHATSAPP &&
2203
2234
  status === WHATSAPP_STATUSES.approved
2204
- ) && ![ZALO].includes(this.state.channel.toUpperCase())) && (
2205
- <CapMenu.Item
2206
- className={
2207
- this.state.channel.toUpperCase() === WECHAT &&
2208
- !template.definition
2209
- ? `unmap-${channelLowerCase}`
2210
- : `delete-${channelLowerCase}`
2211
- }
2212
- onClick={() => this.toggleDeleteTemplateModal(template)}
2213
- >
2214
- {this.state.channel.toUpperCase() === WECHAT &&
2215
- !template.definition ? (
2216
- <FormattedMessage {...messages.unMapButton} />
2217
- ) : (
2218
- <FormattedMessage {...messages.deleteButton} />
2219
- )}
2220
- </CapMenu.Item>
2221
- )}
2235
+ ) && ![ZALO].includes(this.state.channel.toUpperCase())) && this.gatedMenuItem({
2236
+ allowed: this.canPerform(PERMISSIONS.CREATIVE_DELETE),
2237
+ className:
2238
+ this.state.channel.toUpperCase() === WECHAT && !template.definition
2239
+ ? `unmap-${channelLowerCase}`
2240
+ : `delete-${channelLowerCase}`,
2241
+ onClick: () => this.toggleDeleteTemplateModal(template),
2242
+ label:
2243
+ this.state.channel.toUpperCase() === WECHAT && !template.definition
2244
+ ? <FormattedMessage {...messages.unMapButton} />
2245
+ : <FormattedMessage {...messages.deleteButton} />,
2246
+ })}
2222
2247
  </CapMenu>
2223
2248
  }
2224
2249
  >
@@ -2441,10 +2466,65 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2441
2466
  image = {},
2442
2467
  button = {},
2443
2468
  video = {},
2469
+ type = '',
2470
+ cards = [],
2444
2471
  } = {},
2445
2472
  } = {},
2446
2473
  } = template.versions.base;
2474
+ const isViberCarousel = type === VIBER_MEDIA_TYPES.CAROUSEL;
2447
2475
  templateData.content = text;
2476
+ if (isViberCarousel) {
2477
+ const previewCards = Array.isArray(cards) ? cards.slice(0, 1) : [];
2478
+ // Bypass CapCustomCard Viber Meta + line-clamp (clips title/buttons for tall images).
2479
+ // Keep VIBER in className for listing styles; cardType hits the default content path.
2480
+ templateData.cardType = 'VIBER_CAROUSEL_PREVIEW';
2481
+ templateData.className = 'viber-carousel-static VIBER';
2482
+ templateData.style = {
2483
+ height: 'auto',
2484
+ minHeight: '21.125rem',
2485
+ overflow: 'visible',
2486
+ };
2487
+ templateData.bodyStyle = {
2488
+ height: 'auto',
2489
+ overflow: 'visible',
2490
+ paddingBottom: '12px',
2491
+ };
2492
+ templateData.content = (
2493
+ <CapRow vertical className="viber-carousel-static-content">
2494
+ <CapRow className="viber-carousel-static-message-box">
2495
+ <CapLabel type="label1" className="viber-carousel-static-message">
2496
+ {text}
2497
+ </CapLabel>
2498
+ </CapRow>
2499
+ <CapRow className="viber-carousel-static-cards">
2500
+ {previewCards.map((card, cardIdx) => (
2501
+ <CapRow vertical className="viber-carousel-static-card" key={`viber-static-card-${cardIdx}`}>
2502
+ {card?.mediaUrl ? (
2503
+ <CapImage src={card.mediaUrl} className="viber-carousel-static-image" />
2504
+ ) : (
2505
+ <CapRow className="viber-carousel-static-image-placeholder" />
2506
+ )}
2507
+ <CapLabel type="label1" className="viber-carousel-static-text">
2508
+ {card?.text}
2509
+ </CapLabel>
2510
+ <CapRow vertical className="viber-carousel-static-buttons">
2511
+ {(Array.isArray(card?.buttons) && card.buttons.length ? card.buttons : [{}, {}]).slice(0, 2).map((carouselButton, btnIdx) => (
2512
+ <CapLabel
2513
+ key={`viber-static-btn-${cardIdx}-${btnIdx}`}
2514
+ type="label1"
2515
+ className={`viber-carousel-static-button ${btnIdx === 1 ? 'viber-carousel-static-button-secondary' : ''}`}
2516
+ >
2517
+ {(carouselButton?.title || '').trim()}
2518
+ </CapLabel>
2519
+ ))}
2520
+ </CapRow>
2521
+ </CapRow>
2522
+ ))}
2523
+ </CapRow>
2524
+ </CapRow>
2525
+ );
2526
+ break;
2527
+ }
2448
2528
  if (!isEmpty(image)) {
2449
2529
  const { url = '' } = image;
2450
2530
  templateData.url = url;
@@ -2747,16 +2827,18 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2747
2827
  <CapLabel type="label2">
2748
2828
  {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2749
2829
  </CapLabel>
2750
- <CapButton
2751
- type="primary"
2752
- className="archive-template-button"
2753
- prefix={<CapIcon type="archive" size="s" />}
2754
- onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2755
- >
2756
- <span className="archive-btn-label">
2757
- <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2758
- </span>
2759
- </CapButton>
2830
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
2831
+ <CapButton
2832
+ type="primary"
2833
+ className="archive-template-button"
2834
+ prefix={<CapIcon type="archive" size="s" />}
2835
+ onClick={() => this.handleBulkArchiveAction({ ids: selectedIdsArrayLocal, count: selectedCountLocal, isUnarchive: isArchivedModeLocal })}
2836
+ >
2837
+ <span className="archive-btn-label">
2838
+ <FormattedMessage {...(isArchivedModeLocal ? messages.unarchiveButton : messages.archiveButton)} />
2839
+ </span>
2840
+ </CapButton>
2841
+ </NoPermissionWrapper>
2760
2842
  <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2761
2843
  <FormattedMessage {...messages.archiveConfirmCancel} />
2762
2844
  </CapButton>
@@ -2795,14 +2877,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2795
2877
  </div> : (
2796
2878
  showIllustrationForChannel(MOBILE_PUSH_LOWERCASE) &&
2797
2879
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto'} : {}}>
2798
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2880
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2799
2881
  </div>
2800
2882
  )
2801
2883
  }
2802
2884
  {(showWhatsappIllustration || showZaloIllustration) && !get(this.props, 'Templates.isArchivedMode', false) && (
2803
2885
  noLoaderAndSearchText &&
2804
2886
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2805
- {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
2887
+ {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
2806
2888
  {filteredEmptyAndFullModeAs(true) && !isEmpty(templates) && this.state?.hostName && this.whatsappZaloIllustrationText(noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc)}
2807
2889
  {noApprovedWhatsappZaloTemplates && this.whatsappZaloIllustrationText(noApprovedWhatsappZaloTemplatesTitle, showWhatsappIllustration ? noApprovedWhatsappTemplatesDesc : zaloDescIllustration)}
2808
2890
  </div>
@@ -2810,41 +2892,41 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2810
2892
  }
2811
2893
  {showIllustrationForChannel(RCS_LOWERCASE) &&
2812
2894
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2813
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
2895
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel}/>
2814
2896
  {filteredEmptyAndFullModeAs(false) && this.whatsappZaloIllustrationText(noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc)}
2815
2897
  </div>
2816
2898
  }
2817
2899
  {showIllustrationForChannel(INAPP_LOWERCASE) &&
2818
2900
  <div className={`${this.isFullMode() ? 'inapp-illustration-parent' : ''}`}>
2819
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} />
2901
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} />
2820
2902
  </div>
2821
2903
  }
2822
2904
 
2823
2905
  {showIllustrationForChannel(VIBER_LOWERCASE) &&
2824
2906
  <div className={`${this.isFullMode() ? 'viber-illustration-parent' : ''}`}>
2825
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
2907
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel}/>
2826
2908
  </div>
2827
2909
  }
2828
2910
  {showIllustrationForChannel(FB_LOWERCASE) &&
2829
2911
  <div className={`${this.isFullMode() ? 'fb-illustration-parent' : ''}`}>
2830
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
2912
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel}/>
2831
2913
  </div>
2832
2914
  }
2833
2915
  {showIllustrationForChannel(LINE_LOWERCASE) &&
2834
2916
  <div className={`${this.isFullMode() ? 'line-illustration-parent' : ''}`}>
2835
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel}/>
2917
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel}/>
2836
2918
  </div>
2837
2919
  }
2838
2920
  {showIllustrationForChannel(WEBPUSH_LOWERCASE) &&
2839
2921
  <div style={this.isFullMode() ? { height: "calc(100vh - 20.3125rem)", overflow: 'auto' } : {}}>
2840
- <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2922
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2841
2923
  </div>
2842
2924
  }
2843
2925
  {get(this.props, 'Templates.isArchivedMode', false) && isEmpty(templates) && !isLoading && !getAllTemplatesInProgress && isEmpty(this.state.searchText) && (
2844
2926
  <CapRow className={this.isFullMode() ? 'illustration-scroll-wrapper' : ''}>
2845
2927
  <ChannelTypeIllustration
2846
2928
  isFullMode={this.props.isFullMode}
2847
- createTemplate={this.createTemplate}
2929
+ createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}
2848
2930
  currentChannel={currentChannel}
2849
2931
  isArchivedMode
2850
2932
  />
@@ -3153,7 +3235,36 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3153
3235
  }
3154
3236
  };
3155
3237
 
3238
+ canPerform(code) {
3239
+ return hasPermission(getUserAccess(this.props), code);
3240
+ }
3241
+
3242
+ // RBAC: render a dropdown menu item that stays visible but is grayed out,
3243
+ // unclickable, and shows a permission tooltip when the user lacks access
3244
+ // (instead of hiding it, which leaves an empty/no-op 3-dots menu).
3245
+ gatedMenuItem = ({ allowed, className, onClick, label }) => (
3246
+ <CapMenu.Item
3247
+ className={`${className || ''} ${allowed ? '' : 'rbac-disabled'}`.trim()}
3248
+ onClick={() => { if (allowed) onClick(); }}
3249
+ >
3250
+ {allowed ? label : (
3251
+ <CapTooltip title={<FormattedMessage {...globalMessages.noPermissionAction} />}>
3252
+ <span>{label}</span>
3253
+ </CapTooltip>
3254
+ )}
3255
+ </CapMenu.Item>
3256
+ );
3257
+
3258
+ guardWrite(code) {
3259
+ if (this.canPerform(code)) return true;
3260
+ CapNotification.error({
3261
+ message: this.props.intl.formatMessage(globalMessages.noPermissionAction),
3262
+ });
3263
+ return false;
3264
+ }
3265
+
3156
3266
  createTemplate(ev) {
3267
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_CREATE)) return;
3157
3268
  const type = this.props.location.query.type;
3158
3269
  const module = this.props.location.query.module ? this.props.location.query.module : 'default';
3159
3270
  const channel = this.state.channel.toLowerCase();
@@ -3397,6 +3508,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3397
3508
  }
3398
3509
 
3399
3510
  duplicateTemplate(template) {
3511
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_CREATE)) return;
3400
3512
  const duplicateObj = cloneDeep(template);
3401
3513
  duplicateObj.name = `${COPY_OF} ${template?.name} ${moment().format('MM-DD-YYYY HH:mm:ss')}`;
3402
3514
  delete duplicateObj._id;
@@ -3503,6 +3615,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3503
3615
  };
3504
3616
 
3505
3617
  handleEditClick(e, template, modeType, path, options) {
3618
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_EDIT)) return;
3506
3619
  if (template && template.isArchived) {
3507
3620
  CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3508
3621
  return;
@@ -3678,6 +3791,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3678
3791
  }
3679
3792
 
3680
3793
  toggleDeleteTemplateModal = (template) => {
3794
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3681
3795
  this.setState({actionTemplate: template}, () => {
3682
3796
  this.setState({showModal: true});
3683
3797
  });
@@ -3711,6 +3825,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3711
3825
  }
3712
3826
 
3713
3827
  handleBulkArchiveAction = ({ ids, count, isUnarchive = false }) => {
3828
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3714
3829
  const { intl, actions } = this.props;
3715
3830
  const { channel } = this.state;
3716
3831
  const title = isUnarchive
@@ -3730,6 +3845,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3730
3845
  };
3731
3846
 
3732
3847
  handleTemplateArchiveAction = ({ templateId, templateName, isUnarchive = false }) => {
3848
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3733
3849
  const { intl, actions } = this.props;
3734
3850
  const { channel } = this.state;
3735
3851
  const title = isUnarchive
@@ -3962,35 +4078,43 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3962
4078
  trigger="click"
3963
4079
  content={
3964
4080
  <div className="popover-content">
3965
- {this.state.channel !== 'wechat' && !template.isArchived && <div className="popover-action-container">
3966
- <CapButton type="link" onClick={() => this.duplicateTemplate(template)} className="popover-action">
3967
- {this.props.intl.formatMessage(messages.duplicateButton)}
3968
- </CapButton>
4081
+ {this.state.channel !== 'wechat' && !template.isArchived && <div className="popover-action-container">
4082
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}>
4083
+ <CapButton type="link" onClick={() => this.duplicateTemplate(template)} className="popover-action">
4084
+ {this.props.intl.formatMessage(messages.duplicateButton)}
4085
+ </CapButton>
4086
+ </NoPermissionWrapper>
3969
4087
  </div>}
3970
4088
  {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && !template.isArchived && <div className="popover-action-container">
3971
- <CapButton
3972
- type="link"
3973
- onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
3974
- className="popover-action popover-archive-action"
3975
- >
3976
- <CapIcon type="archive" size="s" />
3977
- <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.archiveButton)}</CapLabel.CapLabelInline>
3978
- </CapButton>
4089
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4090
+ <CapButton
4091
+ type="link"
4092
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name })}
4093
+ className="popover-action popover-archive-action"
4094
+ >
4095
+ <CapIcon type="archive" size="s" />
4096
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.archiveButton)}</CapLabel.CapLabelInline>
4097
+ </CapButton>
4098
+ </NoPermissionWrapper>
3979
4099
  </div>}
3980
4100
  {commonUtil.hasCreativesArchivalEnabled() && this.props.isFullMode && isTemplateArchiveEligible && template.isArchived && <div className="popover-action-container">
3981
- <CapButton
3982
- type="link"
3983
- onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
3984
- className="popover-action popover-archive-action"
3985
- >
3986
- <CapIcon type="archive" size="s" />
3987
- <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.unarchiveButton)}</CapLabel.CapLabelInline>
3988
- </CapButton>
4101
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4102
+ <CapButton
4103
+ type="link"
4104
+ onClick={() => this.handleTemplateArchiveAction({ templateId: template._id, templateName: template.name, isUnarchive: true })}
4105
+ className="popover-action popover-archive-action"
4106
+ >
4107
+ <CapIcon type="archive" size="s" />
4108
+ <CapLabel.CapLabelInline type="label1">{this.props.intl.formatMessage(messages.unarchiveButton)}</CapLabel.CapLabelInline>
4109
+ </CapButton>
4110
+ </NoPermissionWrapper>
3989
4111
  </div>}
3990
4112
  <div className="popover-action-container">
3991
- <CapButton type="link" onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action">
3992
- {deleteOption}
3993
- </CapButton>
4113
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_DELETE)}>
4114
+ <CapButton type="link" onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action">
4115
+ {deleteOption}
4116
+ </CapButton>
4117
+ </NoPermissionWrapper>
3994
4118
  </div>
3995
4119
  </div>
3996
4120
  }
@@ -4592,23 +4716,26 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4592
4716
  && !this.props.isFullMode
4593
4717
  )
4594
4718
  ? (
4595
- <CapLink
4596
- onClick={this.openCreativesFullMode}
4597
- title={formatMessage(messages.createNewTemplateLink)}
4598
- suffix={<CapIcon type="open-in-new" size="s" />}
4599
- className="create-new-link"
4600
- />
4719
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}>
4720
+ <CapLink
4721
+ onClick={this.openCreativesFullMode}
4722
+ title={formatMessage(messages.createNewTemplateLink)}
4723
+ suffix={<CapIcon type="open-in-new" size="s" />}
4724
+ className="create-new-link"
4725
+ />
4726
+ </NoPermissionWrapper>
4601
4727
  )
4602
4728
  : this.state?.channel?.toLowerCase() === ZALO_LOWERCASE || (channelLowerCase === WHATSAPP_LOWERCASE && isEmpty(this.state?.hostName)) ? <></> : (
4603
- <CapButton
4604
- className={`create-new-${channelLowerCase} margin-l-8 margin-b-12`}
4605
- type={"primary"}
4606
- isAddBtn
4607
- disabled={this.isCreateDisabled()}
4608
- onClick={this.createTemplate}
4609
- >
4610
- { this.props.intl.formatMessage((isTraiDltFeature && channel.toUpperCase() === SMS ) ? messages.uploadTemplate : messages.createNewActionButton) }
4611
- </CapButton>
4729
+ <NoPermissionWrapper allowed={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}>
4730
+ <CapButton
4731
+ className={`create-new-${channelLowerCase} margin-l-8 margin-b-12`}
4732
+ type={"primary"}
4733
+ disabled={this.isCreateDisabled()}
4734
+ onClick={this.createTemplate}
4735
+ >
4736
+ { this.props.intl.formatMessage((isTraiDltFeature && channel.toUpperCase() === SMS ) ? messages.uploadTemplate : messages.createNewActionButton) }
4737
+ </CapButton>
4738
+ </NoPermissionWrapper>
4612
4739
  );
4613
4740
 
4614
4741
  const isWhatsappCountExeeded = templatesCount >= MAX_WHATSAPP_TEMPLATES;
@@ -5065,15 +5192,22 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
5065
5192
  ? this.props.Templates.selectedWhatsappAccount.name
5066
5193
  : null
5067
5194
  }
5068
- // Pass formData for EMAIL and SMS channels for tag extraction
5195
+ // Pass formData for tag extraction (EMAIL/SMS use nested formData; VIBER uses content object)
5069
5196
  formData={
5070
- this.state.testAndPreviewContent &&
5071
- (this.state.channel?.toUpperCase() === EMAIL ||
5072
- this.state.channel?.toUpperCase() === SMS) &&
5073
- typeof this.state.testAndPreviewContent === 'object' &&
5074
- this.state.testAndPreviewContent?.formData
5075
- ? this.state.testAndPreviewContent.formData
5076
- : null
5197
+ (() => {
5198
+ const previewContent = this.state.testAndPreviewContent;
5199
+ const channelUpper = this.state.channel?.toUpperCase();
5200
+ if (!previewContent || typeof previewContent !== 'object') {
5201
+ return null;
5202
+ }
5203
+ if ([EMAIL, SMS].includes(channelUpper)) {
5204
+ return previewContent.formData || null;
5205
+ }
5206
+ if (channelUpper === VIBER) {
5207
+ return previewContent;
5208
+ }
5209
+ return null;
5210
+ })()
5077
5211
  }
5078
5212
  />
5079
5213
  )}
@@ -16,7 +16,7 @@ jest.mock('@capillarytech/cap-ui-library/assets/images/featureUiNotEnabledIllust
16
16
  import React from 'react';
17
17
  import { shallow, configure } from 'enzyme';
18
18
  import Adapter from '@cfaester/enzyme-adapter-react-18';
19
- import ChannelTypeIllustration from '../ChannelTypeIllustration';
19
+ import { ChannelTypeIllustration } from '../ChannelTypeIllustration';
20
20
  import { MOBILE_PUSH, SMS, EMAIL, LINE, VIBER, FACEBOOK, WHATSAPP, RCS, ZALO, INAPP } from '../../CreativesContainer/constants';
21
21
 
22
22
  configure({ adapter: new Adapter() });