diff --git a/client/admin/users/UserInfo.js b/client/admin/users/UserInfo.js index 65390cb252c2..80e58fc240e0 100644 --- a/client/admin/users/UserInfo.js +++ b/client/admin/users/UserInfo.js @@ -1,14 +1,14 @@ import React, { useMemo, useState, useEffect } from 'react'; -import { Box, Avatar, Button, ButtonGroup, Icon, Margins, Headline, Skeleton, Chip, Tag } from '@rocket.chat/fuselage'; +import { Box, Avatar, Margins, Headline, Skeleton, Chip, Tag } from '@rocket.chat/fuselage'; import moment from 'moment'; import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; -import MarkdownText from '../../components/basic/MarkdownText'; import { useTranslation } from '../../contexts/TranslationContext'; import { roomTypes } from '../../../app/utils/client'; import { DateFormat } from '../../../app/lib'; -import { useRoute } from '../../contexts/RouterContext'; +import { UserInfoActions } from './UserInfoActions'; import Page from '../../components/basic/Page'; +import MarkdownText from '../../components/basic/MarkdownText'; const useTimezoneClock = (utcOffset = 0, updateInterval) => { const [time, setTime] = useState(); @@ -30,8 +30,11 @@ const UTCClock = ({ utcOffset, ...props }) => { export function UserInfoWithData({ userId, ...props }) { const t = useTranslation(); + const [cache, setCache] = useState(); + + const onChange = () => setCache(new Date()); - const { data, state, error } = useEndpointDataExperimental('users.info', useMemo(() => ({ userId }), [userId])); + const { data, state, error } = useEndpointDataExperimental('users.info', useMemo(() => ({ userId }), [userId, cache])); if (state === ENDPOINT_STATES.LOADING) { return @@ -48,24 +51,13 @@ export function UserInfoWithData({ userId, ...props }) { return {t('User_not_found')}; } - return ; + return ; } -export function UserInfo({ data, ...props }) { +export function UserInfo({ data, onChange, ...props }) { const t = useTranslation(); - const directRoute = useRoute('direct'); - const userRoute = useRoute('admin-users'); - - const directMessageClick = () => directRoute.push({ - rid: data.username, - }); - const editUserClick = () => userRoute.push({ - context: 'edit', - id: data._id, - }); - const createdAt = DateFormat.formatDateAndTime(data.createdAt); const lastLogin = data.lastLogin ? DateFormat.formatDateAndTime(data.lastLogin) : ''; @@ -83,21 +75,12 @@ export function UserInfo({ data, ...props }) { - - - - - - - + + {console.log(MarkdownText)} - - {data.bio && data.bio.trim().length > 0 && - {data.bio} - } - - {data.roles && <> + {data.bio && data.bio.trim().length > 0 && {data.bio}} + {!!data.roles.length && <> {t('Roles')} diff --git a/client/admin/users/UserInfoActions.js b/client/admin/users/UserInfoActions.js new file mode 100644 index 000000000000..7dd3551035f5 --- /dev/null +++ b/client/admin/users/UserInfoActions.js @@ -0,0 +1,157 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { Box, Button, ButtonGroup, Icon, Menu } from '@rocket.chat/fuselage'; + +import { Modal } from '../../components/basic/Modal'; +import { useTranslation } from '../../contexts/TranslationContext'; +import { useRoute } from '../../contexts/RouterContext'; +import { usePermission } from '../../contexts/AuthorizationContext'; +import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; +import { useMethod } from '../../contexts/ServerContext'; +import { useSetting } from '../../contexts/SettingsContext'; +import { useEndpointAction } from '../../hooks/useEndpointAction'; + + +const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { + const t = useTranslation(); + const erasureType = useSetting('Message_ErasureType'); + + return + + + {t('Are_you_sure')} + + + + {t(`Delete_User_Warning_${ erasureType }`)} + + + + + + + + ; +}; + +const SuccessModal = ({ onClose, ...props }) => { + const t = useTranslation(); + return + + + {t('Deleted')} + + + + {t('User_has_been_deleted')} + + + + + + + ; +}; + + +export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, ...props }) => { + const t = useTranslation(); + const [modal, setModal] = useState(); + + const directRoute = useRoute('direct'); + const userRoute = useRoute('admin-users'); + const dispatchToastMessage = useToastMessageDispatch(); + + const canDirectMessage = usePermission('create-d'); + const canEditOtherUserInfo = usePermission('edit-other-user-info'); + const canAssignAdminRole = usePermission('assign-admin-role'); + const canEditOtherUserActiveStatus = usePermission('edit-other-user-active-status'); + const canDeleteUser = usePermission('delete-user'); + + const deleteUserQuery = useMemo(() => ({ userId: _id }), [_id]); + const deleteUser = useEndpointAction('POST', 'users.delete', deleteUserQuery); + + const willDeleteUser = useCallback(async () => { + const result = await deleteUser(); + if (result.success) { + setModal( { setModal(); onChange(); }}/>); + } else { + setModal(); + } + }, [deleteUser]); + const confirmDeleteUser = useCallback(() => { + setModal( setModal()}/>); + }, [deleteUser]); + + const setAdminStatus = useMethod('setAdminStatus'); + const changeAdminStatus = useCallback(() => { + try { + setAdminStatus(_id, !isAdmin); + const message = isAdmin ? 'User_is_no_longer_an_admin' : 'User_is_now_an_admin'; + dispatchToastMessage({ type: 'success', message: t(message) }); + onChange(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, [isAdmin]); + + const activeStatusQuery = useMemo(() => ({ + userId: _id, + activeStatus: !isActive, + }), [_id, isActive]); + const changeActiveStatusMessage = isActive ? 'User_has_been_deactivated' : 'User_has_been_activated'; + const changeActiveStatus = useEndpointAction('POST', 'users.setActiveStatus', activeStatusQuery, t(changeActiveStatusMessage)); + + const directMessageClick = () => directRoute.push({ + rid: username, + }); + + const editUserClick = () => userRoute.push({ + context: 'edit', + id: _id, + }); + + const menuOptions = useMemo(() => ({ + ...canDirectMessage && { directMessage: { + label: <>{t('Direct_Message')}, + action: directMessageClick, + } }, + ...canEditOtherUserInfo && { editUser: { + label: <>{t('Edit')}, + action: editUserClick, + } }, + ...canAssignAdminRole && { makeAdmin: { + label: <>{ isAdmin ? t('Remove_Admin') : t('Make_Admin')}, + action: changeAdminStatus, + } }, + ...canDeleteUser && { delete: { + label: {t('Delete')}, + action: confirmDeleteUser, + } }, + ...canEditOtherUserActiveStatus && { changeActiveStatus: { + label: <>{ isActive ? t('Deactivate') : t('Activate')}, + action: async () => { + const result = await changeActiveStatus(); + result.success ? onChange() : undefined; + }, + } }, + }), [canAssignAdminRole, canDeleteUser, canEditOtherUserActiveStatus, canEditOtherUserInfo, canDirectMessage, isActive, isAdmin]); + + const [actions, moreActions] = useMemo(() => { + const keys = Object.keys(menuOptions); + + const firstHalf = keys.slice(0, 2); + const secondHalf = keys.slice(2, keys.length); + + return [firstHalf.length && firstHalf.map((key) => menuOptions[key]), secondHalf.length && Object.fromEntries(secondHalf.map((key) => [key, menuOptions[key]]))]; + }, menuOptions); + + return <> + + + { actions && actions.map((action, index) => ())} + { moreActions && } + + + { modal } + ; +}; diff --git a/client/hooks/useEndpointAction.js b/client/hooks/useEndpointAction.js index 0f37ed89b340..60bbffae830d 100644 --- a/client/hooks/useEndpointAction.js +++ b/client/hooks/useEndpointAction.js @@ -17,7 +17,7 @@ export const useEndpointAction = (httpMethod, endpoint, params = {}, successMess throw new Error(data.status); } - dispatchToastMessage({ type: 'success', message: successMessage }); + successMessage && dispatchToastMessage({ type: 'success', message: successMessage }); return data; } catch (error) { diff --git a/client/polyfills/index.js b/client/polyfills/index.js index a9086d631677..31cada9ea9ff 100644 --- a/client/polyfills/index.js +++ b/client/polyfills/index.js @@ -1,3 +1,7 @@ import '@rocket.chat/fuselage-polyfills'; import 'url-polyfill'; import './customEventPolyfill'; + +Object.fromEntries = Object.fromEntries || function fromEntries(iterable) { + return [...iterable].reduce((obj, { 0: key, 1: val }) => Object.assign(obj, { [key]: val }), {}); +}; diff --git a/package-lock.json b/package-lock.json index 0b8f4e29284a..b2312f4a2519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2799,9 +2799,9 @@ } }, "@rocket.chat/fuselage": { - "version": "0.6.3-dev.29", - "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.3-dev.29.tgz", - "integrity": "sha512-XJiw1fpf7bZvvHeu8Ippe4mYuJScmNd52XeMXdHxX/hqXH9RrhTmimr5r86HatfU7dPqcgrVr1aHcZNiyMBz/g==", + "version": "0.6.3-dev.31", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.3-dev.31.tgz", + "integrity": "sha512-ZcfuHluR9NzpvnORV3D/0iIRKLEi8HQfsEypsh4cseJa1sG6F3VoJJ8uO4033EwYYcS1X4SxKYaQURk75/UtOw==", "requires": { "@rocket.chat/css-in-js": "^0.8.0", "@rocket.chat/fuselage-tokens": "^0.8.0", diff --git a/package.json b/package.json index ef126486a410..31b0578851bc 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "@nivo/line": "^0.61.1", "@nivo/pie": "^0.61.1", "@rocket.chat/apps-engine": "^1.14.0-beta.3119", - "@rocket.chat/fuselage": "^0.6.3-dev.29", + "@rocket.chat/fuselage": "^0.6.3-dev.31", "@rocket.chat/fuselage-hooks": "^0.6.3-dev.23", "@rocket.chat/fuselage-polyfills": "^0.6.3-dev.23", "@rocket.chat/fuselage-ui-kit": "^0.6.3-dev.29",