diff --git a/.changeset/rotten-eggs-end.md b/.changeset/rotten-eggs-end.md new file mode 100644 index 000000000000..7d0ad6ee5047 --- /dev/null +++ b/.changeset/rotten-eggs-end.md @@ -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. diff --git a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx index 1ed21c1234a9..d52d45415c8a 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsTableFilters.tsx @@ -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>; + ); return ( { const isCreateUserDisabled = useShouldPreventAction('activeUsers'); + const getRoles = useEndpoint('GET', '/v1/roles.list'); + const { data } = useQuery(['roles'], async () => getRoles()); + const paginationData = usePagination(); const sortData = useSort('name'); const [tab, setTab] = useState('all'); - const [userFilters, setUserFilters] = useState({ text: '' }); + const [userFilters, setUserFilters] = useState({ 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} /> diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index fa35df715fc5..01d7007561eb 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -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>; filteredUsersQueryResult: UseQueryResult[] }>>; @@ -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 ( <> - + + {isLoading && ( {headers} diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx new file mode 100644 index 000000000000..28508ac94ac5 --- /dev/null +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx @@ -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>; + roleData: { roles: IRole[] } | undefined; +}; + +const UsersTableFilters = ({ roleData, setUsersFilters }: UsersTableFiltersProps) => { + const { t } = useTranslation(); + + const [selectedRoles, setSelectedRoles] = useState([]); + 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 ( + + + + ); +}; + +export default UsersTableFilters; diff --git a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts index f8ea02a34d82..9a592d5e449f 100644 --- a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts +++ b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts @@ -15,9 +15,10 @@ type UseFilteredUsersOptions = { tab: IAdminUserTabs; paginationData: ReturnType; sortData: ReturnType>; + 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), { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index f9fcf0163caf..0b569b4ee564 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -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", diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx index affe467cd965..317f56899d05 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustom.tsx @@ -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>; + setSelectedOptions: (roles: OptionProp[]) => void; searchBarText?: TranslationKey; -}; +} & ComponentProps; export const MultiSelectCustom = ({ dropdownOptions, @@ -67,6 +57,7 @@ export const MultiSelectCustom = ({ selectedOptions, setSelectedOptions, searchBarText, + ...props }: DropDownProps): ReactElement => { const reference = useRef(null); const target = useRef(null); @@ -102,7 +93,7 @@ export const MultiSelectCustom = ({ const count = dropdownOptions.filter((option) => option.checked).length; return ( - + {collapsed && ( diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx index 0a8aee69344b..3a03673bc701 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomAnchor.tsx @@ -14,7 +14,7 @@ type MultiSelectCustomAnchorProps = { } & ComponentProps; const MultiSelectCustomAnchor = forwardRef(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 {isDirty ? `${t(selectedOptionsTitle)} (${selectedOptionsCount})` : t(defaultTitle)} diff --git a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx index d8f8d60d8096..71cb54f81aa5 100644 --- a/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx +++ b/packages/ui-client/src/components/MultiSelectCustom/MultiSelectCustomList.tsx @@ -40,11 +40,7 @@ const MultiSelectCustomList = ({ )} {filteredOptions.map((option) => ( - {option.isGroupTitle ? ( - - {t(option.text as TranslationKey)} - - ) : ( + {option.hasOwnProperty('checked') ? ( + ) : ( + + {t(option.text as TranslationKey)} + )} ))}