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: User Panel > Tab PENDING #30296

Closed
wants to merge 29 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
72bacc5
feat: :sparkles: Filter users list by active users
rique223 Aug 31, 2023
444c930
feat: :sparkles: Implement new roles filter in the admin users page
rique223 Sep 5, 2023
a98e659
create filter for pending users (work in progress)
felipe-rod123 Sep 6, 2023
d70d1b8
create new pendingAction collumn
felipe-rod123 Sep 11, 2023
089c669
add pending actions count
felipe-rod123 Sep 11, 2023
a1d01d5
WIP: Remove filters
rique223 Sep 12, 2023
91046ad
Merge branch 'feat/user-panel' into feat/active-tab
rique223 Sep 18, 2023
d3f01f7
feat: :sparkles: WIP: Implement new actions menu for users page table
rique223 Sep 21, 2023
c89a193
Merge branch 'feat/active-tab' into WM-164-tab-pending
felipe-rod123 Sep 22, 2023
3682720
fix i18n
felipe-rod123 Sep 22, 2023
26940ba
feat: :sparkles: Finish users table menu
rique223 Sep 25, 2023
58dd010
Merge branch 'feat/active-tab' into WM-164-tab-pending
felipe-rod123 Sep 26, 2023
444ab6f
refactor: :recycle: Remove status badge from contextual bar and reorg
rique223 Sep 26, 2023
6e2f2ad
Typecheck
rique223 Sep 26, 2023
55810be
Re-add icon back to InfoPanelTitle
rique223 Sep 26, 2023
b91dce2
create menu component
felipe-rod123 Sep 27, 2023
c368d06
Merge branch 'feat/user-panel' into feat/active-tab
rique223 Sep 27, 2023
f49a37d
fix spacing
felipe-rod123 Sep 27, 2023
270a38e
fix table rows
felipe-rod123 Sep 27, 2023
96d0c47
fix button
felipe-rod123 Sep 27, 2023
d841d0e
fix review
felipe-rod123 Sep 28, 2023
1b938f8
Merge branch 'feat/active-tab' into WM-164-tab-pending
rique223 Sep 29, 2023
32a4e91
fix: :bug: Implement Activate button and small fixes
rique223 Sep 29, 2023
9d12a02
fix pending actions counter (work in progress)
felipe-rod123 Oct 3, 2023
68d8b7d
fix filters (work in progress)
felipe-rod123 Oct 3, 2023
9f77840
Merge branch 'feat/user-panel' into WM-164-tab-pending
felipe-rod123 Oct 10, 2023
8cf407a
fix lint
felipe-rod123 Oct 10, 2023
cd7e702
fix lint
felipe-rod123 Oct 10, 2023
bc437dd
fix
felipe-rod123 Oct 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
25 changes: 17 additions & 8 deletions apps/meteor/client/components/UserInfo/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
email,
verified,
createdAt,
status,
// status,
statusText,
customFields,
canViewAllInfo,
Expand All @@ -78,7 +78,7 @@
{actions && <InfoPanel.Section>{actions}</InfoPanel.Section>}

<InfoPanel.Section>
{userDisplayName && <InfoPanel.Title icon={status} title={userDisplayName} />}
{userDisplayName && <InfoPanel.Title title={userDisplayName} />}
{statusText && (
<InfoPanel.Text>
<MarkdownText content={statusText} parseEmoji={true} variant='inline' />
Expand All @@ -94,17 +94,26 @@
</InfoPanel.Field>
)}

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

{roles.length !== 0 && (

Check failure on line 104 in apps/meteor/client/components/UserInfo/UserInfo.tsx

View workflow job for this annotation

GitHub Actions / 🔎 Code Check / Code Lint

Replace `·········` with `↹↹↹↹↹`
<InfoPanel.Field>
<InfoPanel.Label>{t('Roles')}</InfoPanel.Label>
<UserCard.Roles>{roles}</UserCard.Roles>
</InfoPanel.Field>
)}

