Skip to content

Commit

Permalink
refactor: replace Menu in favor of GenericMenu in AdminUsers (Roc…
Browse files Browse the repository at this point in the history
  • Loading branch information
dougfabris authored Dec 27, 2024
1 parent 1184b83 commit e229533
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 254 deletions.
132 changes: 24 additions & 108 deletions apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
import type { IUser } from '@rocket.chat/core-typings';
import { ButtonGroup, Menu, Option } from '@rocket.chat/fuselage';
import { useRoute, usePermission } from '@rocket.chat/ui-contexts';
import { ButtonGroup, IconButton } from '@rocket.chat/fuselage';
import { GenericMenu } from '@rocket.chat/ui-client';
import type { ReactElement } from 'react';
import { useCallback, useMemo } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import type { AdminUsersTab } from './AdminUsersPage';
import { useChangeAdminStatusAction } from './hooks/useChangeAdminStatusAction';
import { useChangeUserStatusAction } from './hooks/useChangeUserStatusAction';
import { useDeleteUserAction } from './hooks/useDeleteUserAction';
import { useResetE2EEKeyAction } from './hooks/useResetE2EEKeyAction';
import { useResetTOTPAction } from './hooks/useResetTOTPAction';
import type { AdminUserInfoActionsProps } from './hooks/useAdminUserInfoActions';
import { useAdminUserInfoActions } from './hooks/useAdminUserInfoActions';
import { UserInfoAction } from '../../../components/UserInfo';
import { useActionSpread } from '../../hooks/useActionSpread';

type AdminUserInfoActionsProps = {
username: IUser['username'];
userId: IUser['_id'];
isFederatedUser: IUser['federated'];
isActive: boolean;
isAdmin: boolean;
tab: AdminUsersTab;
onChange: () => void;
onReload: () => void;
};

// TODO: Replace menu
const AdminUserInfoActions = ({
username,
userId,
Expand All @@ -37,104 +19,38 @@ const AdminUserInfoActions = ({
onReload,
}: AdminUserInfoActionsProps): ReactElement => {
const { t } = useTranslation();
const directRoute = useRoute('direct');
const userRoute = useRoute('admin-users');
const canDirectMessage = usePermission('create-d');
const canEditOtherUserInfo = usePermission('edit-other-user-info');

const changeAdminStatusAction = useChangeAdminStatusAction(userId, isAdmin, onChange);
const changeUserStatusAction = useChangeUserStatusAction(userId, isActive, onChange);
const deleteUserAction = useDeleteUserAction(userId, onChange, onReload);
const resetTOTPAction = useResetTOTPAction(userId);
const resetE2EKeyAction = useResetE2EEKeyAction(userId);

const directMessageClick = useCallback(
() =>
username &&
directRoute.push({
rid: username,
}),
[directRoute, username],
);

const editUserClick = useCallback(
() =>
userRoute.push({
context: 'edit',
id: userId,
}),
[userId, userRoute],
);

const isNotPendingDeactivatedNorFederated = tab !== 'pending' && tab !== 'deactivated' && !isFederatedUser;
const options = useMemo(
() => ({
...(canDirectMessage && {
directMessage: {
icon: 'balloon' as const,
label: t('Direct_Message'),
title: t('Direct_Message'),
action: directMessageClick,
},
}),
...(canEditOtherUserInfo && {
editUser: {
icon: 'edit' as const,
label: t('Edit'),
title: isFederatedUser ? t('Edit_Federated_User_Not_Allowed') : t('Edit'),
action: editUserClick,
disabled: isFederatedUser,
},
}),
...(isNotPendingDeactivatedNorFederated && changeAdminStatusAction && { makeAdmin: changeAdminStatusAction }),
...(isNotPendingDeactivatedNorFederated && resetE2EKeyAction && { resetE2EKey: resetE2EKeyAction }),
...(isNotPendingDeactivatedNorFederated && resetTOTPAction && { resetTOTP: resetTOTPAction }),
...(changeUserStatusAction && !isFederatedUser && { changeActiveStatus: changeUserStatusAction }),
...(deleteUserAction && { delete: deleteUserAction }),
}),
[
canDirectMessage,
canEditOtherUserInfo,
changeAdminStatusAction,
changeUserStatusAction,
deleteUserAction,
directMessageClick,
editUserClick,
isFederatedUser,
isNotPendingDeactivatedNorFederated,
resetE2EKeyAction,
resetTOTPAction,
t,
],
);

const { actions: actionsDefinition, menu: menuOptions } = useActionSpread(options);
const { actions: actionsDefinition, menuActions: menuOptions } = useAdminUserInfoActions({
username,
userId,
isFederatedUser,
isActive,
isAdmin,
tab,
onChange,
onReload,
});

const menu = useMemo(() => {
if (!menuOptions) {
return null;
}

return (
<Menu
mi={4}
placement='bottom-start'
small={false}
secondary
flexShrink={0}
<GenericMenu
key='menu'
renderItem={({ label: { label, icon }, ...props }): ReactElement => (
<Option label={label} title={label} icon={icon} variant={label === 'Delete' ? 'danger' : ''} {...props} />
)}
options={menuOptions}
button={<IconButton icon='kebab' secondary />}
title={t('More')}
sections={menuOptions}
placement='bottom-end'
small={false}
/>
);
}, [menuOptions]);
}, [t, menuOptions]);

// TODO: sanitize Action type to avoid any
const actions = useMemo(() => {
const mapAction = ([key, { label, icon, action, disabled, title }]: any): ReactElement => (
<UserInfoAction key={key} title={title} label={label} onClick={action} disabled={disabled} icon={icon} />
const mapAction = ([key, { content, title, icon, onClick, disabled }]: any): ReactElement => (
<UserInfoAction key={key} title={title} label={content} onClick={onClick} disabled={disabled} icon={icon} />
);
return [...actionsDefinition.map(mapAction), menu].filter(Boolean);
}, [actionsDefinition, menu]);
Expand Down
24 changes: 12 additions & 12 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ it('should not render voip extension column when voice call is disabled', async
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();
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: /Assign_extension/ })).not.toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: /Unassign_extension/ })).not.toBeInTheDocument();
});

