@capillarytech/creatives-library 9.0.37 → 9.0.39

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 (39) 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/NoPermissionWrapper/_noPermissionWrapper.scss +12 -0
  9. package/v2Components/NoPermissionWrapper/index.js +26 -0
  10. package/v2Containers/App/constants.js +5 -0
  11. package/v2Containers/Assets/Gallery/_gallery.scss +6 -0
  12. package/v2Containers/Assets/Gallery/index.js +34 -10
  13. package/v2Containers/Cap/index.js +5 -2
  14. package/v2Containers/Cap/messages.js +4 -0
  15. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
  16. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
  17. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
  18. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
  19. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
  20. package/v2Containers/Rcs/carouselUtils.js +46 -73
  21. package/v2Containers/Rcs/components/CarouselCard.js +6 -9
  22. package/v2Containers/Rcs/constants.js +2 -7
  23. package/v2Containers/Rcs/index.js +72 -126
  24. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +5 -43
  25. package/v2Containers/Rcs/tests/CarouselCard.test.js +1 -2
  26. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +118 -118
  27. package/v2Containers/Rcs/tests/carouselUtils.test.js +15 -15
  28. package/v2Containers/Rcs/tests/index.test.js +10 -232
  29. package/v2Containers/Rcs/tests/utils.test.js +0 -17
  30. package/v2Containers/Rcs/utils.js +12 -51
  31. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
  32. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -18
  33. package/v2Containers/Templates/ChannelTypeIllustration.js +13 -2
  34. package/v2Containers/Templates/_templates.scss +6 -0
  35. package/v2Containers/Templates/index.js +141 -100
  36. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +1 -1
  37. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +174 -121
  38. package/v2Containers/TemplatesV2/index.js +16 -5
  39. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +268 -268
@@ -2,8 +2,9 @@ import React, { useMemo } from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import zaloillustration from '@capillarytech/cap-ui-library/assets/images/featureUiNotEnabledIllustration.svg';
4
4
  import inAppIllustration from '@capillarytech/cap-ui-library/assets/images/featureUiNotEnabledIllustration.svg';
5
- import { FormattedMessage } from 'react-intl';
5
+ import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
6
6
  import { CapIllustration } from "@capillarytech/cap-ui-library";
7
+ import globalMessages from '../Cap/messages';
7
8
  import emailIllustration from '../Assets/images/emailIllustration.svg';
8
9
  import smsIllustration from '../Assets/images/smsIllustration.svg';
9
10
  import pushIllustration from '../Assets/images/pushIllustration.svg';
@@ -80,11 +81,13 @@ const CHANNEL_CONFIG = {
80
81
 
81
82
  function ChannelTypeIllustration(props) {
82
83
  const {
84
+ intl,
83
85
  isFullMode,
84
86
  createTemplate,
85
87
  currentChannel,
86
88
  hostName,
87
89
  isArchivedMode,
90
+ canCreate = true,
88
91
  } = props;
89
92
 
90
93
  const templateText = useMemo(() => {
@@ -183,6 +186,11 @@ function ChannelTypeIllustration(props) {
183
186
  if (config.hasButton) {
184
187
  result.buttonLabel = <FormattedMessage {...config.buttonMessage} values={{ template: templateText }} />;
185
188
  result.onClick = createTemplate;
189
+ // RBAC: gray out + disable the create button for users without create permission.
190
+ if (!canCreate) {
191
+ result.disabled = true;
192
+ result.buttonTooltip = intl.formatMessage(globalMessages.noPermissionAction);
193
+ }
186
194
  }
187
195
 
188
196
  if (config.descriptionMessage) {
@@ -208,10 +216,13 @@ function ChannelTypeIllustration(props) {
208
216
  );
209
217
  }
210
218
  ChannelTypeIllustration.propTypes = {
219
+ intl: intlShape.isRequired,
211
220
  isFullMode: PropTypes.bool,
212
221
  createTemplate: PropTypes.func.isRequired,
213
222
  currentChannel: PropTypes.string,
214
223
  hostName: PropTypes.string,
215
224
  isArchivedMode: PropTypes.bool,
225
+ canCreate: PropTypes.bool,
216
226
  };
217
- export default ChannelTypeIllustration;
227
+ export { ChannelTypeIllustration };
228
+ export default injectIntl(ChannelTypeIllustration);
@@ -1473,3 +1473,9 @@
1473
1473
  min-width: 0;
1474
1474
  }
1475
1475
  }
1476
+
1477
+ // RBAC: dim + block a control the user lacks permission for (used with CapTooltip).
1478
+ .rbac-disabled {
1479
+ opacity: 0.4;
1480
+ cursor: not-allowed;
1481
+ }
@@ -97,6 +97,9 @@ import WhatsappStatusContainer from '../../v2Components/WhatsappStatusContainer'
97
97
  import WechatRichmediaTemplatePreview from '../../v2Components/TemplatePreview/WechatRichmediaTemplatePreview';
98
98
  import CapCustomSkeleton from '../../v2Components/CapCustomSkeleton';
99
99
  import ChannelTypeIllustration from './ChannelTypeIllustration';
100
+ import NoPermissionWrapper from '../../v2Components/NoPermissionWrapper';
101
+ import { getUserAccess, hasPermission, PERMISSIONS } from '../../utils/permissions';
102
+
100
103
  import TestAndPreviewSlidebox from '../../v2Components/TestAndPreviewSlidebox';
101
104
  import creativesMessages from '../CreativesContainer/messages';
102
105
  import {
@@ -1971,7 +1974,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
1971
1974
  }
1972
1975
  />
1973
1976
  ) : (
1974
- <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}/>
1975
1978
  );
1976
1979
  }
