Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New users page deactivated tab and active tab ui #32032

Merged
merged 11 commits into from
Jul 30, 2024
5 changes: 5 additions & 0 deletions .changeset/gentle-news-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": minor
---

Added a new 'Deactivated' tab to the users page, this tab lists users who have logged in for the first time but have been deactivated for any reason;
25 changes: 16 additions & 9 deletions apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, { useCallback, useMemo } from 'react';

import { UserInfoAction } from '../../../components/UserInfo';
import { useActionSpread } from '../../hooks/useActionSpread';
import type { AdminUserTab } from './AdminUsersPage';
import { useChangeAdminStatusAction } from './hooks/useChangeAdminStatusAction';
import { useChangeUserStatusAction } from './hooks/useChangeUserStatusAction';
import { useDeleteUserAction } from './hooks/useDeleteUserAction';
Expand All @@ -18,6 +19,7 @@ type AdminUserInfoActionsProps = {
isFederatedUser: IUser['federated'];
isActive: boolean;
isAdmin: boolean;
tab: AdminUserTab;
onChange: () => void;
onReload: () => void;
};
Expand All @@ -29,6 +31,7 @@ const AdminUserInfoActions = ({
isFederatedUser,
isActive,
isAdmin,
tab,
onChange,
onReload,
}: AdminUserInfoActionsProps): ReactElement => {
Expand Down Expand Up @@ -62,6 +65,7 @@ const AdminUserInfoActions = ({
[userId, userRoute],
);

const isNotPendingDeactivatedNorFederated = tab !== 'pending' && tab !== 'deactivated' && !isFederatedUser;
const options = useMemo(
() => ({
...(canDirectMessage && {
Expand All @@ -81,24 +85,25 @@ const AdminUserInfoActions = ({
disabled: isFederatedUser,
},
}),
...(changeAdminStatusAction && !isFederatedUser && { makeAdmin: changeAdminStatusAction }),
...(resetE2EKeyAction && !isFederatedUser && { resetE2EKey: resetE2EKeyAction }),
...(resetTOTPAction && !isFederatedUser && { resetTOTP: resetTOTPAction }),
...(deleteUserAction && { delete: deleteUserAction }),
...(isNotPendingDeactivatedNorFederated && changeAdminStatusAction && { makeAdmin: changeAdminStatusAction }),
...(isNotPendingDeactivatedNorFederated && resetE2EKeyAction && { resetE2EKey: resetE2EKeyAction }),
...(isNotPendingDeactivatedNorFederated && resetTOTPAction && { resetTOTP: resetTOTPAction }),
...(changeUserStatusAction && !isFederatedUser && { changeActiveStatus: changeUserStatusAction }),
...(deleteUserAction && { delete: deleteUserAction }),
}),
[
t,
canDirectMessage,
directMessageClick,
canEditOtherUserInfo,
editUserClick,
changeAdminStatusAction,
changeUserStatusAction,
deleteUserAction,
directMessageClick,
editUserClick,
isFederatedUser,
isNotPendingDeactivatedNorFederated,
resetE2EKeyAction,
resetTOTPAction,
isFederatedUser,
t,
],
);

Expand All @@ -117,7 +122,9 @@ const AdminUserInfoActions = ({
secondary
flexShrink={0}
key='menu'
renderItem={({ label: { label, icon }, ...props }): ReactElement => <Option label={label} title={label} icon={icon} {...props} />}
renderItem={({ label: { label, icon }, ...props }): ReactElement => (
<Option label={label} title={label} icon={icon} variant={label === 'Delete' ? 'danger' : ''} {...props} />
)}
options={menuOptions}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import { UserInfo } from '../../../components/UserInfo';
import { UserStatus } from '../../../components/UserStatus';
import { getUserEmailVerified } from '../../../lib/utils/getUserEmailVerified';
import AdminUserInfoActions from './AdminUserInfoActions';
import type { AdminUserTab } from './AdminUsersPage';

type AdminUserInfoWithDataProps = {
uid: IUser['_id'];
onReload: () => void;
tab: AdminUserTab;
};

const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): ReactElement => {
const AdminUserInfoWithData = ({ uid, onReload, tab }: AdminUserInfoWithDataProps): ReactElement => {
const t = useTranslation();
const getRoles = useRolesDescription();
const approveManuallyUsers = useSetting('Accounts_ManuallyApproveNewUsers');
Expand Down Expand Up @@ -123,6 +125,7 @@ const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): R
isFederatedUser={!!data.user.federated}
onChange={onChange}
onReload={onReload}
tab={tab}
/>
}
/>
Expand Down
21 changes: 15 additions & 6 deletions apps/meteor/client/views/admin/users/AdminUsersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IAdminUserTabs, LicenseInfo } from '@rocket.chat/core-typings';
import type { LicenseInfo } from '@rocket.chat/core-typings';
import { Button, ButtonGroup, Callout, ContextualbarIcon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import type { OptionProp } from '@rocket.chat/ui-client';
Expand Down Expand Up @@ -38,6 +38,8 @@ export type UsersFilters = {
roles: OptionProp[];
};

export type AdminUserTab = 'all' | 'active' | 'deactivated' | 'pending';

export type UsersTableSortingOptions = 'name' | 'username' | 'emails.address' | 'status' | 'active';

const AdminUsersPage = (): ReactElement => {
Expand All @@ -64,7 +66,7 @@ const AdminUsersPage = (): ReactElement => {
const paginationData = usePagination();
const sortData = useSort<UsersTableSortingOptions>('name');

const [tab, setTab] = useState<IAdminUserTabs>('all');
const [tab, setTab] = useState<AdminUserTab>('all');
const [userFilters, setUserFilters] = useState<UsersFilters>({ text: '', roles: [] });

const searchTerm = useDebouncedValue(userFilters.text, 500);
Expand All @@ -86,9 +88,10 @@ const AdminUsersPage = (): ReactElement => {
filteredUsersQueryResult?.refetch();
};

const handleTabChangeAndSort = (tab: IAdminUserTabs) => {
const handleTabChange = (tab: AdminUserTab) => {
setTab(tab);

paginationData.setCurrent(0);
sortData.setSort(tab === 'pending' ? 'active' : 'name', 'asc');
};

Expand Down Expand Up @@ -142,14 +145,20 @@ const AdminUsersPage = (): ReactElement => {
</Callout>
)}
<Tabs>
<TabsItem selected={!tab || tab === 'all'} onClick={() => handleTabChangeAndSort('all')}>
<TabsItem selected={!tab || tab === 'all'} onClick={() => handleTabChange('all')}>
{t('All')}
</TabsItem>
<TabsItem selected={tab === 'pending'} onClick={() => handleTabChangeAndSort('pending')} display='flex' flexDirection='row'>
<TabsItem selected={tab === 'pending'} onClick={() => handleTabChange('pending')} display='flex' flexDirection='row'>
{`${t('Pending')} `}
{pendingUsersCount.isLoading && <Skeleton variant='circle' height='x16' width='x16' mis={8} />}
{pendingUsersCount.isSuccess && `(${pendingUsersCount.data})`}
</TabsItem>
<TabsItem selected={tab === 'active'} onClick={() => handleTabChange('active')}>
{t('Active')}
</TabsItem>
<TabsItem selected={tab === 'deactivated'} onClick={() => handleTabChange('deactivated')}>
{t('Deactivated')}
</TabsItem>
</Tabs>
<PageContent>
<UsersTable
Expand Down Expand Up @@ -177,7 +186,7 @@ const AdminUsersPage = (): ReactElement => {
</ContextualbarTitle>
<ContextualbarClose onClick={() => router.navigate('/admin/users')} />
</ContextualbarHeader>
{context === 'info' && id && <AdminUserInfoWithData uid={id} onReload={handleReload} />}
{context === 'info' && id && <AdminUserInfoWithData uid={id} onReload={handleReload} tab={tab} />}
{context === 'edit' && id && <AdminUserFormWithData uid={id} onReload={handleReload} />}
{!isRoutePrevented && context === 'new' && <AdminUserForm onReload={handleReload} />}
{!isRoutePrevented && context === 'invite' && <AdminInviteUsers />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IAdminUserTabs, IRole, Serialized } from '@rocket.chat/core-typings';
import type { IRole, Serialized } from '@rocket.chat/core-typings';
import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage';
import { useEffectEvent, useBreakpoints } from '@rocket.chat/fuselage-hooks';
import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings';
Expand All @@ -17,12 +17,12 @@ import {
} from '../../../../components/GenericTable';
import type { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import type { useSort } from '../../../../components/GenericTable/hooks/useSort';
import type { UsersFilters, UsersTableSortingOptions } from '../AdminUsersPage';
import type { AdminUserTab, UsersFilters, UsersTableSortingOptions } from '../AdminUsersPage';
import UsersTableFilters from './UsersTableFilters';
import UsersTableRow from './UsersTableRow';

type UsersTableProps = {
tab: IAdminUserTabs;
tab: AdminUserTab;
roleData: { roles: IRole[] } | undefined;
onReload: () => void;
setUserFilters: Dispatch<SetStateAction<UsersFilters>>;
Expand Down
120 changes: 64 additions & 56 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { UserStatus as Status } from '@rocket.chat/core-typings';
import type { IAdminUserTabs, IRole, IUser, Serialized } 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 type { DefaultUserInfo } from '@rocket.chat/rest-typings';
import { UserAvatar } from '@rocket.chat/ui-avatar';
Expand All @@ -10,6 +10,7 @@ import React, { useMemo } from 'react';
import { Roles } from '../../../../../app/models/client';
import { GenericTableRow, GenericTableCell } from '../../../../components/GenericTable';
import { UserStatus } from '../../../../components/UserStatus';
import type { AdminUserTab } from '../AdminUsersPage';
import { useChangeAdminStatusAction } from '../hooks/useChangeAdminStatusAction';
import { useChangeUserStatusAction } from '../hooks/useChangeUserStatusAction';
import { useDeleteUserAction } from '../hooks/useDeleteUserAction';
Expand All @@ -23,7 +24,7 @@ type UsersTableRowProps = {
isMobile: boolean;
isLaptop: boolean;
onReload: () => void;
tab: IAdminUserTabs;
tab: AdminUserTab;
isSeatsCapExceeded: boolean;
};

Expand Down Expand Up @@ -65,33 +66,44 @@ const UsersTableRow = ({ user, onClick, onReload, isMobile, isLaptop, tab, isSea
const resendWelcomeEmail = useSendWelcomeEmailMutation();

const isNotPendingDeactivatedNorFederated = tab !== 'pending' && tab !== 'deactivated' && !isFederatedUser;
const menuOptions = {
...(isNotPendingDeactivatedNorFederated &&
changeAdminStatusAction && {
makeAdmin: {
label: { label: changeAdminStatusAction.label, icon: changeAdminStatusAction.icon },
action: changeAdminStatusAction.action,
},
const menuOptions = useMemo(
() => ({
...(isNotPendingDeactivatedNorFederated &&
changeAdminStatusAction && {
makeAdmin: {
label: { label: changeAdminStatusAction.label, icon: changeAdminStatusAction.icon },
action: changeAdminStatusAction.action,
},
}),
...(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 },
}),
...(changeUserStatusAction &&
!isFederatedUser && {
changeActiveStatus: {
label: { label: changeUserStatusAction.label, icon: changeUserStatusAction.icon },
action: changeUserStatusAction.action,
},
}),
...(deleteUserAction && {
delete: { label: { label: deleteUserAction.label, icon: deleteUserAction.icon }, action: deleteUserAction.action },
}),
...(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 },
}),
...(changeUserStatusAction &&
!isFederatedUser && {
changeActiveStatus: {
label: { label: changeUserStatusAction.label, icon: changeUserStatusAction.icon },
action: changeUserStatusAction.action,
},
}),
...(deleteUserAction && {
delete: { label: { label: deleteUserAction.label, icon: deleteUserAction.icon }, action: deleteUserAction.action },
}),
};
[
changeAdminStatusAction,
changeUserStatusAction,
deleteUserAction,
isFederatedUser,
isNotPendingDeactivatedNorFederated,
resetE2EKeyAction,
resetTOTPAction,
],
);

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

Expand Down Expand Up @@ -143,40 +155,36 @@ const UsersTableRow = ({ user, onClick, onReload, isMobile, isLaptop, tab, isSea
)}

<GenericTableCell
display='flex'
justifyContent='flex-end'
onClick={(e): void => {
e.stopPropagation();
}}
>
{tab === 'pending' && (
<>
{active ? (
<Button small secondary onClick={handleResendWelcomeEmail}>
{t('Resend_welcome_email')}
</Button>
) : (
<Button small primary onClick={changeUserStatusAction?.action} disabled={isSeatsCapExceeded}>
{t('Activate')}
</Button>
<Box display='flex' justifyContent='flex-end'>
{tab === 'pending' && (
<>
{active ? (
<Button small secondary onClick={handleResendWelcomeEmail}>
{t('Resend_welcome_email')}
</Button>
) : (
<Button small primary onClick={changeUserStatusAction?.action} disabled={isSeatsCapExceeded}>
{t('Activate')}
</Button>
)}
</>
)}

<Menu
mi={4}
placement='bottom-start'
flexShrink={0}
key='menu'
renderItem={({ label: { label, icon }, ...props }): ReactElement => (
<Option label={label} title={label} icon={icon} variant={label === 'Delete' ? 'danger' : ''} {...props} />
)}
</>
)}

<Menu
mi={4}
placement='bottom-start'
flexShrink={0}
key='menu'
renderItem={({ label: { label, icon }, ...props }): ReactElement =>
label === 'Delete' ? (
<Option label={label} title={label} icon={icon} variant='danger' {...props} />
) : (
<Option label={label} title={label} icon={icon} {...props} />
)
}
options={menuOptions}
/>
options={menuOptions}
/>
</Box>
</GenericTableCell>
</GenericTableRow>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { IAdminUserTabs } from '@rocket.chat/core-typings';
import type { UsersListStatusParamsGET } from '@rocket.chat/rest-typings';
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
Expand All @@ -7,12 +6,12 @@ import { useMemo } from 'react';

import type { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import type { useSort } from '../../../../components/GenericTable/hooks/useSort';
import type { UsersTableSortingOptions } from '../AdminUsersPage';
import type { AdminUserTab, UsersTableSortingOptions } from '../AdminUsersPage';

type UseFilteredUsersOptions = {
searchTerm: string;
prevSearchTerm: MutableRefObject<string>;
tab: IAdminUserTabs;
tab: AdminUserTab;
paginationData: ReturnType<typeof usePagination>;
sortData: ReturnType<typeof useSort<UsersTableSortingOptions>>;
selectedRoles: string[];
Expand All @@ -27,7 +26,7 @@ const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData
setCurrent(0);
}

const listUsersPayload: Partial<Record<IAdminUserTabs, UsersListStatusParamsGET>> = {
const listUsersPayload: Partial<Record<AdminUserTab, UsersListStatusParamsGET>> = {
all: {},
pending: {
hasLoggedIn: false,
Expand Down
2 changes: 0 additions & 2 deletions packages/core-typings/src/IUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,5 +231,3 @@ export type AvatarServiceObject = {
};

export type AvatarObject = AvatarReset | AvatarUrlObj | FormData | AvatarServiceObject;

export type IAdminUserTabs = 'all' | 'active' | 'deactivated' | 'pending';
Loading