Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion apps/meteor/client/views/admin/users/AdminUsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ const AdminUsersPage = (): ReactElement => {
</Tabs>
<PageContent>
<UsersTable
filteredUsersQueryResult={filteredUsersQueryResult}
users={filteredUsersQueryResult.data?.users || []}
isLoading={filteredUsersQueryResult.isLoading}
isError={filteredUsersQueryResult.isError}
isSuccess={filteredUsersQueryResult.isSuccess}
total={filteredUsersQueryResult.data?.total || 0}
setUserFilters={setUserFilters}
paginationData={paginationData}
sortData={sortData}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
import type { IUser, Serialized } from '@rocket.chat/core-typings';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { composeStories } from '@storybook/react';
import { render, screen } from '@testing-library/react';
import { axe } from 'jest-axe';

import UsersTable from './UsersTable';
import * as stories from './UsersTable.stories';
import { createFakeUser } from '../../../../../tests/mocks/data';

const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);

test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />, { wrapper: mockAppRoot().build() });
expect(baseElement).toMatchSnapshot();
});

test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });

// TODO: Needed to skip `button-name` because fuselage‘s `Pagination` buttons are missing names
const results = await axe(container, { rules: { 'button-name': { enabled: false } } });
expect(results).toHaveNoViolations();
});

