Skip to content

Commit

Permalink
feat: access-marketplace permission (#29203)
Browse files Browse the repository at this point in the history
  • Loading branch information
yash-rajpal authored Jun 2, 2023
1 parent 29aab43 commit 5617702
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 179 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-hounds-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': minor
---

feat: access-marketplace permission
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// 2. admin, moderator, and user roles should not be deleted as they are referenced in the code.
export const permissions = [
{ _id: 'access-permissions', roles: ['admin'] },
{ _id: 'access-marketplace', roles: ['admin', 'user'] },
{ _id: 'access-setting-permissions', roles: ['admin'] },
{ _id: 'add-oauth-service', roles: ['admin'] },
{ _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] },
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,70 +1,15 @@
import { OptionDivider } from '@rocket.chat/fuselage';
import { useAtLeastOnePermission, usePermission } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { Fragment } from 'react';

import type { AccountBoxItem, IAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox';
import { isAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox';
import { useHasLicenseModule } from '../../../ee/client/hooks/useHasLicenseModule';
import AdministrationModelList from './AdministrationModelList';
import AppsModelList from './AppsModelList';
import AuditModelList from './AuditModelList';

type AdministrationListProps = {
accountBoxItems: (IAppAccountBoxItem | AccountBoxItem)[];
onDismiss: () => void;
optionsList: (false | JSX.Element)[];
};

const ADMIN_PERMISSIONS = [
'view-statistics',
'run-import',
'view-user-administration',
'view-room-administration',
'create-invite-links',
'manage-cloud',
'view-logs',
'manage-sounds',
'view-federation-data',
'manage-email-inbox',
'manage-emoji',
'manage-outgoing-integrations',
'manage-own-outgoing-integrations',
'manage-incoming-integrations',
'manage-own-incoming-integrations',
'manage-oauth-apps',
'access-mailer',
'manage-user-status',
'access-permissions',
'access-setting-permissions',
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
'view-engagement-dashboard',
'view-moderation-console',
];

const AdministrationList = ({ accountBoxItems, onDismiss }: AdministrationListProps): ReactElement => {
const hasAuditLicense = useHasLicenseModule('auditing') === true;
const hasAdminPermission = useAtLeastOnePermission(ADMIN_PERMISSIONS);
const hasManageAppsPermission = usePermission('manage-apps');
const hasAuditPermission = usePermission('can-audit') && hasAuditLicense;
const hasAuditLogPermission = usePermission('can-audit-log') && hasAuditLicense;

const appBoxItems = accountBoxItems.filter((item): item is IAppAccountBoxItem => isAppAccountBoxItem(item));
const adminBoxItems = accountBoxItems.filter((item): item is AccountBoxItem => !isAppAccountBoxItem(item));
const showAudit = hasAuditPermission || hasAuditLogPermission;
const showAdmin = hasAdminPermission || !!adminBoxItems.length;
const showWorkspace = hasAdminPermission;

const list = [
showAdmin && <AdministrationModelList showWorkspace={showWorkspace} accountBoxItems={adminBoxItems} onDismiss={onDismiss} />,
<AppsModelList appBoxItems={appBoxItems} onDismiss={onDismiss} appsManagementAllowed={hasManageAppsPermission} />,
showAudit && <AuditModelList showAudit={hasAuditPermission} showAuditLog={hasAuditLogPermission} onDismiss={onDismiss} />,
];

const AdministrationList = ({ optionsList }: AdministrationListProps): ReactElement => {
return (
<>
{list.filter(Boolean).map((item, index) => (
{optionsList.map((item, index) => (
<Fragment key={index}>
{index > 0 && <OptionDivider />}
{item}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('AppsModelList', () => {
it('should render all apps options when a user has manage apps permission', async () => {
const AppsModelList = loadMock();

render(<AppsModelList onDismiss={() => null} appBoxItems={[]} appsManagementAllowed />);
render(<AppsModelList onDismiss={() => null} appBoxItems={[]} appsManagementAllowed showMarketplace />);

expect(screen.getByText('Apps')).to.exist;
expect(screen.getByText('Marketplace')).to.exist;
Expand All @@ -50,14 +50,30 @@ describe('AppsModelList', () => {
},
});

render(<AppsModelList onDismiss={() => null} appBoxItems={[]} appsManagementAllowed={false} />);
render(<AppsModelList onDismiss={() => null} appBoxItems={[]} appsManagementAllowed={false} showMarketplace />);

expect(screen.getByText('Apps')).to.exist;
expect(screen.getByText('Marketplace')).to.exist;
expect(screen.getByText('Installed')).to.exist;
expect(screen.queryByText('Requested')).to.not.exist;
});

it('should not render marketplace and installed options when user does not have access-marketplace permission', async () => {
const AppsModelList = loadMock({
'@rocket.chat/ui-contexts': {
'useAtLeastOnePermission': (): boolean => false,
'@noCallThru': false,
},
});

render(<AppsModelList onDismiss={() => null} appBoxItems={[]} appsManagementAllowed={false} />);

expect(screen.getByText('Apps')).to.exist;
expect(screen.queryByText('Marketplace')).to.not.exist;
expect(screen.queryByText('Installed')).to.not.exist;
expect(screen.queryByText('Requested')).to.not.exist;
});

context('when clicked', () => {
const pushRoute = spy();
const handleDismiss = spy();
Expand All @@ -69,7 +85,7 @@ describe('AppsModelList', () => {
it('should go to marketplace', async () => {
const AppsModelList = loadMock();

render(<AppsModelList onDismiss={handleDismiss} appBoxItems={[]} />, { wrapper: ProvidersMock });
render(<AppsModelList onDismiss={handleDismiss} appBoxItems={[]} showMarketplace />, { wrapper: ProvidersMock });

const button = screen.getByText('Marketplace');
userEvent.click(button);
Expand All @@ -80,7 +96,7 @@ describe('AppsModelList', () => {
it('should go to installed', async () => {
const AppsModelList = loadMock();

render(<AppsModelList onDismiss={handleDismiss} appBoxItems={[]} />, { wrapper: ProvidersMock });
render(<AppsModelList onDismiss={handleDismiss} appBoxItems={[]} showMarketplace />, { wrapper: ProvidersMock });

const button = screen.getByText('Installed');

Expand Down
43 changes: 24 additions & 19 deletions apps/meteor/client/components/AdministrationList/AppsModelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ type AppsModelListProps = {
appBoxItems: IAppAccountBoxItem[];
appsManagementAllowed?: boolean;
onDismiss: () => void;
showMarketplace?: boolean;
};

const AppsModelList = ({ appBoxItems, appsManagementAllowed, onDismiss }: AppsModelListProps): ReactElement => {
const AppsModelList = ({ appBoxItems, appsManagementAllowed, showMarketplace, onDismiss }: AppsModelListProps): ReactElement => {
const t = useTranslation();
const marketplaceRoute = useRoute('marketplace');
const page = 'list';
Expand All @@ -26,24 +27,28 @@ const AppsModelList = ({ appBoxItems, appsManagementAllowed, onDismiss }: AppsMo
<OptionTitle>{t('Apps')}</OptionTitle>
<ul>
<>
<ListItem
role='listitem'
icon='store'
text={t('Marketplace')}
onClick={() => {
marketplaceRoute.push({ context: 'explore', page });
onDismiss();
}}
/>
<ListItem
role='listitem'
icon='circle-arrow-down'
text={t('Installed')}
onClick={() => {
marketplaceRoute.push({ context: 'installed', page });
onDismiss();
}}
/>
{showMarketplace && (
<>
<ListItem
role='listitem'
icon='store'
text={t('Marketplace')}
onClick={() => {
marketplaceRoute.push({ context: 'explore', page });
onDismiss();
}}
/>
<ListItem
role='listitem'
icon='circle-arrow-down'
text={t('Installed')}
onClick={() => {
marketplaceRoute.push({ context: 'installed', page });
onDismiss();
}}
/>
</>
)}

{appsManagementAllowed && (
<>
Expand Down
72 changes: 70 additions & 2 deletions apps/meteor/client/sidebar/header/actions/Administration.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
import { Sidebar, Dropdown } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { usePermission, useAtLeastOnePermission } from '@rocket.chat/ui-contexts';
import type { HTMLAttributes, VFC } from 'react';
import React, { useRef } from 'react';
import React, { useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';

import { AccountBox } from '../../../../app/ui-utils/client';
import type { IAppAccountBoxItem, AccountBoxItem } from '../../../../app/ui-utils/client/lib/AccountBox';
import { isAppAccountBoxItem } from '../../../../app/ui-utils/client/lib/AccountBox';
import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule';
import AdministrationList from '../../../components/AdministrationList/AdministrationList';
import AdministrationModelList from '../../../components/AdministrationList/AdministrationModelList';
import AppsModelList from '../../../components/AdministrationList/AppsModelList';
import AuditModelList from '../../../components/AdministrationList/AuditModelList';
import { useReactiveValue } from '../../../hooks/useReactiveValue';
import { useDropdownVisibility } from '../hooks/useDropdownVisibility';

const ADMIN_PERMISSIONS = [
'view-statistics',
'run-import',
'view-user-administration',
'view-room-administration',
'create-invite-links',
'manage-cloud',
'view-logs',
'manage-sounds',
'view-federation-data',
'manage-email-inbox',
'manage-emoji',
'manage-outgoing-integrations',
'manage-own-outgoing-integrations',
'manage-incoming-integrations',
'manage-own-incoming-integrations',
'manage-oauth-apps',
'access-mailer',
'manage-user-status',
'access-permissions',
'access-setting-permissions',
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
'view-engagement-dashboard',
'view-moderation-console',
];

const Administration: VFC<Omit<HTMLAttributes<HTMLElement>, 'is'>> = (props) => {
const reference = useRef(null);
const target = useRef(null);
Expand All @@ -18,13 +53,46 @@ const Administration: VFC<Omit<HTMLAttributes<HTMLElement>, 'is'>> = (props) =>
const getAccountBoxItems = useMutableCallback(() => AccountBox.getItems());
const accountBoxItems = useReactiveValue(getAccountBoxItems);

const hasAuditLicense = useHasLicenseModule('auditing') === true;
const hasManageAppsPermission = usePermission('manage-apps');
const hasAccessMarketplacePermission = usePermission('access-marketplace');
const hasAdminPermission = useAtLeastOnePermission(ADMIN_PERMISSIONS);
const hasAuditPermission = usePermission('can-audit') && hasAuditLicense;
const hasAuditLogPermission = usePermission('can-audit-log') && hasAuditLicense;

const appBoxItems = accountBoxItems.filter((item): item is IAppAccountBoxItem => isAppAccountBoxItem(item));
const adminBoxItems = accountBoxItems.filter((item): item is AccountBoxItem => !isAppAccountBoxItem(item));
const showAdmin = hasAdminPermission || !!adminBoxItems.length;
const showAudit = hasAuditPermission || hasAuditLogPermission;
const showWorkspace = hasAdminPermission;
const showApps = hasAccessMarketplacePermission || hasManageAppsPermission || !!appBoxItems.length;

const onDismiss = useCallback((): void => toggle(false), [toggle]);

const optionsList = [
showAdmin && <AdministrationModelList showWorkspace={showWorkspace} accountBoxItems={adminBoxItems} onDismiss={onDismiss} />,
showApps && (
<AppsModelList
appBoxItems={appBoxItems}
onDismiss={onDismiss}
appsManagementAllowed={hasManageAppsPermission}
showMarketplace={hasAccessMarketplacePermission || hasManageAppsPermission}
/>
),
showAudit && <AuditModelList showAudit={hasAuditPermission} showAuditLog={hasAuditLogPermission} onDismiss={onDismiss} />,
].filter(Boolean);

if (!optionsList || optionsList.length === 0) {
return null;
}

return (
<>
<Sidebar.TopBar.Action icon='menu' onClick={(): void => toggle()} {...props} ref={reference} />
{isVisible &&
createPortal(
<Dropdown reference={reference} ref={target}>
<AdministrationList accountBoxItems={accountBoxItems} onDismiss={(): void => toggle(false)} />
<AdministrationList optionsList={optionsList} />
</Dropdown>,
document.body,
)}
Expand Down
Loading

0 comments on commit 5617702

Please sign in to comment.