Skip to content

Commit

Permalink
feat: New users page deactivated tab and active tab ui (#32032)
Browse files Browse the repository at this point in the history
Co-authored-by: Tasso Evangelista <[email protected]>
  • Loading branch information
rique223 and tassoevan authored Jul 30, 2024
1 parent d15d315 commit ccf6ff4
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 81 deletions.
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. Also added the UI code for the Active tab;
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';

0 comments on commit ccf6ff4

Please sign in to comment.