const createFakeAdminUser = (freeSwitchExtension?: string) =>
createFakeUser({
active: true,
Expand All @@ -17,7 +36,11 @@ it('should not render voip extension column when voice call is disabled', async

render(
<UsersTable
filteredUsersQueryResult={{ isSuccess: true, data: { users: [user], count: 1, offset: 1, total: 1 } } as any}
isSuccess={true}
isLoading={false}
isError={false}
users={[user] as unknown as Serialized<IUser>[]}
total={1}
setUserFilters={() => undefined}
tab='all'
onReload={() => undefined}
Expand All @@ -44,7 +67,11 @@ it('should not render voip extension column or actions if user doesnt have the r

render(
<UsersTable
filteredUsersQueryResult={{ isSuccess: true, data: { users: [user], count: 1, offset: 1, total: 1 } } as any}
isSuccess={true}
isLoading={false}
isError={false}
users={[user] as unknown as Serialized<IUser>[]}
total={1}
setUserFilters={() => undefined}
tab='all'
onReload={() => undefined}
Expand All @@ -71,7 +98,11 @@ it('should render "Unassign_extension" button when user has a associated extensi

render(
<UsersTable
filteredUsersQueryResult={{ isSuccess: true, data: { users: [user], count: 1, offset: 1, total: 1 } } as any}
isSuccess={true}
isLoading={false}
isError={false}
users={[user] as unknown as Serialized<IUser>[]}
total={1}
setUserFilters={() => undefined}
tab='all'
onReload={() => undefined}
Expand All @@ -98,7 +129,11 @@ it('should render "Assign_extension" button when user has no associated extensio

render(
<UsersTable
filteredUsersQueryResult={{ isSuccess: true, data: { users: [user], count: 1, offset: 1, total: 1 } } as any}
isSuccess={true}
isLoading={false}
isError={false}
users={[user] as unknown as Serialized<IUser>[]}
total={1}
setUserFilters={() => undefined}
tab='all'
onReload={() => undefined}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { UserStatus } from '@rocket.chat/core-typings';
import type { Meta, StoryFn } from '@storybook/react';

import UsersTable from './UsersTable';

export default {
title: 'views/admin/UsersTable',
component: UsersTable,
} satisfies Meta<typeof UsersTable>;

const Template: StoryFn<typeof UsersTable> = (args) => <UsersTable {...args} />;

export const Default = Template.bind({});
Default.args = {
users: [
{
_id: '1',
username: 'example.user',
name: 'Example User',
emails: [{ address: 'example@rocket.chat', verified: true }],
status: UserStatus.ONLINE,
roles: ['user'],
active: true,
type: '',
},
{
_id: '2',
username: 'john.doe',
name: 'John Doe',
emails: [{ address: 'john@rocket.chat', verified: true }],
status: UserStatus.OFFLINE,
roles: ['admin', 'user'],
active: true,
type: '',
},
{
_id: '3',
username: 'sarah.smith',
name: 'Sarah Smith',
emails: [{ address: 'sarah@rocket.chat', verified: true }],
status: UserStatus.AWAY,
roles: ['user'],
active: true,
type: '',
},
{
_id: '4',
username: 'mike.wilson',
name: 'Mike Wilson',
emails: [{ address: 'mike@rocket.chat', verified: false }],
status: UserStatus.BUSY,
roles: ['user'],
active: true,
type: '',
},
{
_id: '5',
username: 'emma.davis',
name: 'Emma Davis',
emails: [{ address: 'emma@rocket.chat', verified: true }],
status: UserStatus.ONLINE,
roles: ['moderator', 'user'],
active: true,
type: '',
},
],
total: 5,
isLoading: false,
isSuccess: true,
tab: 'all',
};

export const Loading = Template.bind({});
Loading.args = {
isLoading: true,
};

export const NoResults = Template.bind({});
NoResults.args = {
users: [],
total: 0,
isLoading: false,
isError: false,
isSuccess: true,
};
86 changes: 56 additions & 30 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { IRole, IUser, Serialized } from '@rocket.chat/core-typings';
import { Pagination } from '@rocket.chat/fuselage';
import { useEffectEvent, useBreakpoints } from '@rocket.chat/fuselage-hooks';
import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings';
import type { DefaultUserInfo } from '@rocket.chat/rest-typings';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useRouter } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import type { ReactElement, Dispatch, SetStateAction, MouseEvent, KeyboardEvent } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -27,16 +26,24 @@ import { useVoipExtensionPermission } from '../voip/hooks/useVoipExtensionPermis
type UsersTableProps = {
tab: AdminUsersTab;
roleData: { roles: IRole[] } | undefined;
users: Serialized<DefaultUserInfo>[];
total: number;
isLoading: boolean;
isError: boolean;
isSuccess: boolean;
onReload: () => void;
setUserFilters: Dispatch<SetStateAction<UsersFilters>>;
filteredUsersQueryResult: UseQueryResult<PaginatedResult<{ users: Serialized<DefaultUserInfo>[] }>>;
paginationData: ReturnType<typeof usePagination>;
sortData: ReturnType<typeof useSort<UsersTableSortingOption>>;
isSeatsCapExceeded: boolean;
};

const UsersTable = ({
filteredUsersQueryResult,
users,
total,
isLoading,
isError,
isSuccess,
setUserFilters,
roleData,
tab,
Expand All @@ -52,11 +59,6 @@ const UsersTable = ({
const isMobile = !breakpoints.includes('xl');
const isLaptop = !breakpoints.includes('xxl');

const { data, isLoading, isError, isSuccess } = filteredUsersQueryResult;

const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = paginationData;
const { sortBy, sortDirection, setSort } = sortData;

const canManageVoipExtension = useVoipExtensionPermission();

const isKeyboardEvent = (event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>): event is KeyboardEvent<HTMLElement> => {
Expand All @@ -83,49 +85,75 @@ const UsersTable = ({

const headers = useMemo(
() => [
<GenericTableHeaderCell key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
<GenericTableHeaderCell
key='name'
direction={sortData?.sortDirection}
active={sortData?.sortBy === 'name'}
onClick={sortData?.setSort}
sort='name'
>
{t('Name')}
</GenericTableHeaderCell>,
<GenericTableHeaderCell key='username' direction={sortDirection} active={sortBy === 'username'} onClick={setSort} sort='username'>
<GenericTableHeaderCell
key='username'
direction={sortData?.sortDirection}
active={sortData?.sortBy === 'username'}
onClick={sortData?.setSort}
sort='username'
>
{t('Username')}
</GenericTableHeaderCell>,
!isLaptop && (
<GenericTableHeaderCell
key='email'
direction={sortDirection}
active={sortBy === 'emails.address'}
onClick={setSort}
direction={sortData?.sortDirection}
active={sortData?.sortBy === 'emails.address'}
onClick={sortData?.setSort}
sort='emails.address'
>
{t('Email')}
</GenericTableHeaderCell>
),
!isLaptop && <GenericTableHeaderCell key='roles'>{t('Roles')}</GenericTableHeaderCell>,
tab === 'all' && !isMobile && (
<GenericTableHeaderCell key='status' direction={sortDirection} active={sortBy === 'status'} onClick={setSort} sort='status'>
<GenericTableHeaderCell
key='status'
direction={sortData?.sortDirection}
active={sortData?.sortBy === 'status'}
onClick={sortData?.setSort}
sort='status'
>
{t('Registration_status')}
</GenericTableHeaderCell>
),
tab === 'pending' && !isMobile && (
<GenericTableHeaderCell key='action' direction={sortDirection} active={sortBy === 'active'} onClick={setSort} sort='active'>
<GenericTableHeaderCell
key='action'
direction={sortData?.sortDirection}
active={sortData?.sortBy === 'active'}
onClick={sortData?.setSort}
sort='active'
>
{t('Pending_action')}
</GenericTableHeaderCell>
),
tab === 'all' && canManageVoipExtension && (
<GenericTableHeaderCell
w='x180'
key='freeSwitchExtension'
direction={sortDirection}
active={sortBy === 'freeSwitchExtension'}
onClick={setSort}
direction={sortData?.sortDirection}
active={sortData?.sortBy === 'freeSwitchExtension'}
onClick={sortData?.setSort}
sort='freeSwitchExtension'
>
{t('Voice_call_extension')}
</GenericTableHeaderCell>
),
<GenericTableHeaderCell key='actions' w={tab === 'pending' ? 'x204' : 'x50'} />,
<GenericTableHeaderCell key='actions' w={tab === 'pending' ? 'x204' : 'x50'}>
{t('Actions')}
</GenericTableHeaderCell>,
],
[isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab, canManageVoipExtension],
[sortData, t, isLaptop, tab, isMobile, canManageVoipExtension],
);

return (
Expand All @@ -145,7 +173,7 @@ const UsersTable = ({
<GenericNoResults icon='warning' title={t('Something_went_wrong')} buttonTitle={t('Reload_page')} buttonAction={onReload} />
)}

{isSuccess && data.users.length === 0 && (
{isSuccess && users.length === 0 && (
<GenericNoResults
icon='user'
title={t('Users_Table_Generic_No_users', {
Expand All @@ -156,12 +184,12 @@ const UsersTable = ({
/>
)}

{isSuccess && !!data?.users && (
{isSuccess && !!users && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data.users.map((user) => (
{users.map((user) => (
<UsersTableRow
key={user._id}
tab={tab}
Expand All @@ -178,12 +206,10 @@ const UsersTable = ({
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data.total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
{...paginationProps}
count={total}
onSetItemsPerPage={paginationData?.setItemsPerPage}
onSetCurrent={paginationData?.setCurrent}
{...paginationData}
/>
</>
)}
Expand Down
Loading
Loading