{username && username !== name && (
{bio && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Username')}</InfoPanel.Label>
<InfoPanel.Text data-qa='UserInfoUserName'>{username}</InfoPanel.Text>
<InfoPanel.Label>{t('Bio')}</InfoPanel.Label>
<InfoPanel.Text withTruncatedText={false}>
<MarkdownText variant='inline' content={bio} />
</InfoPanel.Text>
</InfoPanel.Field>
)}

Expand All @@ -131,12 +140,12 @@
</InfoPanel.Field>
)}

{name && (
{/* {name && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Full_Name')}</InfoPanel.Label>
<InfoPanel.Text>{name}</InfoPanel.Text>
</InfoPanel.Field>
)}
)} */}

{phone && (
<InfoPanel.Field>
Expand Down
19 changes: 13 additions & 6 deletions apps/meteor/client/views/admin/users/AdminUsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import AdminUserFormWithData from './AdminUserFormWithData';
import AdminUserInfoWithData from './AdminUserInfoWithData';
import UsersTable from './UsersTable';

const UsersPage = (): ReactElement => {
const UsersPage = async (): Promise<ReactElement> => {
const t = useTranslation();
const seatsCap = useSeatsCap();
const reload = useRef(() => null);
Expand All @@ -25,7 +25,8 @@ const UsersPage = (): ReactElement => {
const canCreateUser = usePermission('create-user');
const canBulkCreateUser = usePermission('bulk-register-user');

const [tab, setTab] = useState<'all' | 'invited' | 'new' | 'active' | 'deactivated'>('all');
const [tab, setTab] = useState<'all' | 'invited' | 'pending' | 'new' | 'active' | 'deactivated'>('all');
const [pendingActionsCount, setPendingActionsCount] = useState<number>(0);

useEffect(() => {
if (!context || !seatsCap) {
Expand All @@ -51,7 +52,7 @@ const UsersPage = (): ReactElement => {
) : (
<ButtonGroup>
{canCreateUser && (
<Button icon='user-plus' onClick={() => router.navigate('/admin/users/new')}>
<Button icon='user-plus' onClick={() => router.navigate('/admin/users/pending')}>
{t('New')}
</Button>
)}
Expand All @@ -68,6 +69,9 @@ const UsersPage = (): ReactElement => {
<Tabs.Item selected={!tab || tab === 'all'} onClick={() => setTab('all')}>
{t('All')}
</Tabs.Item>
<Tabs.Item selected={tab === 'pending'} onClick={() => setTab('pending')}>
{pendingActionsCount === 0 ? t('Pending') : `${t('Pending')} (${pendingActionsCount})`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pendingActionsCount is defined and used but since the set is never ran the number will never change. Why is it incomplete?

</Tabs.Item>
<Tabs.Item selected={tab === 'invited'} onClick={() => setTab('invited')}>
{t('Invited')}
</Tabs.Item>
Expand All @@ -80,8 +84,11 @@ const UsersPage = (): ReactElement => {
<Tabs.Item selected={tab === 'deactivated'} onClick={() => setTab('deactivated')}>
{t('Deactivated')}
</Tabs.Item>
<Tabs.Item selected={tab === 'invited'} onClick={() => setTab('invited')}>
{t('Invited')}
</Tabs.Item>
</Tabs>
<UsersTable reload={reload} tab={tab} onReload={handleReload} />
<UsersTable reload={reload} tab={tab} onReload={handleReload} setPendingActionsCount={setPendingActionsCount} />
</Page.Content>
</Page>
{context && (
Expand All @@ -90,14 +97,14 @@ const UsersPage = (): ReactElement => {
<ContextualbarTitle>
{context === 'info' && t('User_Info')}
{context === 'edit' && t('Edit_User')}
{context === 'new' && t('Add_User')}
{context === 'pending' && t('Add_User')}
{context === 'invite' && t('Invite_Users')}
</ContextualbarTitle>
<ContextualbarClose onClick={() => router.navigate('/admin/users')} />
</ContextualbarHeader>
{context === 'info' && id && <AdminUserInfoWithData uid={id} onReload={handleReload} />}
{context === 'edit' && id && <AdminUserFormWithData uid={id} onReload={handleReload} />}
{context === 'new' && <AdminUserForm onReload={handleReload} />}
{context === 'pending' && <AdminUserForm onReload={handleReload} />}
{context === 'invite' && <AdminInviteUsers />}
</Contextualbar>
)}
Expand Down
97 changes: 97 additions & 0 deletions apps/meteor/client/views/admin/users/UsersTable/ActionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { IUser } from '@rocket.chat/core-typings';
import { isUserFederated } from '@rocket.chat/core-typings';
import { Menu, Option } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import React, { useMemo } from 'react';

import type { Action } from '../../../hooks/useActionSpread';
import { useChangeAdminStatusAction } from '../hooks/useChangeAdminStatusAction';
import { useDeleteUserAction } from '../hooks/useDeleteUserAction';
import { useResetE2EEKeyAction } from '../hooks/useResetE2EEKeyAction';
import { useResetTOTPAction } from '../hooks/useResetTOTPAction';

type ActionsMenuProps = {
user: Pick<IUser, '_id' | 'username' | 'name' | 'status' | 'emails' | 'active' | 'avatarETag' | 'roles'>;
tab: string;
changeUserStatusAction: Action | undefined;
onChange: () => void;
onReload: () => void;
};

const ActionsMenu = ({ user, tab, changeUserStatusAction, onChange, onReload }: ActionsMenuProps): ReactElement | null => {
const userId = user._id;
const isAdmin = user.roles?.includes('admin');
const isFederatedUser = isUserFederated(user);

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

const activeMenuOptions = useMemo(
() =>
tab === 'active'
? {
...(changeAdminStatusAction &&
!isFederatedUser && {
makeAdmin: {
label: { label: changeAdminStatusAction.label, icon: changeAdminStatusAction.icon },
action: changeAdminStatusAction.action,
},
}),
...(resetE2EKeyAction &&
!isFederatedUser && {
resetE2EKey: { label: { label: resetE2EKeyAction.label, icon: resetE2EKeyAction.icon }, action: resetE2EKeyAction.action },
}),
...(resetTOTPAction &&
!isFederatedUser && {
resetTOTP: { label: { label: resetTOTPAction.label, icon: resetTOTPAction.icon }, action: resetTOTPAction.action },
}),
}
: {},
[changeAdminStatusAction, isFederatedUser, resetE2EKeyAction, resetTOTPAction, tab],
);

const menuOptions = useMemo(
() => ({
...activeMenuOptions,
...(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 },
}),
}),
[activeMenuOptions, changeUserStatusAction, deleteUserAction, isFederatedUser],
);

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

return (
<Menu
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}
aria-keyshortcuts='alt'
tabIndex={-1}
/>
);
}, [menuOptions]);
};

export default ActionsMenu;
81 changes: 51 additions & 30 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage';
import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage';
import { useMediaQuery, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement, MutableRefObject } from 'react';
Expand All @@ -16,17 +16,18 @@ import {
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../../components/GenericTable/hooks/useSort';
import { useFilterActiveUsers } from '../hooks/useFilterActiveUsers';
import { useFilterPendingUsers } from '../hooks/useFilterPendingUsers';
import { useListUsers } from '../hooks/useListUsers';
import UsersTableRow from './UsersTableRow';

type UsersTableProps = {
reload: MutableRefObject<() => void>;
tab: string;
onReload: () => void;
setPendingActionsCount: React.Dispatch<React.SetStateAction<number>>;
};

// TODO: Missing error state
const UsersTable = ({ reload, tab, onReload }: UsersTableProps): ReactElement | null => {
const UsersTable = ({ reload, tab, onReload, setPendingActionsCount }: UsersTableProps): ReactElement | null => {
const t = useTranslation();
const router = useRouter();
const mediaQuery = useMediaQuery('(min-width: 1024px)');
Expand All @@ -47,11 +48,13 @@ const UsersTable = ({ reload, tab, onReload }: UsersTableProps): ReactElement |
sortDirection,
itemsPerPage,
current,
setPendingActionsCount,
);

const useAllUsers = () => (tab === 'all' && isSuccess ? data?.users : []);

const filteredUsers = [...useAllUsers(), ...useFilterActiveUsers(data?.users, tab)];
// TODO: fix types
const filteredUsers = [...useAllUsers(), ...useFilterActiveUsers(data?.users, tab), ...useFilterPendingUsers(data?.users as any, tab)];

useEffect(() => {
reload.current = refetch;
Expand Down Expand Up @@ -86,33 +89,28 @@ const UsersTable = ({ reload, tab, onReload }: UsersTableProps): ReactElement |

const headers = useMemo(
() => [
<GenericTableHeaderCell w='x200' key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
<GenericTableHeaderCell key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
{t('Name')}
</GenericTableHeaderCell>,
mediaQuery && (
<GenericTableHeaderCell key='username' direction={sortDirection} active={sortBy === 'username'} onClick={setSort} sort='username'>
felipe-rod123 marked this conversation as resolved.
Show resolved Hide resolved
{t('Username')}
</GenericTableHeaderCell>
),
mediaQuery && (
<GenericTableHeaderCell
w='x140'
key='username'
felipe-rod123 marked this conversation as resolved.
Show resolved Hide resolved
key='email'
direction={sortDirection}
active={sortBy === 'username'}
active={sortBy === 'emails.address'}
onClick={setSort}
sort='username'
sort='emails.address'
>
{t('Username')}
{t('Email')}
</GenericTableHeaderCell>
),
<GenericTableHeaderCell
w='x120'
key='email'
felipe-rod123 marked this conversation as resolved.
Show resolved Hide resolved
direction={sortDirection}
active={sortBy === 'emails.address'}
onClick={setSort}
sort='emails.address'
>
{t('Email')}
</GenericTableHeaderCell>,

mediaQuery && (
<GenericTableHeaderCell w='x120' key='roles' onClick={setSort}>
<GenericTableHeaderCell key='roles' onClick={setSort}>
{t('Roles')}
</GenericTableHeaderCell>
),
Expand All @@ -129,6 +127,20 @@ const UsersTable = ({ reload, tab, onReload }: UsersTableProps): ReactElement |
</GenericTableHeaderCell>
),
<GenericTableHeaderCell key='actions' w='x44' />,

tab === 'all' && (
<GenericTableHeaderCell key='action' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
{t('Token')}
</GenericTableHeaderCell>
),

tab === 'pending' && (
<GenericTableHeaderCell key='action' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
{t('Pending_action')}
</GenericTableHeaderCell>
),

<GenericTableHeaderCell key='menu' />,
],
[mediaQuery, setSort, sortBy, sortDirection, t, tab],
);
Expand All @@ -142,11 +154,19 @@ const UsersTable = ({ reload, tab, onReload }: UsersTableProps): ReactElement |
<GenericTableBody>{isLoading && <GenericTableLoadingTable headerCells={5} />}</GenericTableBody>
</GenericTable>
)}

{isSuccess && !!data && !!filteredUsers && data.count > 0 && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{!isLoading && !isSuccess && tab !== 'all' && tab !== 'active' && (
<States>
<StatesIcon name='user' />
<StatesTitle>{tab === 'pending' ? t('No_pending_users') : t('No_deactivated_users')}</StatesTitle>
<StatesSubtitle>{tab === 'pending' ? t('Users_who_are_pending') : t('Deactivated_users_appear_here')}</StatesSubtitle>
</States>
)}
{filteredUsers.map((user) => (
<UsersTableRow
key={user._id}
Expand All @@ -160,17 +180,18 @@ const UsersTable = ({ reload, tab, onReload }: UsersTableProps): ReactElement |
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
{...paginationProps}
/>
</>
)}
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
{...paginationProps}
/>

{isSuccess && data?.count === 0 && <GenericNoResults />}
{isError && (
<States>
Expand Down
Loading
Loading