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

[NEW]Admin Users Actions #17469

Merged
merged 12 commits into from
Apr 29, 2020
41 changes: 12 additions & 29 deletions client/admin/users/UserInfo.js
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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 <Box w='full' pb='x24'>
Expand All @@ -48,24 +51,13 @@ export function UserInfoWithData({ userId, ...props }) {
return <Box mbs='x16'>{t('User_not_found')}</Box>;
}

return <UserInfo data={data.user} {...props} />;
return <UserInfo data={data.user} onChange={onChange} {...props} />;
}


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) : '';
Expand All @@ -83,20 +75,11 @@ export function UserInfo({ data, ...props }) {
</Margins>
</Box>

<Box display='flex' flexDirection='row'>
<ButtonGroup flexGrow={1} justifyContent='center'>
<Button onClick={directMessageClick}><Icon name='chat' size='x16' mie='x8'/>{t('Direct_Message')}</Button>
<Button onClick={editUserClick}><Icon name='edit' size='x16' mie='x8'/>{t('Edit')}</Button>
</ButtonGroup>
</Box>

<UserInfoActions isActive={data.active} isAdmin={data.roles.includes('admin')} _id={data._id} username={data.username} onChange={onChange}/>
{console.log(MarkdownText)}
<Box display='flex' flexDirection='column' w='full' backgroundColor='neutral-200' p='x16'>
<Margins blockEnd='x4'>

{data.bio && data.bio.trim().length > 0 && <Box fontScale='s1' marginBlockEnd='x8'>
<MarkdownText>{data.bio}</MarkdownText>
</Box>}

{data.bio && data.bio.trim().length > 0 && <MarkdownText fontScale='s1'>{data.bio}</MarkdownText>}
{data.roles && <>
<Box fontScale='micro' color='hint' mbs='none'>{t('Roles')}</Box>
<Box display='flex' flexDirection='row' flexWrap='wrap'>
Expand Down
157 changes: 157 additions & 0 deletions client/admin/users/UserInfoActions.js
Original file line number Diff line number Diff line change
@@ -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 <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t(`Delete_User_Warning_${ erasureType }`)}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};

const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('User_has_been_deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};


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(<SuccessModal onClose={() => { setModal(); onChange(); }}/>);
} else {
setModal();
}
}, [deleteUser]);
const confirmDeleteUser = useCallback(() => {
setModal(<DeleteWarningModal onDelete={willDeleteUser} onCancel={() => 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: <><Icon name='chat' size='x16' mie='x8'/>{t('Direct_Message')}</>,
action: directMessageClick,
} },
...canEditOtherUserInfo && { editUser: {
label: <><Icon name='edit' size='x16' mie='x8'/>{t('Edit')}</>,
action: editUserClick,
} },
...canAssignAdminRole && { makeAdmin: {
label: <><Icon mie='x4' name='key' size='x16'/>{ isAdmin ? t('Remove_Admin') : t('Make_Admin')}</>,
action: changeAdminStatus,
} },
...canDeleteUser && { delete: {
label: <Box color='danger'><Icon mie='x4' name='trash' size='x16'/>{t('Delete')}</Box>,
action: confirmDeleteUser,
} },
...canEditOtherUserActiveStatus && { changeActiveStatus: {
label: <><Icon mie='x4' name='user' size='x16'/>{ isActive ? t('Activate') : t('Deactivate')}</>,
action: async () => {
const result = await changeActiveStatus();
result.success ? onchange() : undefined;
},
} },
}), [canAssignAdminRole, canDeleteUser, canEditOtherUserActiveStatus, canEditOtherUserInfo, canDirectMessage]);

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 <>
<Box display='flex' flexDirection='row' {...props}>
<ButtonGroup flexGrow={1} justifyContent='center'>
{ actions && actions.map((action, index) => (<Button key={index} onClick={action.action}>{action.label}</Button>))}
{ moreActions && <Menu options={moreActions} placement='bottom left'/> }
</ButtonGroup>
</Box>
{ modal }
</>;
};
2 changes: 1 addition & 1 deletion client/hooks/useEndpointAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions client/polyfills/index.js
Original file line number Diff line number Diff line change
@@ -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 }), {});
};
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down