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: Implement users page active tab #30242

Merged
merged 12 commits into from
Oct 4, 2023
Merged
6 changes: 3 additions & 3 deletions apps/meteor/client/components/InfoPanel/InfoPanelTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import React from 'react';

type InfoPanelTitleProps = {
title: string;
icon: ReactNode;
icon?: ReactNode;
};

const isValidIcon = (icon: ReactNode): icon is IconName => typeof icon === 'string';

const InfoPanelTitle: FC<InfoPanelTitleProps> = ({ title, icon }) => (
<Box display='flex' flexShrink={0} alignItems='center' fontScale='h4' color='default' withTruncatedText>
<Box display='flex' flexShrink={0} alignItems='center' fontScale='p1m' color='default' withTruncatedText>
{isValidIcon(icon) ? <Icon name={icon} size='x22' /> : icon}
<Box mis={8} withTruncatedText title={title}>
<Box withTruncatedText title={title} mis={8}>
{title}
</Box>
</Box>
Expand Down
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
40 changes: 20 additions & 20 deletions apps/meteor/client/components/UserInfo/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,30 +81,46 @@ const UserInfo = ({
{userDisplayName && <InfoPanel.Title icon={status} title={userDisplayName} />}
{statusText && (
<InfoPanel.Text>
<MarkdownText content={statusText} parseEmoji={true} />
<MarkdownText content={statusText} parseEmoji={true} variant='inline' />
</InfoPanel.Text>
)}
</InfoPanel.Section>

<InfoPanel.Section>
{nickname && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Nickname')}</InfoPanel.Label>
<InfoPanel.Text>{nickname}</InfoPanel.Text>
</InfoPanel.Field>
)}

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

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

{Number.isInteger(utcOffset) && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Local_Time')}</InfoPanel.Label>
<InfoPanel.Text>{utcOffset && <UTCClock utcOffset={utcOffset} />}</InfoPanel.Text>
</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 @@ -122,22 +138,6 @@ const UserInfo = ({
</InfoPanel.Field>
)}

{nickname && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Nickname')}</InfoPanel.Label>
<InfoPanel.Text>{nickname}</InfoPanel.Text>
</InfoPanel.Field>
)}

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

{phone && (
<InfoPanel.Field>
<InfoPanel.Label>{t('Phone')}</InfoPanel.Label>
Expand Down
10 changes: 8 additions & 2 deletions apps/meteor/client/views/admin/users/AdminUserInfoActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ const AdminUserInfoActions = ({
...(changeAdminStatusAction && !isFederatedUser && { makeAdmin: changeAdminStatusAction }),
...(resetE2EKeyAction && !isFederatedUser && { resetE2EKey: resetE2EKeyAction }),
...(resetTOTPAction && !isFederatedUser && { resetTOTP: resetTOTPAction }),
...(deleteUserAction && { delete: deleteUserAction }),
...(changeUserStatusAction && !isFederatedUser && { changeActiveStatus: changeUserStatusAction }),
...(deleteUserAction && { delete: deleteUserAction }),
}),
[
t,
Expand Down Expand Up @@ -116,7 +116,13 @@ 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 =>
label === 'Delete' ? (
<Option label={label} title={label} icon={icon} variant='danger' {...props} />
) : (
<Option label={label} title={label} icon={icon} {...props} />
)
}
options={menuOptions}
/>
);
Expand Down
12 changes: 6 additions & 6 deletions apps/meteor/client/views/admin/users/AdminUsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const UsersPage = (): ReactElement => {
const canCreateUser = usePermission('create-user');
const canBulkCreateUser = usePermission('bulk-register-user');

const [tab, setTab] = useState<string>('all');
const [tab, setTab] = useState<'all' | 'invited' | 'new' | 'active' | 'deactivated'>('all');

useEffect(() => {
if (!context || !seatsCap) {
Expand Down Expand Up @@ -68,20 +68,20 @@ const UsersPage = (): ReactElement => {
<Tabs.Item selected={!tab || tab === 'all'} onClick={() => setTab('all')}>
{t('All')}
</Tabs.Item>
<Tabs.Item selected={tab === 'tab-invited'} onClick={() => setTab('tab-invited')}>
<Tabs.Item selected={tab === 'invited'} onClick={() => setTab('invited')}>
{t('Invited')}
</Tabs.Item>
<Tabs.Item selected={tab === 'tab-new'} onClick={() => setTab('tab-new')}>
<Tabs.Item selected={tab === 'new'} onClick={() => setTab('new')}>
{t('New_users')}
</Tabs.Item>
<Tabs.Item selected={tab === 'tab-active'} onClick={() => setTab('tab-active')}>
<Tabs.Item selected={tab === 'active'} onClick={() => setTab('active')}>
{t('Active')}
</Tabs.Item>
<Tabs.Item selected={tab === 'tab-deactivated'} onClick={() => setTab('tab-deactivated')}>
<Tabs.Item selected={tab === 'deactivated'} onClick={() => setTab('deactivated')}>
{t('Deactivated')}
</Tabs.Item>
</Tabs>
<UsersTable reload={reload} />
<UsersTable reload={reload} tab={tab} onReload={handleReload} />
</Page.Content>
</Page>
{context && (
Expand Down
152 changes: 80 additions & 72 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Pagination } from '@rocket.chat/fuselage';
import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage';
import { useMediaQuery, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { useEndpoint, useRoute, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement, MutableRefObject } from 'react';
import React, { useRef, useMemo, useState, useEffect } from 'react';

Expand All @@ -17,86 +15,73 @@ import {
} from '../../../../components/GenericTable';
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../../components/GenericTable/hooks/useSort';
import { useFilterActiveUsers } from '../hooks/useFilterActiveUsers';
import { useListUsers } from '../hooks/useListUsers';
import UsersTableRow from './UsersTableRow';

type UsersTableProps = {
reload: MutableRefObject<() => void>;
tab: string;
onReload: () => void;
};

// TODO: Missing error state
const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
const UsersTable = ({ reload, tab, onReload }: UsersTableProps): ReactElement | null => {
const t = useTranslation();
const usersRoute = useRoute('admin-users');
const router = useRouter();
const mediaQuery = useMediaQuery('(min-width: 1024px)');

const [text, setText] = useState('');

const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination();
const { sortBy, sortDirection, setSort } = useSort<'name' | 'username' | 'emails.address' | 'status'>('name');

const searchTerm = useDebouncedValue(text, 500);
const prevSearchTerm = useRef<string>('');

const query = useDebouncedValue(
useMemo(() => {
if (searchTerm !== prevSearchTerm.current) {
setCurrent(0);
}

return {
fields: JSON.stringify({
name: 1,
username: 1,
emails: 1,
roles: 1,
status: 1,
avatarETag: 1,
active: 1,
}),
query: JSON.stringify({
$or: [
{ 'emails.address': { $regex: escapeRegExp(searchTerm), $options: 'i' } },
{ username: { $regex: escapeRegExp(searchTerm), $options: 'i' } },
{ name: { $regex: escapeRegExp(searchTerm), $options: 'i' } },
],
}),
sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`,
count: itemsPerPage,
offset: searchTerm === prevSearchTerm.current ? current : 0,
};
}, [searchTerm, sortBy, sortDirection, itemsPerPage, current, setCurrent]),
500,
const prevSearchTerm = useRef('');

const { data, isLoading, isSuccess, isError, refetch } = useListUsers(
searchTerm,
prevSearchTerm,
setCurrent,
sortBy,
sortDirection,
itemsPerPage,
current,
);

const getUsers = useEndpoint('GET', '/v1/users.list');
const useAllUsers = () => (tab === 'all' && isSuccess ? data?.users : []);

const dispatchToastMessage = useToastMessageDispatch();

const { data, isLoading, error, isSuccess, refetch } = useQuery(
['users', query],
async () => {
const users = await getUsers(query);
return users;
},
{
onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
},
},
);
const filteredUsers = [...useAllUsers(), ...useFilterActiveUsers(data?.users, tab)];

useEffect(() => {
reload.current = refetch;
}, [reload, refetch]);

useEffect(() => {
prevSearchTerm.current = searchTerm;
}, [searchTerm]);
}, [reload, refetch, searchTerm]);

const isKeyboardEvent = (
event: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>,
): event is React.KeyboardEvent<HTMLElement> => {
return (event as React.KeyboardEvent<HTMLElement>).key !== undefined;
};

const handleClickOrKeyDown = useMutableCallback(
(id, e: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>): void => {
e.stopPropagation();

const keyboardSubmitKeys = ['Enter', ' '];

const handleClick = useMutableCallback((id): void =>
usersRoute.push({
context: 'info',
id,
}),
if (isKeyboardEvent(e) && !keyboardSubmitKeys.includes(e.key)) {
return;
}

router.navigate({
name: 'admin-users',
params: {
context: 'info',
id,
},
});
},
);

const headers = useMemo(
Expand Down Expand Up @@ -131,17 +116,23 @@ const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
{t('Roles')}
</GenericTableHeaderCell>
),
<GenericTableHeaderCell w='x100' key='status' direction={sortDirection} active={sortBy === 'status'} onClick={setSort} sort='status'>
{t('Registration_status')}
</GenericTableHeaderCell>,
tab === 'all' && (
<GenericTableHeaderCell
w='x100'
key='status'
direction={sortDirection}
active={sortBy === 'status'}
onClick={setSort}
sort='status'
>
{t('Registration_status')}
</GenericTableHeaderCell>
),
<GenericTableHeaderCell key='actions' w='x44' />,
],
[mediaQuery, setSort, sortBy, sortDirection, t],
[mediaQuery, setSort, sortBy, sortDirection, t, tab],
);

if (error) {
return null;
}

return (
<>
<FilterByText autoFocus placeholder={t('Search_Users')} onChange={({ text }): void => setText(text)} />
Expand All @@ -151,13 +142,21 @@ const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
<GenericTableBody>{isLoading && <GenericTableLoadingTable headerCells={5} />}</GenericTableBody>
</GenericTable>
)}
{data?.users && data.count > 0 && isSuccess && (
{isSuccess && !!data && !!filteredUsers && data.count > 0 && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data?.users.map((user) => (
<UsersTableRow key={user._id} onClick={handleClick} mediaQuery={mediaQuery} user={user} />
{filteredUsers.map((user) => (
<UsersTableRow
key={user._id}
onClick={handleClickOrKeyDown}
mediaQuery={mediaQuery}
user={user}
refetchUsers={refetch}
onReload={onReload}
tab={tab}
/>
))}
</GenericTableBody>
</GenericTable>
Expand All @@ -173,6 +172,15 @@ const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
</>
)}
{isSuccess && data?.count === 0 && <GenericNoResults />}
{isError && (
<States>
<StatesIcon name='warning' variation='danger' />
<StatesTitle>{t('Something_went_wrong')}</StatesTitle>
<StatesActions>
<StatesAction onClick={() => refetch()}>{t('Reload_page')}</StatesAction>
</StatesActions>
</States>
)}
</>
);
};
Expand Down
Loading
Loading