it('should not render voip extension column or actions if user doesnt have the required permission', async () => {
Expand All @@ -63,9 +63,9 @@ it('should not render voip extension column or actions if user doesnt have the r
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();
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: /Assign_extension/ })).not.toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: /Unassign_extension/ })).not.toBeInTheDocument();
});

it('should render "Unassign_extension" button when user has a associated extension', async () => {
Expand All @@ -91,9 +91,9 @@ it('should render "Unassign_extension" button when user has a associated extensi
expect(screen.getByText('Voice_call_extension')).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.getByRole('option', { name: /Unassign_extension/ })).toBeInTheDocument();
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: /Assign_extension/ })).not.toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /Unassign_extension/ })).toBeInTheDocument();
});

it('should render "Assign_extension" button when user has no associated extension', async () => {
Expand All @@ -119,7 +119,7 @@ it('should render "Assign_extension" button when user has no associated extensio
expect(screen.getByText('Voice_call_extension')).toBeInTheDocument();

screen.getByRole('button', { name: 'More_actions' }).click();
expect(await screen.findByRole('listbox')).toBeInTheDocument();
expect(screen.getByRole('option', { name: /Assign_extension/ })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: /Unassign_extension/ })).not.toBeInTheDocument();
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: /Assign_extension/ })).toBeInTheDocument();
expect(screen.queryByRole('menuitem', { name: /Unassign_extension/ })).not.toBeInTheDocument();
});
55 changes: 20 additions & 35 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { UserStatus as Status } from '@rocket.chat/core-typings';
import type { IRole, IUser, Serialized } from '@rocket.chat/core-typings';
import { Box, Button, Menu, Option } from '@rocket.chat/fuselage';
import { Box, Button } from '@rocket.chat/fuselage';
import type { DefaultUserInfo } from '@rocket.chat/rest-typings';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import type { ReactElement, MouseEvent, KeyboardEvent } from 'react';
import { GenericMenu } from '@rocket.chat/ui-client';
import type { KeyboardEvent, MouseEvent, ReactElement } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

Expand Down Expand Up @@ -101,38 +102,26 @@ const UsersTableRow = ({
});

const isNotPendingDeactivatedNorFederated = tab !== 'pending' && tab !== 'deactivated' && !isFederatedUser;
const menuOptions = useMemo(
const actions = useMemo(
() => ({
...(voipExtensionAction && {
voipExtensionAction: {
label: { label: voipExtensionAction.label, icon: voipExtensionAction.icon },
action: voipExtensionAction.action,
},
voipExtensionAction,
}),
...(isNotPendingDeactivatedNorFederated &&
changeAdminStatusAction && {
makeAdmin: {
label: { label: changeAdminStatusAction.label, icon: changeAdminStatusAction.icon },
action: changeAdminStatusAction.action,
},
changeAdminStatusAction,
}),
...(isNotPendingDeactivatedNorFederated &&
resetE2EKeyAction && {
resetE2EKey: { label: { label: resetE2EKeyAction.label, icon: resetE2EKeyAction.icon }, action: resetE2EKeyAction.action },
}),
...(isNotPendingDeactivatedNorFederated &&
resetTOTPAction && {
resetTOTP: { label: { label: resetTOTPAction.label, icon: resetTOTPAction.icon }, action: resetTOTPAction.action },
resetE2EKeyAction,
}),
...(isNotPendingDeactivatedNorFederated && resetTOTPAction && { resetTOTPAction }),
...(changeUserStatusAction &&
!isFederatedUser && {
changeActiveStatus: {
label: { label: changeUserStatusAction.label, icon: changeUserStatusAction.icon },
action: changeUserStatusAction.action,
},
changeUserStatusAction,
}),
...(deleteUserAction && {
delete: { label: { label: deleteUserAction.label, icon: deleteUserAction.icon }, action: deleteUserAction.action },
deleteUserAction,
}),
}),
[
Expand All @@ -147,6 +136,14 @@ const UsersTableRow = ({
],
);

const menuOptions = Object.entries(actions).map(([_key, item]) => {
return {
...item,
id: item.content || item.title || '',
content: item.content || item.title,
};
});

const handleResendWelcomeEmail = () => resendWelcomeEmail.mutateAsync({ email: emails?.[0].address });

return (
Expand Down Expand Up @@ -208,25 +205,13 @@ const UsersTableRow = ({
{t('Resend_welcome_email')}
</Button>
) : (
<Button small primary onClick={changeUserStatusAction?.action} disabled={isSeatsCapExceeded}>
<Button small primary onClick={changeUserStatusAction?.onClick} disabled={isSeatsCapExceeded}>
{t('Activate')}
</Button>
)}
</>
)}

<Menu
mi={4}
placement='bottom-start'
flexShrink={0}
key='menu'
aria-label={t('More_actions')}
title={t('More_actions')}
renderItem={({ label: { label, icon }, ...props }): ReactElement => (
<Option label={label} title={label} icon={icon} variant={label === 'Delete' ? 'danger' : ''} {...props} />
)}
options={menuOptions}
/>
<GenericMenu detached title={t('More_actions')} sections={[{ title: '', items: menuOptions }]} placement='bottom-end' />
</Box>
</GenericTableCell>
</GenericTableRow>
Expand Down
Loading

0 comments on commit e229533

Please sign in to comment.