Skip to content

Commit

Permalink
feat: Implement users page deactivated tab (#30532)
Browse files Browse the repository at this point in the history
* feat: ✨ Filter users list by active users

Implemented the necessary resources to filter the list of users and to return only those who are active, when the active tab is enabled.

* feat: ✨ Implement new roles filter in the admin users page

Implemented a new filter on the users' page that retrieves the roles list and creates a dropdown menu. When any of its options is selected, it will filter the users' list to display only those with the selected roles. Additionally, I made some minor adjustments to the MultiSelectCustom component and introduced some new hooks.

* WIP: Remove filters

* feat: ✨ WIP: Implement new actions menu for users page table

* feat: ✨ Finish users table menu

Completed the implementation of the users table actions menu by preventing the propagation of the click event that triggered the opening of the contextual bar when the menu was clicked. Also, ensured that the contextual bar only opens when the "Enter" or "Space" keys are used for keyboard navigation and enhanced the options menu in the contextual bar as specified in Figma.

* refactor: ♻️ Remove status badge from contextual bar and reorg

Removed the status from the users page contextual bar (the need to remove this may not be concrete, for now it will be only commented until the final decision is made). Also changed the order of the contextual bar info and changed some minor styles to follow figma specs.

* Typecheck

* Re-add icon back to InfoPanelTitle

* Hide registration status and justify actions menu

* feat: ✨ Implement deactivated tab logic and specifications

Implemented the filtering function that will show deactivated users when the deactivated tab has been selected and hid the actions menu options that are unnecessary for deactivated users. Didn't implement the empty state because it has been implemented in the pending tab PR.

* Fix tab types

* Re-add spacing, change components and add save user
  • Loading branch information
rique223 authored Nov 29, 2023
1 parent 156250f commit 29a50b7
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 80 deletions.
2 changes: 1 addition & 1 deletion apps/meteor/client/components/UserCard/UserCardRoles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import UserCardInfo from './UserCardInfo';

const UserCardRoles = ({ children }: { children: ReactNode }): ReactElement => (
<Box m='neg-x2'>
<UserCardInfo flexWrap='wrap' display='flex' flexShrink={0}>
<UserCardInfo mb={8} flexWrap='wrap' display='flex' flexShrink={0}>
{children}
</UserCardInfo>
</Box>
Expand Down
130 changes: 64 additions & 66 deletions apps/meteor/client/components/UserInfo/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { useUserCustomFields } from '../../hooks/useUserCustomFields';
import { useUserDisplayName } from '../../hooks/useUserDisplayName';
import { ContextualbarScrollableContent } from '../Contextualbar';
import InfoPanel from '../InfoPanel';
import InfoPanelAvatar from '../InfoPanel/InfoPanelAvatar';
import InfoPanelField from '../InfoPanel/InfoPanelField';
import InfoPanelLabel from '../InfoPanel/InfoPanelLabel';
import InfoPanelSection from '../InfoPanel/InfoPanelSection';
import InfoPanelText from '../InfoPanel/InfoPanelText';
import InfoPanelTitle from '../InfoPanel/InfoPanelTitle';
import MarkdownText from '../MarkdownText';
import UTCClock from '../UTCClock';
import UserCard from '../UserCard';
Expand Down Expand Up @@ -58,7 +64,6 @@ const UserInfo = ({
status,
statusText,
customFields,
canViewAllInfo,
actions,
reason,
...props
Expand All @@ -72,124 +77,117 @@ const UserInfo = ({
<ContextualbarScrollableContent p={24} {...props}>
<InfoPanel>
{username && (
<InfoPanel.Avatar>
<InfoPanelAvatar>
<UserInfoAvatar username={username} etag={avatarETag} />
</InfoPanel.Avatar>
</InfoPanelAvatar>
)}

{actions && <InfoPanel.Section>{actions}</InfoPanel.Section>}
{actions && <InfoPanelSection>{actions}</InfoPanelSection>}

<InfoPanel.Section>
{userDisplayName && <InfoPanel.Title icon={status} title={userDisplayName} />}
<InfoPanelSection>
{userDisplayName && <InfoPanelTitle icon={status} title={userDisplayName} />}
{statusText && (
<InfoPanel.Text>
<InfoPanelText>
<MarkdownText content={statusText} parseEmoji={true} variant='inline' />
</InfoPanel.Text>
</InfoPanelText>
)}
</InfoPanel.Section>
</InfoPanelSection>

<InfoPanel.Section>
<InfoPanelSection>
{reason && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Reason_for_joining')}</InfoPanel.Label>
<InfoPanel.Text>{reason}</InfoPanel.Text>
</InfoPanel.Field>
<InfoPanelField>
<InfoPanelLabel>{t('Reason_for_joining')}</InfoPanelLabel>
<InfoPanelText>{reason}</InfoPanelText>
</InfoPanelField>
)}
{nickname && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Nickname')}</InfoPanel.Label>
<InfoPanel.Text>{nickname}</InfoPanel.Text>
</InfoPanel.Field>
<InfoPanelField>
<InfoPanelLabel>{t('Nickname')}</InfoPanelLabel>
<InfoPanelText>{nickname}</InfoPanelText>
</InfoPanelField>
)}

{roles.length !== 0 && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Roles')}</InfoPanel.Label>
<InfoPanelField>
<InfoPanelLabel>{t('Roles')}</InfoPanelLabel>
<UserCard.Roles>{roles}</UserCard.Roles>
</InfoPanel.Field>
</InfoPanelField>
)}

{username && username !== name && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Username')}</InfoPanel.Label>
<InfoPanel.Text data-qa='UserInfoUserName'>{username}</InfoPanel.Text>
</InfoPanel.Field>
<InfoPanelField>
<InfoPanelLabel>{t('Username')}</InfoPanelLabel>
<InfoPanelText data-qa='UserInfoUserName'>{username}</InfoPanelText>
</InfoPanelField>
)}

{Number.isInteger(utcOffset) && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Local_Time')}</InfoPanel.Label>
<InfoPanel.Text>{utcOffset && <UTCClock utcOffset={utcOffset} />}</InfoPanel.Text>
</InfoPanel.Field>
<InfoPanelField>
<InfoPanelLabel>{t('Local_Time')}</InfoPanelLabel>
<InfoPanelText>{utcOffset && <UTCClock utcOffset={utcOffset} />}</InfoPanelText>
</InfoPanelField>
)}

{bio && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Bio')}</InfoPanel.Label>
<InfoPanel.Text withTruncatedText={false}>
<InfoPanelField>
<InfoPanelLabel>{t('Bio')}</InfoPanelLabel>
<InfoPanelText withTruncatedText={false}>
<MarkdownText variant='inline' content={bio} />
</InfoPanel.Text>
</InfoPanel.Field>
</InfoPanelText>
</InfoPanelField>
)}