1977
1980
 
@@ -2105,7 +2108,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2105
2108
  );
2106
2109
  })()
2107
2110
  ],
2108
- hoverOption: isArchivedMode ? null : (
2111
+ hoverOption: isArchivedMode || !this.canPerform(PERMISSIONS.CREATIVE_EDIT) ? null : (
2109
2112
  <CapButton
2110
2113
  className={
2111
2114
  this.props.isFullMode
@@ -2169,56 +2172,47 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2169
2172
  {![WECHAT, WHATSAPP, ZALO].includes(
2170
2173
  this.state.channel.toUpperCase()
2171
2174
  ) &&
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
- )}
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
+ })}
2180
2181
  {/* Archive/Unarchive menu item (full mode only, not for Zalo, not for WhatsApp/RCS awaiting/pending) */}
2181
2182
  {commonUtil.hasCreativesArchivalEnabled() && (() => {
2182
2183
  const channelUp = this.state.channel.toUpperCase();
2183
2184
  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
- );
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
+ });
2199
2199
  })()}
2200
2200
  {/* Delete/Unmap menu item */}
2201
2201
  {(!(
2202
2202
  this.state.channel.toUpperCase() === WHATSAPP &&
2203
2203
  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
- )}
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
+ })}
2222
2216
  </CapMenu>
2223
2217
  }
2224
2218
  >
@@ -2747,16 +2741,18 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2747
2741
  <CapLabel type="label2">
2748
2742
  {this.props.intl.formatMessage(messages.templatesSelected, { count: selectedCountLocal })}
2749
2743
  </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>
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>
2760
2756
  <CapButton type="secondary" onClick={() => this.props.actions.clearTemplateSelection()}>
2761
2757
  <FormattedMessage {...messages.archiveConfirmCancel} />
2762
2758
  </CapButton>
@@ -2795,14 +2791,14 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2795
2791
  </div> : (
2796
2792
  showIllustrationForChannel(MOBILE_PUSH_LOWERCASE) &&
2797
2793
  <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}/>
2794
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2799
2795
  </div>
2800
2796
  )
2801
2797
  }
2802
2798
  {(showWhatsappIllustration || showZaloIllustration) && !get(this.props, 'Templates.isArchivedMode', false) && (
2803
2799
  noLoaderAndSearchText &&
2804
2800
  <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}/>}
2801
+ {noWhatsappZaloTemplates && <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>}
2806
2802
  {filteredEmptyAndFullModeAs(true) && !isEmpty(templates) && this.state?.hostName && this.whatsappZaloIllustrationText(noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc)}
2807
2803
  {noApprovedWhatsappZaloTemplates && this.whatsappZaloIllustrationText(noApprovedWhatsappZaloTemplatesTitle, showWhatsappIllustration ? noApprovedWhatsappTemplatesDesc : zaloDescIllustration)}
2808
2804
  </div>
@@ -2810,41 +2806,41 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2810
2806
  }
2811
2807
  {showIllustrationForChannel(RCS_LOWERCASE) &&
2812
2808
  <div style={this.isFullMode() ? { height: "calc(100vh - 325px)", overflow: 'auto' } : {}}>
2813
- <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}/>
2814
2810
  {filteredEmptyAndFullModeAs(false) && this.whatsappZaloIllustrationText(noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc)}
2815
2811
  </div>
2816
2812
  }
2817
2813
  {showIllustrationForChannel(INAPP_LOWERCASE) &&
2818
2814
  <div className={`${this.isFullMode() ? 'inapp-illustration-parent' : ''}`}>
2819
- <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} />
2820
2816
  </div>
2821
2817
  }
2822
2818
 
2823
2819
  {showIllustrationForChannel(VIBER_LOWERCASE) &&
2824
2820
  <div className={`${this.isFullMode() ? 'viber-illustration-parent' : ''}`}>
2825
- <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}/>
2826
2822
  </div>
