feat: New users page active tab (#32024)

Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com>
pull/32811/head^2
Henrique Guimarães Ribeiro 1 year ago committed by GitHub
parent fa82159492
commit 3ffe4a2944
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/rotten-eggs-end.md
  2. 3
      apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx
  3. 12
      apps/meteor/client/views/admin/users/AdminUsersPage.tsx
  4. 17
      apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx
  5. 78
      apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx
  6. 6
      apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts
  7. 3
      packages/i18n/src/locales/en.i18n.json
  8. 26
      packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx
  9. 4
      packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx
  10. 10
      packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx

@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": patch
"@rocket.chat/ui-client": patch
---
Implemented a new tab to the users page called 'Active', this tab lists all users who have logged in for the first time and are active.

@ -9,7 +9,6 @@ const initialRoomTypeFilterStructure = [
{
id: 'filter_by_room',
text: 'Filter_by_room',
isGroupTitle: true,
},
{
id: 'd',
@ -71,7 +70,7 @@ const RoomsTableFilters = ({ setFilters }: { setFilters: Dispatch<SetStateAction
setRoomTypeSelectedOptions(options);
},
[text, setFilters],
) as Dispatch<SetStateAction<OptionProp[]>>;
);
return (
<Box

@ -1,8 +1,10 @@
import type { IAdminUserTabs, LicenseInfo } from '@rocket.chat/core-typings';
import { Button, ButtonGroup, Callout, ContextualbarIcon, Skeleton, Tabs, TabsItem } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import type { OptionProp } from '@rocket.chat/ui-client';
import { ExternalLink } from '@rocket.chat/ui-client';
import { usePermission, useRouteParameter, useTranslation, useRouter } from '@rocket.chat/ui-contexts';
import { usePermission, useRouteParameter, useTranslation, useRouter, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Trans } from 'react-i18next';
@ -33,6 +35,7 @@ import { useSeatsCap } from './useSeatsCap';
export type UsersFilters = {
text: string;
roles: OptionProp[];
};
export type UsersTableSortingOptions = 'name' | 'username' | 'emails.address' | 'status' | 'active';
@ -55,11 +58,14 @@ const AdminUsersPage = (): ReactElement => {
const isCreateUserDisabled = useShouldPreventAction('activeUsers');
const getRoles = useEndpoint('GET', '/v1/roles.list');
const { data } = useQuery(['roles'], async () => getRoles());
const paginationData = usePagination();
const sortData = useSort<UsersTableSortingOptions>('name');
const [tab, setTab] = useState<IAdminUserTabs>('all');
const [userFilters, setUserFilters] = useState<UsersFilters>({ text: '' });
const [userFilters, setUserFilters] = useState<UsersFilters>({ text: '', roles: [] });
const searchTerm = useDebouncedValue(userFilters.text, 500);
const prevSearchTerm = useRef('');
@ -70,6 +76,7 @@ const AdminUsersPage = (): ReactElement => {
sortData,
paginationData,
tab,
selectedRoles: useMemo(() => userFilters.roles.map((role) => role.id), [userFilters.roles]),
});
const pendingUsersCount = usePendingUsersCount(filteredUsersQueryResult.data?.users);
@ -153,6 +160,7 @@ const AdminUsersPage = (): ReactElement => {
sortData={sortData}
tab={tab}
isSeatsCapExceeded={isSeatsCapExceeded}
roleData={data}
/>
</PageContent>
</Page>

@ -1,13 +1,12 @@
import type { IAdminUserTabs, Serialized } from '@rocket.chat/core-typings';
import type { IAdminUserTabs, IRole, Serialized } from '@rocket.chat/core-typings';
import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage';
import { useEffectEvent, useBreakpoints } 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 React, { useMemo } from 'react';
import FilterByText from '../../../../components/FilterByText';
import GenericNoResults from '../../../../components/GenericNoResults';
import {
GenericTable,
@ -19,10 +18,12 @@ import {
import type { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import type { useSort } from '../../../../components/GenericTable/hooks/useSort';
import type { UsersFilters, UsersTableSortingOptions } from '../AdminUsersPage';
import UsersTableFilters from './UsersTableFilters';
import UsersTableRow from './UsersTableRow';
type UsersTableProps = {
tab: IAdminUserTabs;
roleData: { roles: IRole[] } | undefined;
onReload: () => void;
setUserFilters: Dispatch<SetStateAction<UsersFilters>>;
filteredUsersQueryResult: UseQueryResult<PaginatedResult<{ users: Serialized<DefaultUserInfo>[] }>>;
@ -34,6 +35,7 @@ type UsersTableProps = {
const UsersTable = ({
filteredUsersQueryResult,
setUserFilters,
roleData,
tab,
onReload,
paginationData,
@ -113,15 +115,10 @@ const UsersTable = ({
[isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab],
);
const handleSearchTextChange = useCallback(
({ text }) => {
setUserFilters({ text });
},
[setUserFilters],
);
return (
<>
<FilterByText shouldAutoFocus placeholder={t('Search_Users')} onChange={handleSearchTextChange} />
<UsersTableFilters roleData={roleData} setUsersFilters={setUserFilters} />
{isLoading && (
<GenericTable>
<GenericTableHeader>{headers}</GenericTableHeader>

@ -0,0 +1,78 @@
import type { IRole } from '@rocket.chat/core-typings';
import { useBreakpoints } from '@rocket.chat/fuselage-hooks';
import type { OptionProp } from '@rocket.chat/ui-client';
import { MultiSelectCustom } from '@rocket.chat/ui-client';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import FilterByText from '../../../../components/FilterByText';
import type { UsersFilters } from '../AdminUsersPage';
type UsersTableFiltersProps = {
setUsersFilters: React.Dispatch<React.SetStateAction<UsersFilters>>;
roleData: { roles: IRole[] } | undefined;
};
const UsersTableFilters = ({ roleData, setUsersFilters }: UsersTableFiltersProps) => {
const { t } = useTranslation();
const [selectedRoles, setSelectedRoles] = useState<OptionProp[]>([]);
const [text, setText] = useState('');
const handleSearchTextChange = useCallback(
({ text }) => {
setUsersFilters({ text, roles: selectedRoles });
setText(text);
},
[selectedRoles, setUsersFilters],
);
const handleRolesChange = useCallback(
(roles: OptionProp[]) => {
setUsersFilters({ text, roles });
setSelectedRoles(roles);
},
[setUsersFilters, text],
);
const userRolesFilterStructure = useMemo(
() => [
{
id: 'filter_by_role',
text: 'Filter_by_role',
},
{
id: 'all',
text: 'All_roles',
checked: false,
},
...(roleData
? roleData.roles.map((role) => ({
id: role._id,
text: role.description || role.name || role._id,
checked: false,
}))
: []),
],
[roleData],
);
const breakpoints = useBreakpoints();
const fixFiltersSize = breakpoints.includes('lg') ? { maxWidth: 'x224', minWidth: 'x224' } : null;
return (
<FilterByText shouldAutoFocus placeholder={t('Search_Users')} onChange={handleSearchTextChange}>
<MultiSelectCustom
dropdownOptions={userRolesFilterStructure}
defaultTitle='All_roles'
selectedOptionsTitle='Roles'
setSelectedOptions={handleRolesChange}
selectedOptions={selectedRoles}
searchBarText='Search_roles'
{...fixFiltersSize}
/>
</FilterByText>
);
};
export default UsersTableFilters;

@ -15,9 +15,10 @@ type UseFilteredUsersOptions = {
tab: IAdminUserTabs;
paginationData: ReturnType<typeof usePagination>;
sortData: ReturnType<typeof useSort<UsersTableSortingOptions>>;
selectedRoles: string[];
};
const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData, tab }: UseFilteredUsersOptions) => {
const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData, tab, selectedRoles }: UseFilteredUsersOptions) => {
const { setCurrent, itemsPerPage, current } = paginationData;
const { sortBy, sortDirection } = sortData;
@ -45,11 +46,12 @@ const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData
return {
...listUsersPayload[tab],
searchTerm,
roles: selectedRoles,
sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`,
count: itemsPerPage,
offset: searchTerm === prevSearchTerm.current ? current : 0,
};
}, [current, itemsPerPage, prevSearchTerm, searchTerm, setCurrent, sortBy, sortDirection, tab]);
}, [current, itemsPerPage, prevSearchTerm, searchTerm, selectedRoles, setCurrent, sortBy, sortDirection, tab]);
const getUsers = useEndpoint('GET', '/v1/users.listByStatus');
const dispatchToastMessage = useToastMessageDispatch();
const usersListQueryResult = useQuery(['users.list', payload, tab], async () => getUsers(payload), {

@ -410,6 +410,7 @@
"AutoLinker_UrlsRegExp": "AutoLinker URL Regular Expression",
"All_messages": "All messages",
"All_Prices": "All prices",
"All_roles": "All roles",
"All_status": "All status",
"All_users": "All users",
"All_users_in_the_channel_can_write_new_messages": "All users in the channel can write new messages",
@ -2433,6 +2434,7 @@
"Filter_by_category": "Filter by Category",
"Filter_by_Custom_Fields": "Filter by Custom Fields",
"Filter_By_Price": "Filter by price",
"Filter_by_role": "Filter by role",
"Filter_By_Status": "Filter by status",
"Filters": "Filters",
"Filters_applied": "Filters applied",
@ -4763,6 +4765,7 @@
"Search_Page_Size": "Page Size",
"Search_Private_Groups": "Search Private Groups",
"Search_Provider": "Search Provider",
"Search_roles": "Search roles",
"Search_rooms": "Search rooms",
"Search_Rooms": "Search Rooms",
"Search_Users": "Search Users",

@ -1,7 +1,7 @@
import { Box } from '@rocket.chat/fuselage';
import { Box, Button } from '@rocket.chat/fuselage';
import { useOutsideClick, useToggle } from '@rocket.chat/fuselage-hooks';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import type { Dispatch, FormEvent, ReactElement, RefObject, SetStateAction } from 'react';
import type { ComponentProps, FormEvent, ReactElement, RefObject } from 'react';
import { useCallback, useRef } from 'react';
import MultiSelectCustomAnchor from './MultiSelectCustomAnchor';
@ -21,22 +21,12 @@ const onMouseEventPreventSideEffects = (e: MouseEvent): void => {
e.stopImmediatePropagation();
};
type TitleOptionProp = {
export type OptionProp = {
id: string;
text: string;
isGroupTitle: boolean;
checked: never;
checked?: boolean;
};
type CheckboxOptionProp = {
id: string;
text: string;
isGroupTitle: never;
checked: boolean;
};
export type OptionProp = TitleOptionProp | CheckboxOptionProp;
/**
* @param dropdownOptions options available for the multiselect dropdown list
* @param defaultTitle dropdown text before selecting any options (or all of them). For example: 'All rooms'
@ -56,9 +46,9 @@ type DropDownProps = {
defaultTitle: TranslationKey;
selectedOptionsTitle: TranslationKey;
selectedOptions: OptionProp[];
setSelectedOptions: Dispatch<SetStateAction<OptionProp[]>>;
setSelectedOptions: (roles: OptionProp[]) => void;
searchBarText?: TranslationKey;
};
} & ComponentProps<typeof Button>;
export const MultiSelectCustom = ({
dropdownOptions,
@ -67,6 +57,7 @@ export const MultiSelectCustom = ({
selectedOptions,
setSelectedOptions,
searchBarText,
...props
}: DropDownProps): ReactElement => {
const reference = useRef<HTMLInputElement>(null);
const target = useRef<HTMLElement>(null);
@ -102,7 +93,7 @@ export const MultiSelectCustom = ({
const count = dropdownOptions.filter((option) => option.checked).length;
return (
<Box display='flex' flexGrow={1} position='relative'>
<Box display='flex' position='relative'>
<MultiSelectCustomAnchor
ref={reference}
collapsed={collapsed}
@ -112,6 +103,7 @@ export const MultiSelectCustom = ({
selectedOptionsTitle={selectedOptionsTitle}
selectedOptionsCount={count}
maxCount={dropdownOptions.length}
{...props}
/>
{collapsed && (
<MultiSelectCustomListWrapper ref={target}>

@ -14,7 +14,7 @@ type MultiSelectCustomAnchorProps = {
} & ComponentProps<typeof Box>;
const MultiSelectCustomAnchor = forwardRef<HTMLElement, MultiSelectCustomAnchorProps>(function MultiSelectCustomAnchor(
{ collapsed, selectedOptionsCount, selectedOptionsTitle, defaultTitle, maxCount, ...props },
{ className, collapsed, selectedOptionsCount, selectedOptionsTitle, defaultTitle, maxCount, ...props },
ref,
) {
const t = useTranslation();
@ -34,7 +34,7 @@ const MultiSelectCustomAnchor = forwardRef<HTMLElement, MultiSelectCustomAnchorP
justifyContent='space-between'
alignItems='center'
h='x40'
className={['rcx-input-box__wrapper', customStyle].filter(Boolean)}
className={['rcx-input-box__wrapper', customStyle, ...(Array.isArray(className) ? className : [className])].filter(Boolean)}
{...props}
>
{isDirty ? `${t(selectedOptionsTitle)} (${selectedOptionsCount})` : t(defaultTitle)}

@ -40,11 +40,7 @@ const MultiSelectCustomList = ({
)}
{filteredOptions.map((option) => (
<Fragment key={option.id}>
{option.isGroupTitle ? (
<Box mi='x12' mb='x4' fontScale='p2b' color='default'>
{t(option.text as TranslationKey)}
</Box>
) : (
{option.hasOwnProperty('checked') ? (
<Option key={option.id}>
<Box pis='x4' pb='x4' w='full' display='flex' justifyContent='space-between' is='label'>
{t(option.text as TranslationKey)}
@ -52,6 +48,10 @@ const MultiSelectCustomList = ({
<CheckBox checked={option.checked} pi={0} name={option.text} id={option.id} onChange={() => onSelected(option)} />
</Box>
</Option>
) : (
<Box mi='x12' mb='x4' fontScale='p2b' color='default'>
{t(option.text as TranslationKey)}
</Box>
)}
</Fragment>
))}

Loading…
Cancel
Save