{canViewAllInfo && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Last_login')}</InfoPanel.Label>
<InfoPanel.Text>{lastLogin ? timeAgo(lastLogin) : t('Never')}</InfoPanel.Text>
</InfoPanel.Field>
)}

{name && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Full_Name')}</InfoPanel.Label>
<InfoPanel.Text>{name}</InfoPanel.Text>
</InfoPanel.Field>
{Number.isInteger(utcOffset) && (
<InfoPanelField>
<InfoPanelLabel>{t('Last_login')}</InfoPanelLabel>
<InfoPanelText>{lastLogin ? timeAgo(lastLogin) : t('Never')}</InfoPanelText>
</InfoPanelField>
)}

{phone && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Phone')}</InfoPanel.Label>
<InfoPanel.Text display='flex' flexDirection='row' alignItems='center'>
<InfoPanelField>
<InfoPanelLabel>{t('Phone')}</InfoPanelLabel>
<InfoPanelText display='flex' flexDirection='row' alignItems='center'>
<Box is='a' withTruncatedText href={`tel:${phone}`}>
{phone}
</Box>
</InfoPanel.Text>
</InfoPanel.Field>
</InfoPanelText>
</InfoPanelField>
)}

{email && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Email')}</InfoPanel.Label>
<InfoPanel.Text display='flex' flexDirection='row' alignItems='center'>
<InfoPanelField>
<InfoPanelLabel>{t('Email')}</InfoPanelLabel>
<InfoPanelText display='flex' flexDirection='row' alignItems='center'>
<Box is='a' withTruncatedText href={`mailto:${email}`}>
{email}
</Box>
<Margins inline={4}>
<Tag>{verified ? t('Verified') : t('Not_verified')}</Tag>
</Margins>
</InfoPanel.Text>
</InfoPanel.Field>
</InfoPanelText>
</InfoPanelField>
)}

