Skip to content

Commit

Permalink
feat: added voip extension permission checks on admin user page
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksandernsilva committed Oct 13, 2024
1 parent fb8876d commit f6cc50e
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 43 deletions.
2 changes: 1 addition & 1 deletion apps/meteor/client/views/admin/users/AdminUsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ const AdminUsersPage = (): ReactElement => {
<UsersTable
filteredUsersQueryResult={filteredUsersQueryResult}
setUserFilters={setUserFilters}
onReload={handleReload}
paginationData={paginationData}
sortData={sortData}
tab={tab}
isSeatsCapExceeded={isSeatsCapExceeded}
roleData={data}
onReload={handleReload}
/>
</PageContent>
</Page>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,31 @@ import '@testing-library/jest-dom';

import UsersPageHeaderContent from './UsersPageHeaderContent';

it('should render "Associate Extension" button when VoIP_TeamCollab_Enabled setting is enabled', async () => {
it('should not show "Assign Extension" button if voip setting is enabled but user dont have required permission', async () => {
render(<UsersPageHeaderContent isSeatsCapExceeded={false} seatsCap={{ activeUsers: 1, maxActiveUsers: 1 }} />, {
legacyRoot: true,
wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(),
});

expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument();
});

it('should not show "Assign Extension" button if user has required permission but voip setting is disabled', async () => {
render(<UsersPageHeaderContent isSeatsCapExceeded={false} seatsCap={{ activeUsers: 1, maxActiveUsers: 1 }} />, {
legacyRoot: true,
wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(),
});

expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument();
});

it('should show "Assign Extension" button if user has required permission and voip setting is enabled', async () => {
render(<UsersPageHeaderContent isSeatsCapExceeded={false} seatsCap={{ activeUsers: 1, maxActiveUsers: 1 }} />, {
legacyRoot: true,
wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).withPermission('manage-voip-extensions').build(),
});

expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeEnabled();
});

Expand Down
14 changes: 5 additions & 9 deletions apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Button, ButtonGroup, Margins } from '@rocket.chat/fuselage';
import { usePermission, useRouter, useSetModal, useSetting } from '@rocket.chat/ui-contexts';
import { usePermission, useRouter } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import { useExternalLink } from '../../../hooks/useExternalLink';
import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl';
import SeatsCapUsage from './SeatsCapUsage';
import type { SeatCapProps } from './useSeatsCap';
import AssignExtensionModal from './voip/AssignExtensionModal';
import AssignExtensionButton from './voip/AssignExtensionButton';
import { useVoipExtensionPermission } from './voip/hooks/useVoipExtensionPermission';

type UsersPageHeaderContentProps = {
isSeatsCapExceeded: boolean;
Expand All @@ -17,10 +18,9 @@ type UsersPageHeaderContentProps = {
const UsersPageHeaderContent = ({ isSeatsCapExceeded, seatsCap }: UsersPageHeaderContentProps) => {
const { t } = useTranslation();
const router = useRouter();
const setModal = useSetModal();
const canCreateUser = usePermission('create-user');
const canBulkCreateUser = usePermission('bulk-register-user');
const canRegisterExtension = useSetting('VoIP_TeamCollab_Enabled');
const canManageVoipExtension = useVoipExtensionPermission();

const manageSubscriptionUrl = useCheckoutUrl()({ target: 'user-page', action: 'buy_more' });
const openExternalLink = useExternalLink();
Expand All @@ -41,11 +41,7 @@ const UsersPageHeaderContent = ({ isSeatsCapExceeded, seatsCap }: UsersPageHeade
</Margins>
)}
<ButtonGroup>
{canRegisterExtension && (
<Button icon='phone' onClick={(): void => setModal(<AssignExtensionModal onClose={(): void => setModal(null)} />)}>
{t('Assign_extension')}
</Button>
)}
{canManageVoipExtension && <AssignExtensionButton />}

{canBulkCreateUser && (
<Button icon='mail' onClick={handleInviteButtonClick} disabled={isSeatsCapExceeded}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const createFakeAdminUser = (freeSwitchExtension?: string) =>
freeSwitchExtension,
});

it('should not render "Voice call extension" column when voice call is disabled', async () => {
it('should not render voip extension column when voice call is disabled', async () => {
const user = createFakeAdminUser('1000');

render(
Expand Down Expand Up @@ -41,7 +41,7 @@ it('should not render "Voice call extension" column when voice call is disabled'
expect(screen.queryByRole('option', { name: /Unassign_extension/ })).not.toBeInTheDocument();
});

it('should render "Unassign_extension" button when user has a associated extension', async () => {
it('should not render voip extension column or actions if user doesnt have the required permission', async () => {
const user = createFakeAdminUser('1000');

render(
Expand All @@ -61,6 +61,34 @@ it('should render "Unassign_extension" button when user has a associated extensi
},
);

expect(screen.queryByText('Voice_call_extension')).not.toBeInTheDocument();

screen.getByRole('button', { name: 'More_actions' }).click();
expect(await screen.findByRole('listbox')).toBeInTheDocument();
expect(screen.queryByRole('option', { name: /Assign_extension/ })).not.toBeInTheDocument();
expect(screen.queryByRole('option', { name: /Unassign_extension/ })).not.toBeInTheDocument();
});

it('should render "Unassign_extension" button when user has a associated extension', async () => {
const user = createFakeAdminUser('1000');

render(
<UsersTable
filteredUsersQueryResult={{ isSuccess: true, data: { users: [user], count: 1, offset: 1, total: 1 } } as any}
setUserFilters={() => undefined}
tab='all'
onReload={() => undefined}
paginationData={{} as any}
sortData={{} as any}
isSeatsCapExceeded={false}
roleData={undefined}
/>,
{
legacyRoot: true,
wrapper: mockAppRoot().withUser(user).withSetting('VoIP_TeamCollab_Enabled', true).withPermission('manage-voip-extensions').build(),
},
);

expect(screen.getByText('Voice_call_extension')).toBeInTheDocument();

screen.getByRole('button', { name: 'More_actions' }).click();
Expand All @@ -85,7 +113,7 @@ it('should render "Assign_extension" button when user has no associated extensio
/>,
{
legacyRoot: true,
wrapper: mockAppRoot().withUser(user).withSetting('VoIP_TeamCollab_Enabled', true).build(),
wrapper: mockAppRoot().withUser(user).withSetting('VoIP_TeamCollab_Enabled', true).withPermission('manage-voip-extensions').build(),
},
);

Expand Down
20 changes: 11 additions & 9 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Pagination } from '@rocket.chat/fuselage';
import { useEffectEvent, useBreakpoints } from '@rocket.chat/fuselage-hooks';
import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useRouter, useTranslation, useSetting } from '@rocket.chat/ui-contexts';
import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import type { ReactElement, Dispatch, SetStateAction } from 'react';
import React, { useMemo } from 'react';
Expand All @@ -19,6 +19,7 @@ import {
import type { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import type { useSort } from '../../../../components/GenericTable/hooks/useSort';
import type { AdminUsersTab, UsersFilters, UsersTableSortingOption } from '../AdminUsersPage';
import { useVoipExtensionPermission } from '../voip/hooks/useVoipExtensionPermission';
import UsersTableFilters from './UsersTableFilters';
import UsersTableRow from './UsersTableRow';

Expand Down Expand Up @@ -49,13 +50,14 @@ const UsersTable = ({

const isMobile = !breakpoints.includes('xl');
const isLaptop = !breakpoints.includes('xxl');
const isVoIPEnabled = useSetting<boolean>('VoIP_TeamCollab_Enabled') || false;

const { data, isLoading, isError, isSuccess } = filteredUsersQueryResult;

const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = paginationData;
const { sortBy, sortDirection, setSort } = sortData;

const canManageVoipExtension = useVoipExtensionPermission();

const isKeyboardEvent = (
event: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>,
): event is React.KeyboardEvent<HTMLElement> => {
Expand Down Expand Up @@ -112,7 +114,7 @@ const UsersTable = ({
{t('Pending_action')}
</GenericTableHeaderCell>
),
tab === 'all' && isVoIPEnabled && (
tab === 'all' && canManageVoipExtension && (
<GenericTableHeaderCell
w='x180'
key='freeSwitchExtension'
Expand All @@ -126,7 +128,7 @@ const UsersTable = ({
),
<GenericTableHeaderCell key='actions' w={tab === 'pending' ? 'x204' : 'x50'} />,
],
[isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab, isVoIPEnabled],
[isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab, canManageVoipExtension],
);

return (
Expand Down Expand Up @@ -162,14 +164,14 @@ const UsersTable = ({
{data.users.map((user) => (
<UsersTableRow
key={user._id}
onClick={handleClickOrKeyDown}
tab={tab}
user={user}
isMobile={isMobile}
isLaptop={isLaptop}
user={user}
onReload={onReload}
tab={tab}
isSeatsCapExceeded={isSeatsCapExceeded}
showVoipExtension={isVoIPEnabled}
showVoipExtension={canManageVoipExtension}
onReload={onReload}
onClick={handleClickOrKeyDown}
/>
))}
</GenericTableBody>
Expand Down
21 changes: 13 additions & 8 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,28 @@ import { useDeleteUserAction } from '../hooks/useDeleteUserAction';
import { useResetE2EEKeyAction } from '../hooks/useResetE2EEKeyAction';
import { useResetTOTPAction } from '../hooks/useResetTOTPAction';
import { useSendWelcomeEmailMutation } from '../hooks/useSendWelcomeEmailMutation';
import { useVoipExtensionAction } from '../hooks/useVoipExtensionAction';
import { useVoipExtensionAction } from '../voip/hooks/useVoipExtensionAction';

type UsersTableRowProps = {
user: Serialized<DefaultUserInfo>;
onClick: (id: IUser['_id'], e: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => void;
tab: AdminUsersTab;
isMobile: boolean;
isLaptop: boolean;
onReload: () => void;
tab: AdminUsersTab;
onClick: (id: IUser['_id'], e: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => void;
isSeatsCapExceeded: boolean;
showVoipExtension: boolean;
};

const UsersTableRow = ({
user,
onClick,
onReload,
tab,
isMobile,
isLaptop,
tab,
isSeatsCapExceeded,
showVoipExtension,
onClick,
onReload,
}: UsersTableRowProps): ReactElement => {
const { t } = useTranslation();

Expand Down Expand Up @@ -75,7 +75,12 @@ const UsersTableRow = ({
const resetTOTPAction = useResetTOTPAction(userId);
const resetE2EKeyAction = useResetE2EEKeyAction(userId);
const resendWelcomeEmail = useSendWelcomeEmailMutation();
const voipExtensionAction = useVoipExtensionAction({ extension: freeSwitchExtension, username, name });
const voipExtensionAction = useVoipExtensionAction({
enabled: showVoipExtension,
extension: freeSwitchExtension,
username,
name,
});

const isNotPendingDeactivatedNorFederated = tab !== 'pending' && tab !== 'deactivated' && !isFederatedUser;
const menuOptions = useMemo(
Expand Down Expand Up @@ -173,7 +178,7 @@ const UsersTableRow = ({
</GenericTableCell>
)}

{tab === 'all' && showVoipExtension && username && (
{tab === 'all' && showVoipExtension && (
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
{freeSwitchExtension || t('Not_assigned')}
</GenericTableCell>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useSetModal, useSetting } from '@rocket.chat/ui-contexts';
import { useSetModal } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import type { Action } from '../../../hooks/useActionSpread';
import AssignExtensionModal from '../voip/AssignExtensionModal';
import RemoveExtensionModal from '../voip/RemoveExtensionModal';
import { useVoipExtensionPermission } from '../voip/hooks/useVoipExtensionPermission';

type VoipExtensionActionParams = {
name: string;
Expand All @@ -14,7 +15,7 @@ type VoipExtensionActionParams = {
};

export const useVoipExtensionAction = ({ name, username, extension }: VoipExtensionActionParams): Action | undefined => {
const isVoipEnabled = useSetting('VoIP_TeamCollab_Enabled');
const canManageVoipExtensions = useVoipExtensionPermission();
const { t } = useTranslation();
const setModal = useSetModal();

Expand All @@ -27,7 +28,7 @@ export const useVoipExtensionAction = ({ name, username, extension }: VoipExtens
setModal(<AssignExtensionModal defaultUsername={username} onClose={(): void => setModal(null)} />);
});

return isVoipEnabled
return canManageVoipExtensions
? {
icon: extension ? 'phone-disabled' : 'phone',
label: extension ? t('Unassign_extension') : t('Assign_extension'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { IconButton } from '@rocket.chat/fuselage';
import { Button } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useSetModal } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import { GenericTableCell } from '../../../../components/GenericTable';
import AssignExtensionModal from './AssignExtensionModal';

const AssignExtensionButton = ({ username }: { username: string }) => {
const AssignExtensionButton = () => {
const { t } = useTranslation();
const setModal = useSetModal();

const handleAssociation = useEffectEvent((e) => {
e.stopPropagation();
setModal(<AssignExtensionModal defaultUsername={username} onClose={(): void => setModal(null)} />);
const handleAssign = useEffectEvent(() => {
setModal(<AssignExtensionModal onClose={(): void => setModal(null)} />);
});

return (
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
<IconButton icon='user-plus' small title={t('Associate_Extension')} onClick={handleAssociation} />
</GenericTableCell>
<Button icon='phone' onClick={handleAssign}>
{t('Assign_extension')}
</Button>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useSetModal } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import type { Action } from '../../../../hooks/useActionSpread';
import AssignExtensionModal from '../AssignExtensionModal';
import RemoveExtensionModal from '../RemoveExtensionModal';

type VoipExtensionActionParams = {
name: string;
username: string;
extension?: string;
enabled: boolean;
};

export const useVoipExtensionAction = ({ name, username, extension, enabled }: VoipExtensionActionParams): Action | undefined => {
const { t } = useTranslation();
const setModal = useSetModal();

const handleExtensionAssignment = useEffectEvent(() => {
if (extension) {
setModal(<RemoveExtensionModal name={name} username={username} extension={extension} onClose={(): void => setModal(null)} />);
return;
}

setModal(<AssignExtensionModal defaultUsername={username} onClose={(): void => setModal(null)} />);
});

return enabled
? {
icon: extension ? 'phone-disabled' : 'phone',
label: extension ? t('Unassign_extension') : t('Assign_extension'),
action: handleExtensionAssignment,
}
: undefined;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useSetting, usePermission } from '@rocket.chat/ui-contexts';

export const useVoipExtensionPermission = () => {
const isVoipSettingEnabled = useSetting<boolean>('VoIP_TeamCollab_Enabled', false);
const canManageVoipExtensions = usePermission('manage-voip-extensions');

return isVoipSettingEnabled && canManageVoipExtensions;
};

0 comments on commit f6cc50e

Please sign in to comment.