2827
2823
  }
2828
2824
  {showIllustrationForChannel(FB_LOWERCASE) &&
2829
2825
  <div className={`${this.isFullMode() ? 'fb-illustration-parent' : ''}`}>
2830
- <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}/>
2831
2827
  </div>
2832
2828
  }
2833
2829
  {showIllustrationForChannel(LINE_LOWERCASE) &&
2834
2830
  <div className={`${this.isFullMode() ? 'line-illustration-parent' : ''}`}>
2835
- <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}/>
2836
2832
  </div>
2837
2833
  }
2838
2834
  {showIllustrationForChannel(WEBPUSH_LOWERCASE) &&
2839
2835
  <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}/>
2836
+ <ChannelTypeIllustration isFullMode={this.props.isFullMode} createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)} currentChannel={currentChannel} hostName={this.state?.hostName}/>
2841
2837
  </div>
2842
2838
  }
2843
2839
  {get(this.props, 'Templates.isArchivedMode', false) && isEmpty(templates) && !isLoading && !getAllTemplatesInProgress && isEmpty(this.state.searchText) && (
2844
2840
  <CapRow className={this.isFullMode() ? 'illustration-scroll-wrapper' : ''}>
2845
2841
  <ChannelTypeIllustration
2846
2842
  isFullMode={this.props.isFullMode}
2847
- createTemplate={this.createTemplate}
2843
+ createTemplate={this.createTemplate} canCreate={this.canPerform(PERMISSIONS.CREATIVE_CREATE)}
2848
2844
  currentChannel={currentChannel}
2849
2845
  isArchivedMode
2850
2846
  />
@@ -3153,7 +3149,36 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3153
3149
  }
3154
3150
  };
3155
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
+
3156
3180
  createTemplate(ev) {
3181
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_CREATE)) return;
3157
3182
  const type = this.props.location.query.type;
3158
3183
  const module = this.props.location.query.module ? this.props.location.query.module : 'default';
3159
3184
  const channel = this.state.channel.toLowerCase();
@@ -3397,6 +3422,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3397
3422
  }
3398
3423
 
3399
3424
  duplicateTemplate(template) {
3425
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_CREATE)) return;
3400
3426
  const duplicateObj = cloneDeep(template);
3401
3427
  duplicateObj.name = `${COPY_OF} ${template?.name} ${moment().format('MM-DD-YYYY HH:mm:ss')}`;
3402
3428
  delete duplicateObj._id;
@@ -3503,6 +3529,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3503
3529
  };
3504
3530
 
3505
3531
  handleEditClick(e, template, modeType, path, options) {
3532
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_EDIT)) return;
3506
3533
  if (template && template.isArchived) {
3507
3534
  CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3508
3535
  return;
@@ -3678,6 +3705,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3678
3705
  }
3679
3706
 
3680
3707
  toggleDeleteTemplateModal = (template) => {
3708
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3681
3709
  this.setState({actionTemplate: template}, () => {
3682
3710
  this.setState({showModal: true});
3683
3711
  });
@@ -3711,6 +3739,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3711
3739
  }
3712
3740
 
3713
3741
  handleBulkArchiveAction = ({ ids, count, isUnarchive = false }) => {
3742
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3714
3743
  const { intl, actions } = this.props;
3715
3744
  const { channel } = this.state;
3716
3745
  const title = isUnarchive
@@ -3730,6 +3759,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3730
3759
  };
3731
3760
 
3732
3761
  handleTemplateArchiveAction = ({ templateId, templateName, isUnarchive = false }) => {
3762
+ if (!this.guardWrite(PERMISSIONS.CREATIVE_DELETE)) return;
3733
3763
  const { intl, actions } = this.props;
3734
3764
  const { channel } = this.state;
3735
3765
  const title = isUnarchive
@@ -3962,35 +3992,43 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3962
3992
  trigger="click"
3963
3993
  content={
3964
3994
  <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>
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>
3969
4001
  </div>}
3970
4002
  {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>
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>
3979
4013
  </div>}
3980
4014
  {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>
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>
3989
4025
  </div>}
3990
4026
  <div className="popover-action-container">
3991
- <CapButton type="link" onClick={() => this.toggleDeleteTemplateModal(template)} className="popover-action">
3992
- {deleteOption}
3993
- </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>
3994
4032
  </div>
3995
4033
  </div>
3996
4034
  }
@@ -4592,23 +4630,26 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
4592
4630
  && !this.props.isFullMode
4593
4631
  )
4594
4632
  ? (
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
- />
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>
4601
4641
  )
4602
4642
  : 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>
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>
4612
4653
  );
4613
4654
 
4614
4655
  const isWhatsappCountExeeded = templatesCount >= MAX_WHATSAPP_TEMPLATES;
@@ -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() });