Skip to content

Commit 2b9cc5d

Browse files
authored
refactor: Decouple admin users table data from layout (RocketChat#36884)
1 parent 46e37bf commit 2b9cc5d

5 files changed

Lines changed: 1954 additions & 35 deletions

File tree

apps/meteor/client/views/admin/users/AdminUsersPage.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ const AdminUsersPage = (): ReactElement => {
148148
</Tabs>
149149
<PageContent>
150150
<UsersTable
151-
filteredUsersQueryResult={filteredUsersQueryResult}
151+
users={filteredUsersQueryResult.data?.users || []}
152+
isLoading={filteredUsersQueryResult.isLoading}
153+
isError={filteredUsersQueryResult.isError}
154+
isSuccess={filteredUsersQueryResult.isSuccess}
155+
total={filteredUsersQueryResult.data?.total || 0}
152156
setUserFilters={setUserFilters}
153157
paginationData={paginationData}
154158
sortData={sortData}

apps/meteor/client/views/admin/users/UsersTable/UsersTable.spec.tsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
import type { IUser, Serialized } from '@rocket.chat/core-typings';
12
import { mockAppRoot } from '@rocket.chat/mock-providers';
3+
import { composeStories } from '@storybook/react';
24
import { render, screen } from '@testing-library/react';
5+
import { axe } from 'jest-axe';
36

47
import UsersTable from './UsersTable';
8+
import * as stories from './UsersTable.stories';
59
import { createFakeUser } from '../../../../../tests/mocks/data';
610

11+
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
12+
13+
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
14+
const { baseElement } = render(<Story />, { wrapper: mockAppRoot().build() });
15+
expect(baseElement).toMatchSnapshot();
16+
});
17+
18+
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
19+
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
20+
21+
// TODO: Needed to skip `button-name` because fuselage‘s `Pagination` buttons are missing names
22+
const results = await axe(container, { rules: { 'button-name': { enabled: false } } });
23+
expect(results).toHaveNoViolations();
24+
});
25+
726
const createFakeAdminUser = (freeSwitchExtension?: string) =>
827
createFakeUser({
928
active: true,
@@ -17,7 +36,11 @@ it('should not render voip extension column when voice call is disabled', async
1736

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

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

7299
render(
73100
<UsersTable
74-
filteredUsersQueryResult={{ isSuccess: true, data: { users: [user], count: 1, offset: 1, total: 1 } } as any}
101+
isSuccess={true}
102+
isLoading={false}
103+
isError={false}
104+
users={[user] as unknown as Serialized<IUser>[]}
105+
total={1}
75106
setUserFilters={() => undefined}
76107
tab='all'
77108
onReload={() => undefined}
@@ -98,7 +129,11 @@ it('should render "Assign_extension" button when user has no associated extensio
98129

99130
render(
100131
<UsersTable
101-
filteredUsersQueryResult={{ isSuccess: true, data: { users: [user], count: 1, offset: 1, total: 1 } } as any}
132+
isSuccess={true}
133+
isLoading={false}
134+
isError={false}
135+
users={[user] as unknown as Serialized<IUser>[]}
136+
total={1}
102137
setUserFilters={() => undefined}
103138
tab='all'
104139
onReload={() => undefined}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { UserStatus } from '@rocket.chat/core-typings';
2+
import type { Meta, StoryFn } from '@storybook/react';
3+
4+
import UsersTable from './UsersTable';
5+
6+
export default {
7+
title: 'views/admin/UsersTable',
8+
component: UsersTable,
9+
} satisfies Meta<typeof UsersTable>;
10+
11+
const Template: StoryFn<typeof UsersTable> = (args) => <UsersTable {...args} />;
12+
13+
export const Default = Template.bind({});
14+
Default.args = {
15+
users: [
16+
{
17+
_id: '1',
18+
username: 'example.user',
19+
name: 'Example User',
20+
emails: [{ address: 'example@rocket.chat', verified: true }],
21+
status: UserStatus.ONLINE,
22+
roles: ['user'],
23+
active: true,
24+
type: '',
25+
},
26+
{
27+
_id: '2',
28+
username: 'john.doe',
29+
name: 'John Doe',
30+
emails: [{ address: 'john@rocket.chat', verified: true }],
31+
status: UserStatus.OFFLINE,
32+
roles: ['admin', 'user'],
33+
active: true,
34+
type: '',
35+
},
36+
{
37+
_id: '3',
38+
username: 'sarah.smith',
39+
name: 'Sarah Smith',
40+
emails: [{ address: 'sarah@rocket.chat', verified: true }],
41+
status: UserStatus.AWAY,
42+
roles: ['user'],
43+
active: true,
44+
type: '',
45+
},
46+
{
47+
_id: '4',
48+
username: 'mike.wilson',
49+
name: 'Mike Wilson',
50+
emails: [{ address: 'mike@rocket.chat', verified: false }],
51+
status: UserStatus.BUSY,
52+
roles: ['user'],
53+
active: true,
54+
type: '',
55+
},
56+
{
57+
_id: '5',
58+
username: 'emma.davis',
59+
name: 'Emma Davis',
60+
emails: [{ address: 'emma@rocket.chat', verified: true }],
61+
status: UserStatus.ONLINE,
62+
roles: ['moderator', 'user'],
63+
active: true,
64+
type: '',
65+
},
66+
],
67+
total: 5,
68+
isLoading: false,
69+
isSuccess: true,
70+
tab: 'all',
71+
};
72+
73+
export const Loading = Template.bind({});
74+
Loading.args = {
75+
isLoading: true,
76+
};
77+
78+
export const NoResults = Template.bind({});
79+
NoResults.args = {
80+
users: [],
81+
total: 0,
82+
isLoading: false,
83+
isError: false,
84+
isSuccess: true,
85+
};

apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import type { IRole, IUser, Serialized } from '@rocket.chat/core-typings';
22
import { Pagination } from '@rocket.chat/fuselage';
33
import { useEffectEvent, useBreakpoints } from '@rocket.chat/fuselage-hooks';
4-
import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings';
4+
import type { DefaultUserInfo } from '@rocket.chat/rest-typings';
55
import type { TranslationKey } from '@rocket.chat/ui-contexts';
66
import { useRouter } from '@rocket.chat/ui-contexts';
7-
import type { UseQueryResult } from '@tanstack/react-query';
87
import type { ReactElement, Dispatch, SetStateAction, MouseEvent, KeyboardEvent } from 'react';
98
import { useMemo } from 'react';
109
import { useTranslation } from 'react-i18next';
@@ -27,16 +26,24 @@ import { useVoipExtensionPermission } from '../voip/hooks/useVoipExtensionPermis
2726
type UsersTableProps = {
2827
tab: AdminUsersTab;
2928
roleData: { roles: IRole[] } | undefined;
29+
users: Serialized<DefaultUserInfo>[];
30+
total: number;
31+
isLoading: boolean;
32+
isError: boolean;
33+
isSuccess: boolean;
3034
onReload: () => void;
3135
setUserFilters: Dispatch<SetStateAction<UsersFilters>>;
32-
filteredUsersQueryResult: UseQueryResult<PaginatedResult<{ users: Serialized<DefaultUserInfo>[] }>>;
3336
paginationData: ReturnType<typeof usePagination>;
3437
sortData: ReturnType<typeof useSort<UsersTableSortingOption>>;
3538
isSeatsCapExceeded: boolean;
3639
};
3740

3841
const UsersTable = ({
39-
filteredUsersQueryResult,
42+
users,
43+
total,
44+
isLoading,
45+
isError,
46+
isSuccess,
4047
setUserFilters,
4148
roleData,
4249
tab,
@@ -52,11 +59,6 @@ const UsersTable = ({
5259
const isMobile = !breakpoints.includes('xl');
5360
const isLaptop = !breakpoints.includes('xxl');
5461

55-
const { data, isLoading, isError, isSuccess } = filteredUsersQueryResult;
56-
57-
const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = paginationData;
58-
const { sortBy, sortDirection, setSort } = sortData;
59-
6062
const canManageVoipExtension = useVoipExtensionPermission();
6163

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

8486
const headers = useMemo(
8587
() => [
86-
<GenericTableHeaderCell key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
88+
<GenericTableHeaderCell
89+
key='name'
90+
direction={sortData?.sortDirection}
91+
active={sortData?.sortBy === 'name'}
92+
onClick={sortData?.setSort}
93+
sort='name'
94+
>
8795
{t('Name')}
8896
</GenericTableHeaderCell>,
89-
<GenericTableHeaderCell key='username' direction={sortDirection} active={sortBy === 'username'} onClick={setSort} sort='username'>
97+
<GenericTableHeaderCell
98+
key='username'
99+
direction={sortData?.sortDirection}
100+
active={sortData?.sortBy === 'username'}
101+
onClick={sortData?.setSort}
102+
sort='username'
103+
>
90104
{t('Username')}
91105
</GenericTableHeaderCell>,
92106
!isLaptop && (
93107
<GenericTableHeaderCell
94108
key='email'
95-
direction={sortDirection}
96-
active={sortBy === 'emails.address'}
97-
onClick={setSort}
109+
direction={sortData?.sortDirection}
110+
active={sortData?.sortBy === 'emails.address'}
111+
onClick={sortData?.setSort}
98112
sort='emails.address'
99113
>
100114
{t('Email')}
101115
</GenericTableHeaderCell>
102116
),
103117
!isLaptop && <GenericTableHeaderCell key='roles'>{t('Roles')}</GenericTableHeaderCell>,
104118
tab === 'all' && !isMobile && (
105-
<GenericTableHeaderCell key='status' direction={sortDirection} active={sortBy === 'status'} onClick={setSort} sort='status'>
119+
<GenericTableHeaderCell
120+
key='status'
121+
direction={sortData?.sortDirection}
122+
active={sortData?.sortBy === 'status'}
123+
onClick={sortData?.setSort}
124+
sort='status'
125+
>
106126
{t('Registration_status')}
107127
</GenericTableHeaderCell>
108128
),
109129
tab === 'pending' && !isMobile && (
110-
<GenericTableHeaderCell key='action' direction={sortDirection} active={sortBy === 'active'} onClick={setSort} sort='active'>
130+
<GenericTableHeaderCell
131+
key='action'
132+
direction={sortData?.sortDirection}
133+
active={sortData?.sortBy === 'active'}
134+
onClick={sortData?.setSort}
135+
sort='active'
136+
>
111137
{t('Pending_action')}
112138
</GenericTableHeaderCell>
113139
),
114140
tab === 'all' && canManageVoipExtension && (
115141
<GenericTableHeaderCell
116142
w='x180'
117143
key='freeSwitchExtension'
118-
direction={sortDirection}
119-
active={sortBy === 'freeSwitchExtension'}
120-
onClick={setSort}
144+
direction={sortData?.sortDirection}
145+
active={sortData?.sortBy === 'freeSwitchExtension'}
146+
onClick={sortData?.setSort}
121147
sort='freeSwitchExtension'
122148
>
123149
{t('Voice_call_extension')}
124150
</GenericTableHeaderCell>
125151
),
126-
<GenericTableHeaderCell key='actions' w={tab === 'pending' ? 'x204' : 'x50'} />,
152+
<GenericTableHeaderCell key='actions' w={tab === 'pending' ? 'x204' : 'x50'}>
153+
{t('Actions')}
154+
</GenericTableHeaderCell>,
127155
],
128-
[isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab, canManageVoipExtension],
156+
[sortData, t, isLaptop, tab, isMobile, canManageVoipExtension],
129157
);
130158

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

148-
{isSuccess && data.users.length === 0 && (
176+
{isSuccess && users.length === 0 && (
149177
<GenericNoResults
150178
icon='user'
151179
title={t('Users_Table_Generic_No_users', {
@@ -156,12 +184,12 @@ const UsersTable = ({
156184
/>
157185
)}
158186

159-
{isSuccess && !!data?.users && (
187+
{isSuccess && !!users && (
160188
<>
161189
<GenericTable>
162190
<GenericTableHeader>{headers}</GenericTableHeader>
163191
<GenericTableBody>
164-
{data.users.map((user) => (
192+
{users.map((user) => (
165193
<UsersTableRow
166194
key={user._id}
167195
tab={tab}
@@ -178,12 +206,10 @@ const UsersTable = ({
178206
</GenericTable>
179207
<Pagination
180208
divider
181-
current={current}
182-
itemsPerPage={itemsPerPage}
183-
count={data.total || 0}
184-
onSetItemsPerPage={setItemsPerPage}
185-
onSetCurrent={setCurrent}
186-
{...paginationProps}
209+
count={total}
210+
onSetItemsPerPage={paginationData?.setItemsPerPage}
211+
onSetCurrent={paginationData?.setCurrent}
212+
{...paginationData}
187213
/>
188214
</>
189215
)}

0 commit comments

Comments
 (0)