{userCustomFields?.map(
(customField) =>
customField?.value && (
<InfoPanel.Field key={customField.value}>
<InfoPanel.Label>{t(customField.label as TranslationKey)}</InfoPanel.Label>
<InfoPanel.Text>
<InfoPanelField key={customField.value}>
<InfoPanelLabel>{t(customField.label as TranslationKey)}</InfoPanelLabel>
<InfoPanelText>
<MarkdownText content={customField.value} variant='inline' />
</InfoPanel.Text>
</InfoPanel.Field>
</InfoPanelText>
</InfoPanelField>
),
)}

{createdAt && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Created_at')}</InfoPanel.Label>
<InfoPanel.Text>{timeAgo(createdAt)}</InfoPanel.Text>
</InfoPanel.Field>
<InfoPanelField>
<InfoPanelLabel>{t('Created_at')}</InfoPanelLabel>
<InfoPanelText>{timeAgo(createdAt)}</InfoPanelText>
</InfoPanelField>
)}
</InfoPanel.Section>
</InfoPanelSection>
</InfoPanel>
</ContextualbarScrollableContent>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/views/admin/users/AdminUserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ const AdminUserForm = ({ userData, onReload, setCreatedUsersCount, context, refe
</ContextualbarScrollableContent>
<ContextualbarFooter>
<Button primary disabled={!isDirty} onClick={handleSubmit(handleSaveUser)} w='100%'>
{t('Add_user')}
{isNewUserPage ? t('Add_user') : t('Save_user')}
</Button>
</ContextualbarFooter>
</>
Expand Down
17 changes: 10 additions & 7 deletions apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type AdminUserInfoActionsProps = {
isAdmin: boolean;
onChange: () => void;
onReload: () => void;
tab: 'all' | 'invited' | 'active' | 'deactivated' | 'pending';
};

const AdminUserInfoActions = ({
Expand All @@ -30,6 +31,7 @@ const AdminUserInfoActions = ({
isAdmin,
onChange,
onReload,
tab,
}: AdminUserInfoActionsProps): ReactElement => {
const t = useTranslation();
const directRoute = useRoute('direct');
Expand Down Expand Up @@ -80,24 +82,25 @@ const AdminUserInfoActions = ({
disabled: isFederatedUser,
},
}),
...(changeAdminStatusAction && !isFederatedUser && { makeAdmin: changeAdminStatusAction }),
...(resetE2EKeyAction && !isFederatedUser && { resetE2EKey: resetE2EKeyAction }),
...(resetTOTPAction && !isFederatedUser && { resetTOTP: resetTOTPAction }),
...(changeAdminStatusAction && !isFederatedUser && tab !== 'deactivated' && { makeAdmin: changeAdminStatusAction }),
...(resetE2EKeyAction && !isFederatedUser && tab !== 'deactivated' && { resetE2EKey: resetE2EKeyAction }),
...(resetTOTPAction && !isFederatedUser && tab !== 'deactivated' && { resetTOTP: resetTOTPAction }),
...(changeUserStatusAction && !isFederatedUser && { changeActiveStatus: changeUserStatusAction }),
...(deleteUserAction && { delete: deleteUserAction }),
}),
[
t,
canDirectMessage,
t,
directMessageClick,
canEditOtherUserInfo,
isFederatedUser,
editUserClick,
changeAdminStatusAction,
changeUserStatusAction,
deleteUserAction,
tab,
resetE2EKeyAction,
resetTOTPAction,
isFederatedUser,
changeUserStatusAction,
deleteUserAction,
],
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import AdminUserInfoActions from './AdminUserInfoActions';
type AdminUserInfoWithDataProps = {
uid: IUser['_id'];
onReload: () => void;
tab: 'all' | 'invited' | 'active' | 'deactivated' | 'pending';
};

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 @@ -124,6 +125,7 @@ const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): R
isFederatedUser={isUserFederated(data?.user as unknown as IUser)}
onChange={onChange}
onReload={onReload}
tab={tab}
/>
}
/>
Expand Down
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 @@ -106,7 +106,7 @@ const UsersPage = (): 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} context={context} />}
{context === 'new' && <AdminUserForm onReload={handleReload} setCreatedUsersCount={setCreatedUsersCount} context={context} />}
{context === 'created' && id && <AdminUserCreated uid={id} createdUsersCount={createdUsersCount} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const UsersTableRow = ({ user, onClick, mediaQuery, refetchUsers, onReload, tab

const menuOptions = {
...(tab !== 'pending' &&
tab !== 'deactivated' &&
changeAdminStatusAction &&
!isFederatedUser && {
makeAdmin: {
Expand All @@ -63,11 +64,13 @@ const UsersTableRow = ({ user, onClick, mediaQuery, refetchUsers, onReload, tab
},
}),
...(tab !== 'pending' &&
tab !== 'deactivated' &&
resetE2EKeyAction &&
!isFederatedUser && {
resetE2EKey: { label: { label: resetE2EKeyAction.label, icon: resetE2EKeyAction.icon }, action: resetE2EKeyAction.action },
}),
...(tab !== 'pending' &&
tab !== 'deactivated' &&
resetTOTPAction &&
!isFederatedUser && {
resetTOTP: { label: { label: resetTOTPAction.label, icon: resetTOTPAction.icon }, action: resetTOTPAction.action },
Expand All @@ -91,7 +94,7 @@ const UsersTableRow = ({ user, onClick, mediaQuery, refetchUsers, onReload, tab
dispatchToastMessage({ type: 'success', message: t('Welcome_email_resent') });
};

const checkPendingButton = (): ReactElement => {
const renderPendingButton = (): ReactElement => {
if (active) {
return (
<Button small secondary mie={8} onClick={handleResendWelcomeEmail}>
Expand Down Expand Up @@ -166,7 +169,7 @@ const UsersTableRow = ({ user, onClick, mediaQuery, refetchUsers, onReload, tab
e.stopPropagation();
}}
>
{tab === 'pending' && checkPendingButton()}
{tab === 'pending' && renderPendingButton()}

<Menu
mi={4}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { IUser } from '@rocket.chat/core-typings';

export const useFilterActiveOrDeactivatedUsers = (
users: Pick<IUser, '_id' | 'username' | 'name' | 'status' | 'roles' | 'emails' | 'active' | 'avatarETag'>[] | undefined,
tab: string,
) => {
if (!users || (tab !== 'active' && tab !== 'deactivated')) return [];

return tab === 'active' ? users.filter((currentUser) => currentUser.active) : users.filter((currentUser) => !currentUser.active);
};
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -4564,6 +4564,7 @@
"Save_Mobile_Bandwidth": "Save Mobile Bandwidth",
"Save_to_enable_this_action": "Save to enable this action",
"Save_To_Webdav": "Save to WebDAV",
"Save_user": "Save user",
"Save_your_encryption_password": "Save your encryption password",
"save-all-canned-responses": "Save All Canned Responses",
"save-all-canned-responses_description": "Permission to save all canned responses",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export type OptionProp = TitleOptionProp | CheckboxOptionProp;
@param selectedOptionsTitle dropdown text after clicking one or more options. For example: 'Rooms (3)'
* @param selectedOptions array with clicked options. This is used in the useFilteredTypeRooms hook, to filter the Rooms' table, for example. This array joins all of the individual clicked options from all available MultiSelectCustom components in the page. It helps to create a union filter for all the selections.
* @param setSelectedOptions part of an useState hook to set the previous selectedOptions
* @param customSetSelected part of an useState hook to set the individual selected checkboxes from this instance.
* @param searchBarText optional text prop that creates a search bar inside the dropdown, when added.
* @returns a React Component that should be used with a custom hook for filters, such as useFilteredTypeRooms.tsx.
* Check out the following files, for examples:
Expand Down

0 comments on commit 29a50b7

Please sign in to comment.