From 25011cbfc9f63a3cf2c9102e7cb40665671c2d76 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 31 May 2022 12:33:06 -0300 Subject: [PATCH 1/6] wip --- .../client/views/admin/users/AddUser.js | 6 +- .../{UserInfo.js => AdminUserInfoWithData.js} | 8 +- ...itUserWithData.js => EditUserWithData.tsx} | 11 +- .../users/{InviteUsers.js => InviteUsers.tsx} | 16 +- .../admin/users/UserPageHeaderContent.tsx | 31 ---- .../client/views/admin/users/UserRow.js | 61 ------- .../client/views/admin/users/UsersPage.js | 122 ------------- .../client/views/admin/users/UsersPage.tsx | 90 ++++++++++ .../users/{UsersRoute.js => UsersRoute.tsx} | 6 +- .../client/views/admin/users/UsersTable.js | 126 ------------- .../admin/users/UsersTable/UsersTable.tsx | 165 ++++++++++++++++++ .../admin/users/UsersTable/UsersTableRow.tsx | 63 +++++++ .../views/admin/users/UsersTable/index.ts | 1 + .../rocketchat-i18n/i18n/en.i18n.json | 2 + packages/core-typings/src/IUser.ts | 3 + .../rest-typings/src/v1/customUserStatus.ts | 2 +- packages/rest-typings/src/v1/users.ts | 15 +- 17 files changed, 366 insertions(+), 362 deletions(-) rename apps/meteor/client/views/admin/users/{UserInfo.js => AdminUserInfoWithData.js} (93%) rename apps/meteor/client/views/admin/users/{EditUserWithData.js => EditUserWithData.tsx} (70%) rename apps/meteor/client/views/admin/users/{InviteUsers.js => InviteUsers.tsx} (75%) delete mode 100644 apps/meteor/client/views/admin/users/UserPageHeaderContent.tsx delete mode 100644 apps/meteor/client/views/admin/users/UserRow.js delete mode 100644 apps/meteor/client/views/admin/users/UsersPage.js create mode 100644 apps/meteor/client/views/admin/users/UsersPage.tsx rename apps/meteor/client/views/admin/users/{UsersRoute.js => UsersRoute.tsx} (80%) delete mode 100644 apps/meteor/client/views/admin/users/UsersTable.js create mode 100644 apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx create mode 100644 apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx create mode 100644 apps/meteor/client/views/admin/users/UsersTable/index.ts diff --git a/apps/meteor/client/views/admin/users/AddUser.js b/apps/meteor/client/views/admin/users/AddUser.js index 010d20659b81..0be3bd91fbad 100644 --- a/apps/meteor/client/views/admin/users/AddUser.js +++ b/apps/meteor/client/views/admin/users/AddUser.js @@ -8,7 +8,7 @@ import { useEndpointData } from '../../../hooks/useEndpointData'; import { useForm } from '../../../hooks/useForm'; import UserForm from './UserForm'; -export function AddUser({ roles, onReload, ...props }) { +const AddUser = ({ onReload, ...props }) => { const t = useTranslation(); const router = useRoute('admin-users'); @@ -126,4 +126,6 @@ export function AddUser({ roles, onReload, ...props }) { return ( ); -} +}; + +export default AddUser; diff --git a/apps/meteor/client/views/admin/users/UserInfo.js b/apps/meteor/client/views/admin/users/AdminUserInfoWithData.js similarity index 93% rename from apps/meteor/client/views/admin/users/UserInfo.js rename to apps/meteor/client/views/admin/users/AdminUserInfoWithData.js index 4a0be4ef9681..38bfbd478d0b 100644 --- a/apps/meteor/client/views/admin/users/UserInfo.js +++ b/apps/meteor/client/views/admin/users/AdminUserInfoWithData.js @@ -13,7 +13,7 @@ import { getUserEmailVerified } from '../../../lib/utils/getUserEmailVerified'; import UserInfo from '../../room/contextualBar/UserInfo/UserInfo'; import { UserInfoActions } from './UserInfoActions'; -export function UserInfoWithData({ uid, username, onReload, ...props }) { +const AdminUserInfoWithData = ({ uid, onReload, ...props }) => { const t = useTranslation(); const showRealNames = useSetting('UI_Use_Real_Name'); const getRoles = useRolesDescription(); @@ -26,7 +26,7 @@ export function UserInfoWithData({ uid, username, onReload, ...props }) { reload: reloadUserInfo, } = useEndpointData( 'users.info', - useMemo(() => ({ ...(uid && { userId: uid }), ...(username && { username }) }), [uid, username]), + useMemo(() => ({ ...(uid && { userId: uid }) }), [uid]), ); const onChange = useMutableCallback(() => { @@ -96,4 +96,6 @@ export function UserInfoWithData({ uid, username, onReload, ...props }) { {...props} /> ); -} +}; + +export default AdminUserInfoWithData; diff --git a/apps/meteor/client/views/admin/users/EditUserWithData.js b/apps/meteor/client/views/admin/users/EditUserWithData.tsx similarity index 70% rename from apps/meteor/client/views/admin/users/EditUserWithData.js rename to apps/meteor/client/views/admin/users/EditUserWithData.tsx index aec9e89807c1..fa09e469c4a3 100644 --- a/apps/meteor/client/views/admin/users/EditUserWithData.js +++ b/apps/meteor/client/views/admin/users/EditUserWithData.tsx @@ -1,15 +1,16 @@ +import { IUser } from '@rocket.chat/core-typings'; import { Box, Callout } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo } from 'react'; +import React, { useMemo, ReactElement } from 'react'; import { FormSkeleton } from '../../../components/Skeleton'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; import EditUser from './EditUser'; -function EditUserWithData({ uid, ...props }) { +const EditUserWithData = ({ uid, onReload, ...props }: { uid: IUser['_id']; onReload: () => void }): ReactElement => { const t = useTranslation(); - const { value: roleData, phase: roleState, error: roleError } = useEndpointData('roles.list', ''); + const { value: roleData, phase: roleState, error: roleError } = useEndpointData('roles.list'); const { value: data, phase: state, @@ -35,7 +36,7 @@ function EditUserWithData({ uid, ...props }) { ); } - return ; -} + return ; +}; export default EditUserWithData; diff --git a/apps/meteor/client/views/admin/users/InviteUsers.js b/apps/meteor/client/views/admin/users/InviteUsers.tsx similarity index 75% rename from apps/meteor/client/views/admin/users/InviteUsers.js rename to apps/meteor/client/views/admin/users/InviteUsers.tsx index 7344fcb85ebd..6c05439b851e 100644 --- a/apps/meteor/client/views/admin/users/InviteUsers.js +++ b/apps/meteor/client/views/admin/users/InviteUsers.tsx @@ -1,17 +1,18 @@ import { Box, Button, Icon, TextAreaInput } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useState, ReactElement, ChangeEvent } from 'react'; import { validateEmail } from '../../../../lib/emailValidator'; import VerticalBar from '../../../components/VerticalBar'; -export function InviteUsers({ data, ...props }) { +const InviteUsers = ({ ...props }): ReactElement => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const [text, setText] = useState(''); const sendInvites = useMethod('sendInvitationEmail'); - const getEmails = useCallback((text) => text.split(/[\ ,;]+/i).filter((val) => validateEmail(val)), []); - const handleClick = async () => { + const getEmails = useCallback((text) => text.split(/[\ ,;]+/i).filter((val: string) => validateEmail(val)), []); + + const handleClick = async (): Promise => { try { await sendInvites(getEmails(text)); dispatchToastMessage({ type: 'success', message: t('Emails_sent_successfully!') }); @@ -19,6 +20,7 @@ export function InviteUsers({ data, ...props }) { dispatchToastMessage({ type: 'error', message: error.message }); } }; + return ( @@ -27,11 +29,13 @@ export function InviteUsers({ data, ...props }) { {t('Send_invitation_email_info')} - setText(e.currentTarget.value)} /> + ): void => setText(e.currentTarget.value)} /> ); -} +}; + +export default InviteUsers; diff --git a/apps/meteor/client/views/admin/users/UserPageHeaderContent.tsx b/apps/meteor/client/views/admin/users/UserPageHeaderContent.tsx deleted file mode 100644 index f7bb9928f68d..000000000000 --- a/apps/meteor/client/views/admin/users/UserPageHeaderContent.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { ReactElement } from 'react'; - -const UserPageHeaderContent = (): ReactElement => { - const usersRoute = useRoute('admin-users'); - const t = useTranslation(); - - const handleNewButtonClick = (): void => { - usersRoute.push({ context: 'new' }); - }; - - const handleInviteButtonClick = (): void => { - usersRoute.push({ context: 'invite' }); - }; - - return ( - <> - - - - - - ); -}; - -export default UserPageHeaderContent; diff --git a/apps/meteor/client/views/admin/users/UserRow.js b/apps/meteor/client/views/admin/users/UserRow.js deleted file mode 100644 index 5844c6bcb4a0..000000000000 --- a/apps/meteor/client/views/admin/users/UserRow.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Box, Table } from '@rocket.chat/fuselage'; -import { capitalize } from '@rocket.chat/string-helpers'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import { Roles } from '../../../../app/models/client'; -import UserAvatar from '../../../components/avatar/UserAvatar'; - -const style = { - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - overflow: 'hidden', -}; - -const UserRow = ({ emails, _id, username, name, roles, status, avatarETag, onClick, mediaQuery, active }) => { - const t = useTranslation(); - - const statusText = active ? t(capitalize(status)) : t('Disabled'); - const roleNames = (roles || []) - .map((roleId) => Roles.findOne(roleId, { fields: { name: 1 } })?.name) - .filter((roleName) => !!roleName) - .join(', '); - - return ( - - - - - - - - {name || username} - - {!mediaQuery && name && ( - - {' '} - {`@${username}`}{' '} - - )} - - - - - {mediaQuery && ( - - - {username} - {' '} - - - )} - {emails && emails.length && emails[0].address} - {mediaQuery && {roleNames}} - - {statusText} - - - ); -}; - -export default UserRow; diff --git a/apps/meteor/client/views/admin/users/UsersPage.js b/apps/meteor/client/views/admin/users/UsersPage.js deleted file mode 100644 index de5a6960468c..000000000000 --- a/apps/meteor/client/views/admin/users/UsersPage.js +++ /dev/null @@ -1,122 +0,0 @@ -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useEffect, useMemo, useState } from 'react'; - -import UserPageHeaderContentWithSeatsCap from '../../../../ee/client/views/admin/users/UserPageHeaderContentWithSeatsCap'; -import { useSeatsCap } from '../../../../ee/client/views/admin/users/useSeatsCap'; -import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; -import { useEndpointData } from '../../../hooks/useEndpointData'; -import { AddUser } from './AddUser'; -import EditUserWithData from './EditUserWithData'; -import { InviteUsers } from './InviteUsers'; -import { UserInfoWithData } from './UserInfo'; -import UserPageHeaderContent from './UserPageHeaderContent'; -import UsersTable from './UsersTable'; - -const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1); - -const useQuery = ({ text, itemsPerPage, current }, sortFields) => - useMemo( - () => ({ - fields: JSON.stringify({ - name: 1, - username: 1, - emails: 1, - roles: 1, - status: 1, - avatarETag: 1, - active: 1, - }), - query: JSON.stringify({ - $or: [ - { 'emails.address': { $regex: text || '', $options: 'i' } }, - { username: { $regex: text || '', $options: 'i' } }, - { name: { $regex: text || '', $options: 'i' } }, - ], - }), - sort: JSON.stringify( - sortFields.reduce((agg, [column, direction]) => { - agg[column] = sortDir(direction); - return agg; - }, {}), - ), - ...(itemsPerPage && { count: itemsPerPage }), - ...(current && { offset: current }), - }), - [text, itemsPerPage, current, sortFields], - ); - -function UsersPage() { - const context = useRouteParameter('context'); - const id = useRouteParameter('id'); - const seatsCap = useSeatsCap(); - const usersRoute = useRoute('admin-users'); - - useEffect(() => { - if (!context || !seatsCap) { - return; - } - - if (seatsCap.activeUsers >= seatsCap.maxActiveUsers && !['edit', 'info'].includes(context)) { - usersRoute.push({}); - } - }, [context, seatsCap, usersRoute]); - - const t = useTranslation(); - - const handleVerticalBarCloseButtonClick = () => { - usersRoute.push({}); - }; - const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 }); - const [sort, setSort] = useState([ - ['name', 'asc'], - ['usernames', 'asc'], - ]); - - const debouncedParams = useDebouncedValue(params, 500); - const debouncedSort = useDebouncedValue(sort, 500); - const query = useQuery(debouncedParams, debouncedSort); - const { value: data = {}, reload: reloadList } = useEndpointData('users.list', query); - - const reload = () => { - seatsCap?.reload(); - reloadList(); - }; - - return ( - - - - {seatsCap && - (seatsCap.maxActiveUsers < Number.POSITIVE_INFINITY ? ( - - ) : ( - - ))} - - - - - - {context && ( - - - {context === 'info' && t('User_Info')} - {context === 'edit' && t('Edit_User')} - {context === 'new' && t('Add_User')} - {context === 'invite' && t('Invite_Users')} - - - - {context === 'info' && id && } - {context === 'edit' && } - {context === 'new' && } - {context === 'invite' && } - - )} - - ); -} - -export default UsersPage; diff --git a/apps/meteor/client/views/admin/users/UsersPage.tsx b/apps/meteor/client/views/admin/users/UsersPage.tsx new file mode 100644 index 000000000000..d71b9849dc68 --- /dev/null +++ b/apps/meteor/client/views/admin/users/UsersPage.tsx @@ -0,0 +1,90 @@ +import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage'; +import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useEffect, ReactElement, useRef } from 'react'; + +import UserPageHeaderContentWithSeatsCap from '../../../../ee/client/views/admin/users/UserPageHeaderContentWithSeatsCap'; +import { useSeatsCap } from '../../../../ee/client/views/admin/users/useSeatsCap'; +import Page from '../../../components/Page'; +import VerticalBar from '../../../components/VerticalBar'; +import AddUser from './AddUser'; +import AdminUserInfoWithData from './AdminUserInfoWithData'; +import EditUserWithData from './EditUserWithData'; +import InviteUsers from './InviteUsers'; +import UsersTable from './UsersTable'; + +const UsersPage = (): ReactElement => { + const t = useTranslation(); + const context = useRouteParameter('context'); + const id = useRouteParameter('id'); + const seatsCap = useSeatsCap(); + const reload = useRef(() => null); + const usersRoute = useRoute('admin-users'); + + useEffect(() => { + if (!context || !seatsCap) { + return; + } + + if (seatsCap.activeUsers >= seatsCap.maxActiveUsers && !['edit', 'info'].includes(context)) { + usersRoute.push({}); + } + }, [context, seatsCap, usersRoute]); + + const handleCloseVerticalBar = (): void => { + usersRoute.push({}); + }; + + const handleNewUser = (): void => { + usersRoute.push({ context: 'new' }); + }; + + const handleInviteUser = (): void => { + usersRoute.push({ context: 'invite' }); + }; + + const handleReload = (): void => { + seatsCap?.reload(); + reload.current(); + }; + + return ( + + + + {seatsCap && seatsCap.maxActiveUsers < Number.POSITIVE_INFINITY ? ( + + ) : ( + + + + + )} + + + + + + {context && ( + + + {context === 'info' && t('User_Info')} + {context === 'edit' && t('Edit_User')} + {context === 'new' && t('Add_User')} + {context === 'invite' && t('Invite_Users')} + + + {context === 'info' && id && } + {context === 'edit' && id && } + {context === 'new' && } + {context === 'invite' && } + + )} + + ); +}; + +export default UsersPage; diff --git a/apps/meteor/client/views/admin/users/UsersRoute.js b/apps/meteor/client/views/admin/users/UsersRoute.tsx similarity index 80% rename from apps/meteor/client/views/admin/users/UsersRoute.js rename to apps/meteor/client/views/admin/users/UsersRoute.tsx index 827cdf1b86f1..ace289e93dba 100644 --- a/apps/meteor/client/views/admin/users/UsersRoute.js +++ b/apps/meteor/client/views/admin/users/UsersRoute.tsx @@ -1,10 +1,10 @@ import { usePermission } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import UsersPage from './UsersPage'; -function UsersRoute() { +const UsersRoute = (): ReactElement => { const canViewUserAdministration = usePermission('view-user-administration'); if (!canViewUserAdministration) { @@ -12,6 +12,6 @@ function UsersRoute() { } return ; -} +}; export default UsersRoute; diff --git a/apps/meteor/client/views/admin/users/UsersTable.js b/apps/meteor/client/views/admin/users/UsersTable.js deleted file mode 100644 index 08b59da047d9..000000000000 --- a/apps/meteor/client/views/admin/users/UsersTable.js +++ /dev/null @@ -1,126 +0,0 @@ -import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback } from 'react'; - -import FilterByText from '../../../components/FilterByText'; -import GenericTable from '../../../components/GenericTable'; -import UserRow from './UserRow'; - -function UsersTable({ params, onChangeParams, sort, onChangeSort, ...props }) { - const t = useTranslation(); - - const usersRoute = useRoute('admin-users'); - - const onClick = useCallback( - (username) => () => - usersRoute.push({ - context: 'info', - id: username, - }), - [usersRoute], - ); - - const onHeaderClick = useCallback( - (id) => { - const preparedSort = []; - - const [[sortBy, sortDirection]] = sort; - - if (sortBy === id) { - preparedSort.push([id, sortDirection === 'asc' ? 'desc' : 'asc']); - } else { - preparedSort.push([id, 'asc']); - } - - // - // Special cases - - // If the sortable field is `name`, we should also add `usernames` - if (id === 'name') { - preparedSort.push(['usernames', sortDirection]); - } - - // If the sortable field is `name`, we should also add `usernames` - if (id === 'status') { - preparedSort.push(['active', sortDirection === 'asc' ? 'desc' : 'asc']); - } - - onChangeSort(preparedSort); - }, - [onChangeSort, sort], - ); - - const mediaQuery = useMediaQuery('(min-width: 1024px)'); - - return ( - - - {t('Name')} - - {mediaQuery && ( - - {t('Username')} - - )} - - {t('Email')} - - {mediaQuery && ( - - {t('Roles')} - - )} - - {t('Status')} - - - } - results={props.users} - total={props.total} - setParams={onChangeParams} - params={params} - renderFilter={({ onChange, ...props }) => } - > - {(props) => } - - ); -} - -export default UsersTable; diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx new file mode 100644 index 000000000000..1f8ed0cd22a6 --- /dev/null +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -0,0 +1,165 @@ +import { IUserList } from '@rocket.chat/core-typings'; +import { States, StatesIcon, StatesTitle, Pagination } from '@rocket.chat/fuselage'; +import { useMediaQuery, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, useMemo, MutableRefObject, useState, useEffect } from 'react'; + +import FilterByText from '../../../../components/FilterByText'; +import { + GenericTable, + GenericTableHeader, + GenericTableHeaderCell, + GenericTableBody, + GenericTableLoadingTable, +} from '../../../../components/GenericTable'; +import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; +import { useSort } from '../../../../components/GenericTable/hooks/useSort'; +import { useEndpointData } from '../../../../hooks/useEndpointData'; +import { AsyncStatePhase } from '../../../../lib/asyncState'; +import UsersTableRow from './UsersTableRow'; + +const UsersTable = ({ reload }: { reload: MutableRefObject<() => void> }): ReactElement | null => { + const t = useTranslation(); + const usersRoute = useRoute('admin-users'); + const mediaQuery = useMediaQuery('(min-width: 1024px)'); + const [text, setText] = useState(''); + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + const { sortBy, sortDirection, setSort } = useSort<'name' | 'username' | 'emails.address' | 'status'>('name'); + + const query = useDebouncedValue( + useMemo( + () => ({ + fields: JSON.stringify({ + name: 1, + username: 1, + emails: 1, + roles: 1, + status: 1, + avatarETag: 1, + active: 1, + }), + query: JSON.stringify({ + $or: [ + { 'emails.address': { $regex: text || '', $options: 'i' } }, + { username: { $regex: text || '', $options: 'i' } }, + { name: { $regex: text || '', $options: 'i' } }, + ], + }), + sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, + count: itemsPerPage, + offset: current, + }), + [text, itemsPerPage, current, sortBy, sortDirection], + ), + 500, + ); + + const { value, phase, reload: reloadList } = useEndpointData('users.list', query); + + useEffect(() => { + reload.current = reloadList; + }, [reload, reloadList]); + + const handleClick = useMutableCallback((id): void => + usersRoute.push({ + context: 'info', + id, + }), + ); + + console.log(value?.users); + + if (phase === AsyncStatePhase.LOADING || phase === AsyncStatePhase.REJECTED) { + return null; + } + + const users = value?.users.map((user) => ({ + ...user, + lastLogin: new Date(user.lastLogin), + })); + + return ( + <> + setText(text)} /> + {value?.users.length === 0 && ( + + + {t('No_results_found')} + + )} + {value?.users && value.users.length > 0 && ( + <> + + + + {t('Name')} + + {mediaQuery && ( + + {t('Username')} + + )} + + {t('Email')} + + {mediaQuery && ( + + {t('Roles')} + + )} + + {t('Status')} + + + + {phase === AsyncStatePhase.LOADING && } + {users?.map((user) => ( + + ))} + + + {phase === AsyncStatePhase.RESOLVED && ( + + )} + + )} + + ); +}; + +export default UsersTable; diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx new file mode 100644 index 000000000000..894d71574962 --- /dev/null +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx @@ -0,0 +1,63 @@ +import { IUserList } from '@rocket.chat/core-typings'; +import { Box, TableRow, TableCell } from '@rocket.chat/fuselage'; +import { capitalize } from '@rocket.chat/string-helpers'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement } from 'react'; + +import { Roles } from '../../../../../app/models/client'; +import UserAvatar from '../../../../components/avatar/UserAvatar'; + +type UsersTableRowProps = { + user: IUserList; + onClick: (id: IUserList['_id']) => void; + mediaQuery: boolean; +}; + +const UsersTableRow = ({ user, onClick, mediaQuery }: UsersTableRowProps): ReactElement => { + const t = useTranslation(); + const { _id, emails, username, name, roles, status, active, avatarETag } = user; + const statusText = active ? t(capitalize(status)) : t('Disabled'); + + const roleNames = (roles || []) + .map((roleId) => Roles.findOne(roleId, { fields: { name: 1 } })?.name) + .filter((roleName) => !!roleName) + .join(', '); + + return ( + onClick(_id)} onClick={(): void => onClick(_id)} tabIndex={0} role='link' action qa-user-id={_id}> + + + {username && } + + + + {name || username} + + {!mediaQuery && name && ( + + {' '} + {`@${username}`}{' '} + + )} + + + + + {mediaQuery && ( + + + {username} + {' '} + + + )} + {emails?.length && emails[0].address} + {mediaQuery && {roleNames}} + + {statusText} + + + ); +}; + +export default UsersTableRow; diff --git a/apps/meteor/client/views/admin/users/UsersTable/index.ts b/apps/meteor/client/views/admin/users/UsersTable/index.ts new file mode 100644 index 000000000000..4b01807f4f6c --- /dev/null +++ b/apps/meteor/client/views/admin/users/UsersTable/index.ts @@ -0,0 +1 @@ +export { default } from './UsersTable'; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 8de95b3caae6..6a07a35e03de 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -270,6 +270,7 @@ "Add_Reaction": "Add Reaction", "Add_Role": "Add Role", "Add_Sender_To_ReplyTo": "Add Sender to Reply-To", + "Add_URL": "Add URL", "Add_user": "Add user", "Add_User": "Add User", "Add_users": "Add users", @@ -3780,6 +3781,7 @@ "Unsafe_Url": "Unsafe URL", "Rocket_Chat_Alert": "Rocket.Chat Alert", "Role": "Role", + "Roles": "Roles", "Role_Editing": "Role Editing", "Role_Mapping": "Role mapping", "Role_removed": "Role removed", diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 9d0b4547d639..ebf2285200f2 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -110,6 +110,7 @@ export interface IUser extends IRocketChatRecord { active: boolean; username?: string; nickname?: string; + bio?: string; name?: string; services?: IUserServices; emails?: IUserEmail[]; @@ -168,3 +169,5 @@ export type IUserInRole = Pick< IUser, '_id' | 'name' | 'username' | 'emails' | 'avatarETag' | 'createdAt' | 'roles' | 'type' | 'active' | '_updatedAt' >; + +export type IUserList = Pick; diff --git a/packages/rest-typings/src/v1/customUserStatus.ts b/packages/rest-typings/src/v1/customUserStatus.ts index 9f9f8350180f..5269a1123ee3 100644 --- a/packages/rest-typings/src/v1/customUserStatus.ts +++ b/packages/rest-typings/src/v1/customUserStatus.ts @@ -6,7 +6,7 @@ import type { PaginatedResult } from '../helpers/PaginatedResult'; export type CustomUserStatusEndpoints = { 'custom-user-status.list': { GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ - statuses: IUserStatus[]; + users: IUserStatus[]; }>; }; }; diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 0f9951f4dfec..c6246ae6b1fb 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -1,4 +1,7 @@ -import type { ITeam, IUser } from '@rocket.chat/core-typings'; +import type { ITeam, IUser, IUserList } from '@rocket.chat/core-typings'; + +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; +import type { PaginatedResult } from '../helpers/PaginatedResult'; export type UsersEndpoints = { 'users.info': { @@ -14,13 +17,21 @@ export type UsersEndpoints = { items: Required>[]; }; }; + 'users.list': { + GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ + users: IUserList[]; + }>; + }; 'users.listTeams': { GET: (params: { userId: IUser['_id'] }) => { teams: Array }; }; + 'users.resetAvatar': { + POST: (params: { userId?: IUser['_id']; username?: IUser['username'] }) => void; + }; 'users.setAvatar': { POST: (params: { userId?: IUser['_id']; username?: IUser['username']; avatarUrl?: string }) => void; }; - 'users.resetAvatar': { + 'users.update': { POST: (params: { userId?: IUser['_id']; username?: IUser['username'] }) => void; }; }; From a1562a2374bc193744516d95a575467992986196 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Thu, 2 Jun 2022 22:14:03 -0300 Subject: [PATCH 2/6] fix: types --- .../views/admin/users/UsersTable/UsersTable.tsx | 12 ++---------- .../views/admin/users/UsersTable/UsersTableRow.tsx | 4 ++-- packages/core-typings/src/IUser.ts | 2 +- packages/rest-typings/src/v1/customUserStatus.ts | 2 +- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index 1f8ed0cd22a6..699a05e23f16 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -1,4 +1,3 @@ -import { IUserList } from '@rocket.chat/core-typings'; import { States, StatesIcon, StatesTitle, Pagination } from '@rocket.chat/fuselage'; import { useMediaQuery, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; @@ -67,17 +66,10 @@ const UsersTable = ({ reload }: { reload: MutableRefObject<() => void> }): React }), ); - console.log(value?.users); - - if (phase === AsyncStatePhase.LOADING || phase === AsyncStatePhase.REJECTED) { + if (phase === AsyncStatePhase.REJECTED) { return null; } - const users = value?.users.map((user) => ({ - ...user, - lastLogin: new Date(user.lastLogin), - })); - return ( <> setText(text)} /> @@ -141,7 +133,7 @@ const UsersTable = ({ reload }: { reload: MutableRefObject<() => void> }): React {phase === AsyncStatePhase.LOADING && } - {users?.map((user) => ( + {value?.users.map((user) => ( ))} diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx index 894d71574962..78d93d68d2c0 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx @@ -1,7 +1,7 @@ import { IUserList } from '@rocket.chat/core-typings'; import { Box, TableRow, TableCell } from '@rocket.chat/fuselage'; import { capitalize } from '@rocket.chat/string-helpers'; -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation, TranslationKey } from '@rocket.chat/ui-contexts'; import React, { ReactElement } from 'react'; import { Roles } from '../../../../../app/models/client'; @@ -16,7 +16,7 @@ type UsersTableRowProps = { const UsersTableRow = ({ user, onClick, mediaQuery }: UsersTableRowProps): ReactElement => { const t = useTranslation(); const { _id, emails, username, name, roles, status, active, avatarETag } = user; - const statusText = active ? t(capitalize(status)) : t('Disabled'); + const statusText = active ? t(capitalize(status as string) as TranslationKey) : t('Disabled'); const roleNames = (roles || []) .map((roleId) => Roles.findOne(roleId, { fields: { name: 1 } })?.name) diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index ebf2285200f2..31d826400af6 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -170,4 +170,4 @@ export type IUserInRole = Pick< '_id' | 'name' | 'username' | 'emails' | 'avatarETag' | 'createdAt' | 'roles' | 'type' | 'active' | '_updatedAt' >; -export type IUserList = Pick; +export type IUserList = Pick; diff --git a/packages/rest-typings/src/v1/customUserStatus.ts b/packages/rest-typings/src/v1/customUserStatus.ts index 2d80d40cbdd5..943aebb9fdca 100644 --- a/packages/rest-typings/src/v1/customUserStatus.ts +++ b/packages/rest-typings/src/v1/customUserStatus.ts @@ -6,7 +6,7 @@ import type { PaginatedResult } from '../helpers/PaginatedResult'; export type CustomUserStatusEndpoints = { 'custom-user-status.list': { GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ - users: IUserStatus[]; + statuses: IUserStatus[]; }>; }; 'custom-user-status.create': { From 8cce5a95941f09722273decd3dba921dadfc0ff9 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 10 Jun 2022 17:13:44 -0300 Subject: [PATCH 3/6] Rearrange some types --- apps/meteor/client/views/admin/users/EditUserWithData.tsx | 7 ++++++- apps/meteor/client/views/admin/users/InviteUsers.tsx | 6 ++++-- .../client/views/admin/users/UsersTable/UsersTable.tsx | 6 +++++- packages/core-typings/src/IUser.ts | 2 -- packages/rest-typings/src/v1/users.ts | 4 ++-- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/meteor/client/views/admin/users/EditUserWithData.tsx b/apps/meteor/client/views/admin/users/EditUserWithData.tsx index 27895573b2ea..84b63b12d3c0 100644 --- a/apps/meteor/client/views/admin/users/EditUserWithData.tsx +++ b/apps/meteor/client/views/admin/users/EditUserWithData.tsx @@ -8,7 +8,12 @@ import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; import EditUser from './EditUser'; -const EditUserWithData = ({ uid, onReload, ...props }: { uid: IUser['_id']; onReload: () => void }): ReactElement => { +type EditUserWithDataProps = { + uid: IUser['_id']; + onReload: () => void; +}; + +const EditUserWithData = ({ uid, onReload, ...props }: EditUserWithDataProps): ReactElement => { const t = useTranslation(); const { value: roleData, phase: roleState, error: roleError } = useEndpointData('/v1/roles.list'); const { diff --git a/apps/meteor/client/views/admin/users/InviteUsers.tsx b/apps/meteor/client/views/admin/users/InviteUsers.tsx index 6c05439b851e..082c90115f32 100644 --- a/apps/meteor/client/views/admin/users/InviteUsers.tsx +++ b/apps/meteor/client/views/admin/users/InviteUsers.tsx @@ -1,11 +1,13 @@ import { Box, Button, Icon, TextAreaInput } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useState, ReactElement, ChangeEvent } from 'react'; +import React, { useCallback, useState, ReactElement, ChangeEvent, ComponentProps } from 'react'; import { validateEmail } from '../../../../lib/emailValidator'; import VerticalBar from '../../../components/VerticalBar'; -const InviteUsers = ({ ...props }): ReactElement => { +type InviteUsersProps = ComponentProps; + +const InviteUsers = (props: InviteUsersProps): ReactElement => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const [text, setText] = useState(''); diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx index 6af9c7bcc409..eb16bb1ad851 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx @@ -17,7 +17,11 @@ import { useEndpointData } from '../../../../hooks/useEndpointData'; import { AsyncStatePhase } from '../../../../lib/asyncState'; import UsersTableRow from './UsersTableRow'; -const UsersTable = ({ reload }: { reload: MutableRefObject<() => void> }): ReactElement | null => { +type UsersTableProps = { + reload: MutableRefObject<() => void>; +}; + +const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => { const t = useTranslation(); const usersRoute = useRoute('admin-users'); const mediaQuery = useMediaQuery('(min-width: 1024px)'); diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index cad6a2f60d10..0dde24a2d7ba 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -169,5 +169,3 @@ export type IUserInRole = Pick< IUser, '_id' | 'name' | 'username' | 'emails' | 'avatarETag' | 'createdAt' | 'roles' | 'type' | 'active' | '_updatedAt' >; - -export type IUserList = Pick; diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 96976a279c46..9d9413034dde 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -1,4 +1,4 @@ -import type { ITeam, IUser, IUserList } from '@rocket.chat/core-typings'; +import type { ITeam, IUser } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; @@ -142,7 +142,7 @@ export type UsersEndpoints = { }; '/v1/users.list': { GET: (params: PaginatedRequest<{ query: string }>) => PaginatedResult<{ - users: IUserList[]; + users: Pick[]; }>; }; '/v1/users.listTeams': { From 89c8c0918a8b5460917bc7c705a92c14c58e143f Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 10 Jun 2022 17:27:52 -0300 Subject: [PATCH 4/6] Fix missing type --- .../views/admin/users/UsersTable/UsersTableRow.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx index 78d93d68d2c0..1ddfede85e6f 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx @@ -1,4 +1,4 @@ -import { IUserList } from '@rocket.chat/core-typings'; +import { IRole, IUser } from '@rocket.chat/core-typings'; import { Box, TableRow, TableCell } from '@rocket.chat/fuselage'; import { capitalize } from '@rocket.chat/string-helpers'; import { useTranslation, TranslationKey } from '@rocket.chat/ui-contexts'; @@ -8,8 +8,8 @@ import { Roles } from '../../../../../app/models/client'; import UserAvatar from '../../../../components/avatar/UserAvatar'; type UsersTableRowProps = { - user: IUserList; - onClick: (id: IUserList['_id']) => void; + user: Pick; + onClick: (id: Pick['_id']) => void; mediaQuery: boolean; }; @@ -19,8 +19,8 @@ const UsersTableRow = ({ user, onClick, mediaQuery }: UsersTableRowProps): React const statusText = active ? t(capitalize(status as string) as TranslationKey) : t('Disabled'); const roleNames = (roles || []) - .map((roleId) => Roles.findOne(roleId, { fields: { name: 1 } })?.name) - .filter((roleName) => !!roleName) + .map((roleId) => (Roles.findOne(roleId, { fields: { name: 1 } }) as IRole | undefined)?.name) + .filter((roleName): roleName is string => !!roleName) .join(', '); return ( From 7586b38842c9a4f7be9639e8166b7ae9e69bcc25 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 10 Jun 2022 19:59:20 -0300 Subject: [PATCH 5/6] Simplify type expression --- .../client/views/admin/users/UsersTable/UsersTableRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx index 1ddfede85e6f..a7157802391c 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx @@ -9,7 +9,7 @@ import UserAvatar from '../../../../components/avatar/UserAvatar'; type UsersTableRowProps = { user: Pick; - onClick: (id: Pick['_id']) => void; + onClick: (id: IUser['_id']) => void; mediaQuery: boolean; }; From 1c9f7d0be0ca1d59d2012124b267c861bd63036a Mon Sep 17 00:00:00 2001 From: dougfabris Date: Mon, 13 Jun 2022 15:17:58 -0300 Subject: [PATCH 6/6] fix: e2e test --- apps/meteor/tests/e2e/11-admin.spec.ts | 2 +- apps/meteor/tests/e2e/utils/pageobjects/Administration.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/11-admin.spec.ts b/apps/meteor/tests/e2e/11-admin.spec.ts index d4f1f1dc16f2..450aa0e2408b 100644 --- a/apps/meteor/tests/e2e/11-admin.spec.ts +++ b/apps/meteor/tests/e2e/11-admin.spec.ts @@ -147,7 +147,7 @@ test.describe('[Administration]', () => { }); test('expect dont user when write wrong name', async () => { await admin.usersFilter().type('any_user_wrong'); - await expect(admin.notFoundChannelOrUser()).toBeVisible(); + await expect(admin.notFoundChannels()).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts b/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts index 759bc14636fa..4fcf3d4c9fa7 100644 --- a/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts +++ b/apps/meteor/tests/e2e/utils/pageobjects/Administration.ts @@ -99,6 +99,10 @@ export default class Administration extends BasePage { return this.getPage().locator("//div[text()='No data found']"); } + public notFoundChannels(): Locator { + return this.getPage().locator("//div[text()='No results found']"); + } + public usersRocketCat(): Locator { return this.getPage().locator('td=Rocket.Cat'); }