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: New users page all tab #31917

Merged
merged 43 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
35a3b76
refactor: :recycle: Create isSMTPConfigured helper
rique223 Mar 5, 2024
c7ea10e
feat: :sparkles: Create users.listByStatys and users.sendWelcomeEmail…
rique223 Mar 5, 2024
ed5cfec
test: :white_check_mark: Create tests for the users.listByStatus and …
rique223 Mar 5, 2024
4b15497
Typecheck
rique223 Mar 5, 2024
ab3f578
Merge branch 'develop' into feat/new-user-panel-backend
rique223 Mar 6, 2024
cde16a1
feat: :sparkles: Add all tab to users page and implement useFilteredU…
rique223 Mar 6, 2024
4dd02cf
feat: :sparkles: Add Registration Status column to users table
rique223 Mar 6, 2024
f5d3068
Reviews first part
rique223 Mar 8, 2024
f3e1731
Reviews second part
rique223 Mar 8, 2024
1208a88
Reviews third part
rique223 Mar 8, 2024
60876b8
Review
rique223 Mar 14, 2024
3b91f00
Create fifty-cups-sort.md
rique223 Mar 14, 2024
ebc30fe
Create chilly-poems-explode.md
rique223 Mar 14, 2024
fe4b650
Merge branch 'develop' into feat/new-user-panel-backend
rique223 Mar 15, 2024
27ba076
Merge branch 'feat/new-user-panel-backend' into feat/new-user-panel-all
rique223 Mar 15, 2024
8dafa9e
Remove console.log
rique223 Mar 15, 2024
6b2ab3e
Merge branch 'develop' into feat/new-user-panel-backend
rique223 Mar 18, 2024
fe7d42d
Merge branch 'feat/new-user-panel-backend' into feat/new-user-panel-all
rique223 Mar 18, 2024
ae7e828
Merge branch 'develop' into feat/new-user-panel-backend
rique223 Mar 19, 2024
d8696c4
Merge branch 'feat/new-user-panel-backend' into feat/new-user-panel-all
rique223 Mar 19, 2024
a6d49b2
Review
rique223 Mar 19, 2024
eea8f9c
Merge branch 'develop' into feat/new-user-panel-backend
rique223 Mar 20, 2024
de4a14d
Merge branch 'feat/new-user-panel-backend' into feat/new-user-panel-all
rique223 Mar 20, 2024
bf1a5a0
Change PickedUser to DefaultUserInfo
rique223 Mar 20, 2024
1010637
Create pink-parrots-end.md
rique223 Mar 21, 2024
19a12b4
refactor: ♻️ Refactor listUsersByStatus endpoint to be more flexible …
rique223 Mar 22, 2024
e9649e2
Merge branch 'feat/new-user-panel-backend' into feat/new-user-panel-all
rique223 Mar 22, 2024
8923dca
Update listByStatus typing
rique223 Mar 22, 2024
ed29479
Merge branch 'feat/new-user-panel-backend' into feat/new-user-panel-all
rique223 Mar 22, 2024
5b74149
Typecheck
rique223 Mar 22, 2024
e2db32c
Typecheck
rique223 Mar 22, 2024
fbb307b
Update fifty-cups-sort.md
rique223 Apr 1, 2024
afd5108
Update pink-parrots-end.md
rique223 Apr 1, 2024
fe43f5b
Merge branch 'develop' into feat/new-user-panel-backend
rique223 Apr 3, 2024
ccae8e3
Merge branch 'develop' into feat/new-user-panel-backend
rique223 Apr 8, 2024
c19face
Merge branch 'feat/new-user-panel-backend' into feat/new-user-panel-all
rique223 Apr 8, 2024
ac0e48c
Merge
rique223 Apr 23, 2024
eda4df8
Update changeset
rique223 Apr 24, 2024
3ee0dc6
Merge branch 'develop' into feat/new-user-panel-all
kodiakhq[bot] Apr 26, 2024
3b90c6b
Merge branch 'develop' into feat/new-user-panel-all
rique223 Apr 29, 2024
fd1c4cc
Merge branch 'develop' into feat/new-user-panel-all
rique223 Apr 30, 2024
063c633
Merge branch 'develop' into feat/new-user-panel-all
rique223 May 2, 2024
ba02acb
Merge branch 'develop' into feat/new-user-panel-all
kodiakhq[bot] May 6, 2024
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
7 changes: 7 additions & 0 deletions .changeset/chilly-poems-explode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/i18n": patch
---

Introduced a tab layout to the users page and implemented a tab called "All" that lists all users.
62 changes: 54 additions & 8 deletions apps/meteor/client/views/admin/users/AdminUsersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Button, ButtonGroup, ContextualbarIcon } from '@rocket.chat/fuselage';
import type { IAdminUserTabs } from '@rocket.chat/core-typings';
import { Button, ButtonGroup, ContextualbarIcon, Tabs, TabsItem } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { usePermission, useRouteParameter, useTranslation, useRouter } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useRef } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';

import UserPageHeaderContentWithSeatsCap from '../../../../ee/client/views/admin/users/UserPageHeaderContentWithSeatsCap';
import { useSeatsCap } from '../../../../ee/client/views/admin/users/useSeatsCap';
Expand All @@ -12,6 +14,8 @@ import {
ContextualbarClose,
ContextualbarDialog,
} from '../../../components/Contextualbar';
import { usePagination } from '../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../components/GenericTable/hooks/useSort';
import { Page, PageHeader, PageContent } from '../../../components/Page';
import { useShouldPreventAction } from '../../../hooks/useShouldPreventAction';
import AdminInviteUsers from './AdminInviteUsers';
Expand All @@ -20,12 +24,18 @@ import AdminUserFormWithData from './AdminUserFormWithData';
import AdminUserInfoWithData from './AdminUserInfoWithData';
import AdminUserUpgrade from './AdminUserUpgrade';
import UsersTable from './UsersTable';
import useFilteredUsers from './hooks/useFilteredUsers';

const UsersPage = (): ReactElement => {
export type UsersFilters = {
text: string;
};

export type UsersTableSortingOptions = 'name' | 'username' | 'emails.address' | 'status';

const AdminUsersPage = (): ReactElement => {
const t = useTranslation();

const seatsCap = useSeatsCap();
const reload = useRef(() => null);

const router = useRouter();
const context = useRouteParameter('context');
Expand All @@ -36,12 +46,36 @@ const UsersPage = (): ReactElement => {

const isCreateUserDisabled = useShouldPreventAction('activeUsers');

const paginationData = usePagination();
const sortData = useSort<'name' | 'username' | 'emails.address' | 'status'>('name');

const [tab, setTab] = useState<IAdminUserTabs>('all');
const [userFilters, setUserFilters] = useState<UsersFilters>({ text: '' });

const searchTerm = useDebouncedValue(userFilters.text, 500);
const prevSearchTerm = useRef('');

const filteredUsersQueryResult = useFilteredUsers({
searchTerm,
prevSearchTerm,
sortData,
paginationData,
tab,
});

const handleReload = (): void => {
seatsCap?.reload();
reload.current();
filteredUsersQueryResult?.refetch();
};

const isRoutePrevented = context && ['new', 'invite'].includes(context) && isCreateUserDisabled;
useEffect(() => {
prevSearchTerm.current = searchTerm;
}, [searchTerm]);

const isRoutePrevented = useMemo(
() => context && ['new', 'invite'].includes(context) && isCreateUserDisabled,
[context, isCreateUserDisabled],
);

return (
<Page flexDirection='row'>
Expand All @@ -65,7 +99,19 @@ const UsersPage = (): ReactElement => {
)}
</PageHeader>
<PageContent>
<UsersTable reload={reload} />
<Tabs>
<TabsItem selected={!tab || tab === 'all'} onClick={() => setTab('all')}>
{t('All')}
</TabsItem>
</Tabs>
<UsersTable
filteredUsersQueryResult={filteredUsersQueryResult}
setUserFilters={setUserFilters}
onReload={handleReload}
paginationData={paginationData}
sortData={sortData}
tab={tab}
/>
</PageContent>
</Page>
{context && (
Expand Down Expand Up @@ -93,4 +139,4 @@ const UsersPage = (): ReactElement => {
);
};

export default UsersPage;
export default AdminUsersPage;
198 changes: 100 additions & 98 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Pagination } 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 type { ReactElement, MutableRefObject } from 'react';
import React, { useRef, useMemo, useState, useEffect } from 'react';
import type { IAdminUserTabs, Serialized } from '@rocket.chat/core-typings';
import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage';
import { useMediaQuery, useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings';
import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import type { ReactElement, Dispatch, SetStateAction } from 'react';
import React, { useCallback, useMemo } from 'react';

import FilterByText from '../../../../components/FilterByText';
import GenericNoResults from '../../../../components/GenericNoResults';
Expand All @@ -15,93 +16,67 @@ import {
GenericTableBody,
GenericTableLoadingTable,
} from '../../../../components/GenericTable';
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../../components/GenericTable/hooks/useSort';
import type { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import type { useSort } from '../../../../components/GenericTable/hooks/useSort';
import type { UsersFilters } from '../AdminUsersPage';
import UsersTableRow from './UsersTableRow';

type UsersTableProps = {
reload: MutableRefObject<() => void>;
tab: IAdminUserTabs;
onReload: () => void;
setUserFilters: Dispatch<SetStateAction<UsersFilters>>;
filteredUsersQueryResult: UseQueryResult<PaginatedResult<{ users: Serialized<DefaultUserInfo>[] }>>;
paginationData: ReturnType<typeof usePagination>;
sortData: ReturnType<typeof useSort<'name' | 'username' | 'emails.address' | 'status'>>;
};

// TODO: Missing error state
const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
const UsersTable = ({
filteredUsersQueryResult,
setUserFilters,
tab,
onReload,
paginationData,
sortData,
}: 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 { data, isLoading, isError, isSuccess } = filteredUsersQueryResult;

const searchTerm = useDebouncedValue(text, 500);
const prevSearchTerm = useRef<string>('');
const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = paginationData;
const { sortBy, sortDirection, setSort } = sortData;

const query = useDebouncedValue(
useMemo(() => {
if (searchTerm !== prevSearchTerm.current) {
setCurrent(0);
}
const isKeyboardEvent = (
event: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>,
): event is React.KeyboardEvent<HTMLElement> => {
return (event as React.KeyboardEvent<HTMLElement>).key !== undefined;
};

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 handleClickOrKeyDown = useEffectEvent(
(id, e: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>): void => {
e.stopPropagation();

const getUsers = useEndpoint('GET', '/v1/users.list');
const keyboardSubmitKeys = ['Enter', ' '];

const dispatchToastMessage = useToastMessageDispatch();
if (isKeyboardEvent(e) && !keyboardSubmitKeys.includes(e.key)) {
return;
}

const { data, isLoading, error, isSuccess, refetch } = useQuery(
['users', query],
async () => {
const users = await getUsers(query);
return users;
},
{
onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
},
router.navigate({
name: 'admin-users',
params: {
context: 'info',
id,
},
});
},
);

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

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

const handleClick = useMutableCallback((id): void =>
usersRoute.push({
context: 'info',
id,
}),
);

const headers = useMemo(
() => [
<GenericTableHeaderCell w='x200' key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
<GenericTableHeaderCell w='x240' key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
{t('Name')}
</GenericTableHeaderCell>,
mediaQuery && (
Expand All @@ -116,48 +91,76 @@ const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
{t('Username')}
</GenericTableHeaderCell>
),
<GenericTableHeaderCell
w='x120'
key='email'
direction={sortDirection}
active={sortBy === 'emails.address'}
onClick={setSort}
sort='emails.address'
>
{t('Email')}
</GenericTableHeaderCell>,
mediaQuery && (
<GenericTableHeaderCell
w='x120'
key='email'
direction={sortDirection}
active={sortBy === 'emails.address'}
onClick={setSort}
sort='emails.address'
>
{t('Email')}
</GenericTableHeaderCell>
),
mediaQuery && (
<GenericTableHeaderCell w='x120' key='roles' onClick={setSort}>
{t('Roles')}
</GenericTableHeaderCell>
),
<GenericTableHeaderCell w='x100' key='status' direction={sortDirection} active={sortBy === 'status'} onClick={setSort} sort='status'>
{t('Status')}
</GenericTableHeaderCell>,
tab === 'all' && (
<GenericTableHeaderCell
w='x100'
key='status'
direction={sortDirection}
active={sortBy === 'status'}
onClick={setSort}
sort='status'
>
{t('Registration_status')}
</GenericTableHeaderCell>
),
],
[mediaQuery, setSort, sortBy, sortDirection, t],
[mediaQuery, setSort, sortBy, sortDirection, t, tab],
);

if (error) {
return null;
}

const handleSearchTextChange = useCallback(
({ text }) => {
setUserFilters({ text });
},
[setUserFilters],
);
return (
<>
<FilterByText shouldAutoFocus placeholder={t('Search_Users')} onChange={({ text }): void => setText(text)} />
<FilterByText shouldAutoFocus placeholder={t('Search_Users')} onChange={handleSearchTextChange} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>{isLoading && <GenericTableLoadingTable headerCells={5} />}</GenericTableBody>
<GenericTableBody>
<GenericTableLoadingTable headerCells={5} />
</GenericTableBody>
</GenericTable>
)}
{data?.users && data.count > 0 && isSuccess && (

{isError && (
<States>
<StatesIcon name='warning' variation='danger' />
<StatesTitle>{t('Something_went_wrong')}</StatesTitle>
<StatesActions>
<StatesAction onClick={onReload}>{t('Reload_page')}</StatesAction>
</StatesActions>
</States>
)}

{isSuccess && data.users.length === 0 && <GenericNoResults />}

{isSuccess && !!data?.users && (
<>
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>
<GenericTableBody>
{data?.users.map((user) => (
<UsersTableRow key={user._id} onClick={handleClick} mediaQuery={mediaQuery} user={user} />
{data.users.map((user) => (
<UsersTableRow key={user._id} onClick={handleClickOrKeyDown} mediaQuery={mediaQuery} user={user} tab={tab} />
))}
</GenericTableBody>
</GenericTable>
Expand All @@ -172,7 +175,6 @@ const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
/>
</>
)}
{isSuccess && data?.count === 0 && <GenericNoResults />}
</>
);
};
Expand Down
Loading
Loading