Skip to content
Snippets Groups Projects
Unverified Commit 977cd70d authored by Douglas Fabris's avatar Douglas Fabris Committed by GitHub
Browse files

[IMPROVE] Rewrite Admin Permissions to Typescript

parent 311c9f84
No related branches found
No related tags found
No related merge requests found
Showing
with 635 additions and 164 deletions
......@@ -150,6 +150,8 @@ API.v1.addRoute(
username: 1,
emails: 1,
avatarETag: 1,
createdAt: 1,
_updatedAt: 1,
};
if (!role) {
......
......@@ -7,6 +7,7 @@ type HeaderCellProps = {
active?: boolean;
direction?: 'asc' | 'desc';
sort?: string;
clickable?: boolean;
onClick?: (sort: string) => void;
} & ComponentProps<typeof Box>;
......
import { useMemo } from 'react';
import { useCurrent } from './useCurrent';
import { useItemsPerPage } from './useItemsPerPage';
import { useItemsPerPageLabel } from './useItemsPerPageLabel';
......@@ -12,19 +14,19 @@ export const usePagination = (): {
showingResultsLabel: ReturnType<typeof useShowingResultsLabel>;
} => {
const [itemsPerPage, setItemsPerPage] = useItemsPerPage();
const [current, setCurrent] = useCurrent();
const itemsPerPageLabel = useItemsPerPageLabel();
const showingResultsLabel = useShowingResultsLabel();
return {
itemsPerPage,
setItemsPerPage,
current,
setCurrent,
itemsPerPageLabel,
showingResultsLabel,
};
return useMemo(
() => ({
itemsPerPage,
setItemsPerPage,
current,
setCurrent,
itemsPerPageLabel,
showingResultsLabel,
}),
[itemsPerPage, setItemsPerPage, current, setCurrent, itemsPerPageLabel, showingResultsLabel],
);
};
import { IRole } from '@rocket.chat/core-typings';
import { Box, ButtonGroup, Button, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React from 'react';
import React, { ReactElement } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import GenericModal from '../../../components/GenericModal';
import VerticalBar from '../../../components/VerticalBar';
......@@ -9,37 +11,47 @@ import { useRoute } from '../../../contexts/RouterContext';
import { useEndpoint } from '../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useForm } from '../../../hooks/useForm';
import RoleForm from './RoleForm';
const EditRolePage = ({ data }) => {
const EditRolePage = ({ role }: { role?: IRole }): ReactElement => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const setModal = useSetModal();
const usersInRoleRouter = useRoute('admin-permissions');
const router = useRoute('admin-permissions');
const { values, handlers, hasUnsavedChanges } = useForm({
roleId: data._id,
name: data.name,
description: data.description || '',
scope: data.scope || 'Users',
mandatory2fa: !!data.mandatory2fa,
});
const saveRole = useEndpoint('POST', 'roles.update');
const createRole = useEndpoint('POST', 'roles.create');
const updateRole = useEndpoint('POST', 'roles.update');
const deleteRole = useEndpoint('POST', 'roles.delete');
const methods = useForm({
defaultValues: {
roleId: role?._id,
name: role?.name,
description: role?.description,
scope: role?.scope || 'Users',
mandatory2fa: !!role?.mandatory2fa,
},
});
const handleManageUsers = useMutableCallback(() => {
usersInRoleRouter.push({
context: 'users-in-role',
_id: data._id,
});
if (role?._id) {
usersInRoleRouter.push({
context: 'users-in-role',
_id: role._id,
});
}
});
const handleSave = useMutableCallback(async () => {
const handleSave = useMutableCallback(async (data) => {
try {
await saveRole(values);
if (data.roleId) {
await updateRole(data);
dispatchToastMessage({ type: 'success', message: t('Saved') });
return router.push({});
}
await createRole(data);
dispatchToastMessage({ type: 'success', message: t('Saved') });
router.push({});
} catch (error) {
......@@ -48,11 +60,16 @@ const EditRolePage = ({ data }) => {
});
const handleDelete = useMutableCallback(async () => {
const deleteRoleAction = async () => {
if (!role?._id) {
return;
}
const deleteRoleAction = async (): Promise<void> => {
try {
await deleteRole({ roleId: data._id });
await deleteRole({ roleId: role._id });
dispatchToastMessage({ type: 'success', message: t('Role_removed') });
setModal();
router.push({});
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
......@@ -64,8 +81,8 @@ const EditRolePage = ({ data }) => {
<GenericModal
variant='danger'
onConfirm={deleteRoleAction}
onClose={() => setModal()}
onCancel={() => setModal()}
onClose={(): void => setModal()}
onCancel={(): void => setModal()}
confirmText={t('Delete')}
>
{t('Delete_Role_Warning')}
......@@ -78,21 +95,23 @@ const EditRolePage = ({ data }) => {
<VerticalBar.ScrollableContent>
<Box w='full' alignSelf='center' mb='neg-x8'>
<Margins block='x8'>
<RoleForm values={values} handlers={handlers} editing isProtected={data.protected} />
<FormProvider {...methods}>
<RoleForm editing={Boolean(role?._id)} isProtected={role?.protected} />
</FormProvider>
</Margins>
</Box>
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
<ButtonGroup vertical stretch>
<Button primary disabled={!hasUnsavedChanges} onClick={handleSave}>
<Button primary disabled={!methods.formState.isDirty} onClick={methods.handleSubmit(handleSave)}>
{t('Save')}
</Button>
{!data.protected && (
{!role?.protected && role?._id && (
<Button danger onClick={handleDelete}>
{t('Delete')}
</Button>
)}
<Button onClick={handleManageUsers}>{t('Users_in_role')}</Button>
{role?._id && <Button onClick={handleManageUsers}>{t('Users_in_role')}</Button>}
</ButtonGroup>
</VerticalBar.Footer>
</>
......
import { IRole } from '@rocket.chat/core-typings';
import { Callout } from '@rocket.chat/fuselage';
import React from 'react';
import React, { ReactElement } from 'react';
import { useRouteParameter } from '../../../contexts/RouterContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import EditRolePage from './EditRolePage';
import { useRole } from './useRole';
import { useRole } from './hooks/useRole';
const EditRolePageContainer = ({ _id }) => {
const EditRolePageWithData = ({ roleId }: { roleId?: IRole['_id'] }): ReactElement => {
const t = useTranslation();
const role = useRole(_id);
const role = useRole(roleId);
const context = useRouteParameter('context');
if (!role) {
if (!role && context === 'edit') {
return <Callout type='danger'>{t('error-invalid-role')}</Callout>;
}
return <EditRolePage key={_id} data={role} />;
return <EditRolePage key={roleId} role={role} />;
};
export default EditRolePageContainer;
export default EditRolePageWithData;
import { Box, ButtonGroup, Button, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState } from 'react';
import VerticalBar from '../../../components/VerticalBar';
import { useRoute } from '../../../contexts/RouterContext';
import { useEndpoint } from '../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useForm } from '../../../hooks/useForm';
import RoleForm from './RoleForm';
const NewRolePage = () => {
const t = useTranslation();
const router = useRoute('admin-permissions');
const dispatchToastMessage = useToastMessageDispatch();
const [errors, setErrors] = useState();
const { values, handlers } = useForm({
name: '',
description: '',
scope: 'Users',
mandatory2fa: false,
});
const saveRole = useEndpoint('POST', 'roles.create');
const handleSave = useMutableCallback(async () => {
if (values.name === '') {
return setErrors({ name: t('error-the-field-is-required', { field: t('Role') }) });
}
try {
await saveRole(values);
dispatchToastMessage({ type: 'success', message: t('Saved') });
router.push({});
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
return (
<>
<VerticalBar.ScrollableContent>
<Box w='full' alignSelf='center' mb='neg-x8'>
<Margins block='x8'>
<RoleForm values={values} handlers={handlers} errors={errors} />
</Margins>
</Box>
</VerticalBar.ScrollableContent>
<VerticalBar.Footer>
<ButtonGroup stretch w='full'>
<Button primary onClick={handleSave}>
{t('Save')}
</Button>
</ButtonGroup>
</VerticalBar.Footer>
</>
);
};
export default NewRolePage;
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React from 'react';
import React, { ReactElement } from 'react';
import VerticalBar from '../../../components/VerticalBar';
import { useRouteParameter, useRoute } from '../../../contexts/RouterContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import EditRolePage from './EditRolePageContainer';
import NewRolePage from './NewRolePage';
import EditRolePageWithData from './EditRolePageWithData';
const PermissionsContextBar = () => {
const PermissionsContextBar = (): ReactElement | null => {
const t = useTranslation();
const _id = useRouteParameter('_id');
const context = useRouteParameter('context');
const router = useRoute('admin-permissions');
const handleVerticalBarCloseButton = useMutableCallback(() => {
const handleCloseVerticalBar = useMutableCallback(() => {
router.push({});
});
......@@ -22,12 +20,10 @@ const PermissionsContextBar = () => {
(context && (
<VerticalBar>
<VerticalBar.Header>
{context === 'new' && t('New_role')}
{context === 'edit' && t('Role_Editing')}
<VerticalBar.Close onClick={handleVerticalBarCloseButton} />
{context === 'edit' ? t('Role_Editing') : t('New_role')}
<VerticalBar.Close onClick={handleCloseVerticalBar} />
</VerticalBar.Header>
{context === 'new' && <NewRolePage />}
{context === 'edit' && <EditRolePage _id={_id} />}
<EditRolePageWithData roleId={_id} />
</VerticalBar>
)) ||
null
......
import React from 'react';
import React, { ReactElement } from 'react';
import { usePermission } from '../../../contexts/AuthorizationContext';
import { useRouteParameter } from '../../../contexts/RouterContext';
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
import PermissionsTable from './PermissionsTable';
import UsersInRole from './UsersInRolePageContainer';
import PermissionsTable from './PermissionsTable/PermissionsTable';
import UsersInRole from './UsersInRole';
const PermissionsRouter = () => {
const PermissionsRouter = (): ReactElement => {
const canViewPermission = usePermission('access-permissions');
const canViewSettingPermission = usePermission('access-setting-permissions');
const context = useRouteParameter('context');
......
import { Table } from '@rocket.chat/fuselage';
import { IRole, IPermission } from '@rocket.chat/core-typings';
import { TableRow, TableCell } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState, memo } from 'react';
import React, { useState, memo, ReactElement } from 'react';
import { CONSTANTS } from '../../../../app/authorization/lib';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { CONSTANTS } from '../../../../../app/authorization/lib';
import { useTranslation, TranslationKey } from '../../../../contexts/TranslationContext';
import { useChangeRole } from '../hooks/useChangeRole';
import RoleCell from './RoleCell';
const useChangeRole = ({ onGrant, onRemove, permissionId }) => {
const dispatchToastMessage = useToastMessageDispatch();
return useMutableCallback(async (roleId, granted) => {
try {
if (granted) {
await onRemove(permissionId, roleId);
} else {
await onGrant(permissionId, roleId);
}
return !granted;
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
return granted;
});
};
const getName = (t, permission) => {
const getName = (t: ReturnType<typeof useTranslation>, permission: IPermission): string => {
if (permission.level === CONSTANTS.SETTINGS_LEVEL) {
let path = '';
if (permission.group) {
path = `${t(permission.group)} > `;
path = `${t(permission.group as TranslationKey)} > `;
}
if (permission.section) {
path = `${path}${t(permission.section)} > `;
path = `${path}${t(permission.section as TranslationKey)} > `;
}
return `${path}${t(permission.settingId)}`;
return `${path}${t(permission.settingId as TranslationKey)}`;
}
return t(permission._id);
return t(permission._id as TranslationKey);
};
const PermissionRow = ({ permission, t, roleList, onGrant, onRemove, ...props }) => {
const { _id, roles } = permission;
type PermissionRowProps = {
permission: IPermission;
roleList: IRole[];
onGrant: (permissionId: IPermission['_id'], roleId: IRole['_id']) => Promise<void>;
onRemove: (permissionId: IPermission['_id'], roleId: IRole['_id']) => Promise<void>;
};
const PermissionRow = ({ permission, roleList, onGrant, onRemove }: PermissionRowProps): ReactElement => {
const t = useTranslation();
const { _id, roles } = permission;
const [hovered, setHovered] = useState(false);
const changeRole = useChangeRole({ onGrant, onRemove, permissionId: _id });
const onMouseEnter = useMutableCallback(() => setHovered(true));
const onMouseLeave = useMutableCallback(() => setHovered(false));
const changeRole = useChangeRole({ onGrant, onRemove, permissionId: _id });
return (
<Table.Row key={_id} role='link' action tabIndex={0} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...props}>
<Table.Cell maxWidth='x300' withTruncatedText title={t(`${_id}_description`)}>
<TableRow key={_id} role='link' action tabIndex={0} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<TableCell maxWidth='x300' withTruncatedText title={t(`${_id}_description` as TranslationKey)}>
{getName(t, permission)}
</Table.Cell>
</TableCell>
{roleList.map(({ _id, name, description }) => (
<RoleCell
key={_id}
......@@ -64,7 +56,7 @@ const PermissionRow = ({ permission, t, roleList, onGrant, onRemove, ...props })
permissionId={_id}
/>
))}
</Table.Row>
</TableRow>
);
};
......
import { Margins, Icon, Tabs, Button } from '@rocket.chat/fuselage';
import { Margins, Icon, Tabs, Button, Pagination, Tile } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState, useCallback } from 'react';
import React, { useState, ReactElement } from 'react';
import GenericTable from '../../../components/GenericTable';
import Page from '../../../components/Page';
import { usePermission } from '../../../contexts/AuthorizationContext';
import { useRoute } from '../../../contexts/RouterContext';
import { useMethod } from '../../../contexts/ServerContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import FilterComponent from './FilterComponent';
import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '../../../../components/GenericTable';
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import Page from '../../../../components/Page';
import { usePermission } from '../../../../contexts/AuthorizationContext';
import { useRoute } from '../../../../contexts/RouterContext';
import { useMethod } from '../../../../contexts/ServerContext';
import { useTranslation } from '../../../../contexts/TranslationContext';
import PermissionsContextBar from '../PermissionsContextBar';
import { usePermissionsAndRoles } from '../hooks/usePermissionsAndRoles';
import PermissionRow from './PermissionRow';
import PermissionsContextBar from './PermissionsContextBar';
import PermissionsTableFilter from './PermissionsTableFilter';
import RoleHeader from './RoleHeader';
import { usePermissionsAndRoles } from './hooks/usePermissionsAndRoles';
const PermissionsTable = () => {
const PermissionsTable = (): ReactElement => {
const t = useTranslation();
const [filter, setFilter] = useState('');
const canViewPermission = usePermission('access-permissions');
const canViewSettingPermission = usePermission('access-setting-permissions');
const defaultType = canViewPermission ? 'permissions' : 'settings';
const [type, setType] = useState(defaultType);
const [params, setParams] = useState({ limit: 25, skip: 0 });
const router = useRoute('admin-permissions');
const grantRole = useMethod('authorization:addPermissionToRole');
const removeRole = useMethod('authorization:removeRoleFromPermission');
const permissionsData = usePermissionsAndRoles(type, filter, params.limit, params.skip);
const [permissions, total, roleList] = permissionsData;
const handleParams = useMutableCallback(({ current, itemsPerPage }) => {
setParams({ skip: current, limit: itemsPerPage });
});
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
const { permissions, total, roleList } = usePermissionsAndRoles(type, filter, itemsPerPage, current);
const handlePermissionsTab = useMutableCallback(() => {
if (type === 'permissions') {
......@@ -76,36 +70,44 @@ const PermissionsTable = () => {
</Margins>
<Page.Content mb='neg-x8'>
<Margins block='x8'>
<FilterComponent onChange={setFilter} />
<GenericTable
header={
<>
<GenericTable.HeaderCell width='x120'>{t('Name')}</GenericTable.HeaderCell>
{roleList.map(({ _id, name, description }) => (
<RoleHeader key={_id} _id={_id} name={name} description={description} router={router} />
))}
</>
}
total={total}
results={permissions}
params={params}
setParams={handleParams}
fixed={false}
>
{useCallback(
(permission) => (
<PermissionRow
key={permission._id}
permission={permission}
t={t}
roleList={roleList}
onGrant={grantRole}
onRemove={removeRole}
/>
),
[grantRole, removeRole, roleList, t],
)}
</GenericTable>
<PermissionsTableFilter onChange={setFilter} />
{permissions?.length === 0 && (
<Tile fontScale='p2' elevation='0' color='info' textAlign='center'>
{t('No_data_found')}
</Tile>
)}
{permissions?.length > 0 && (
<>
<GenericTable fixed={false}>
<GenericTableHeader>
<GenericTableHeaderCell width='x120'>{t('Name')}</GenericTableHeaderCell>
{roleList?.map(({ _id, name, description }) => (
<RoleHeader key={_id} _id={_id} name={name} description={description} />
))}
</GenericTableHeader>
<GenericTableBody>
{permissions?.map((permission) => (
<PermissionRow
key={permission._id}
permission={permission}
roleList={roleList}
onGrant={grantRole}
onRemove={removeRole}
/>
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={total}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
)}
</Margins>
</Page.Content>
</Page>
......
import { TextInput } from '@rocket.chat/fuselage';
import { useMutableCallback, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, ReactElement } from 'react';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useTranslation } from '../../../../contexts/TranslationContext';
const FilterComponent = ({ onChange }) => {
const PermissionsTableFilter = ({ onChange }: { onChange: (debouncedFilter: string) => void }): ReactElement => {
const t = useTranslation();
const [filter, setFilter] = useState('');
const debouncedFilter = useDebouncedValue(filter, 500);
useEffect(() => {
......@@ -21,4 +20,4 @@ const FilterComponent = ({ onChange }) => {
return <TextInput value={filter} onChange={handleFilter} placeholder={t('Search')} flexGrow={0} />;
};
export default FilterComponent;
export default PermissionsTableFilter;
import { Table, Margins, Box, CheckBox, Throbber } from '@rocket.chat/fuselage';
import { IRole } from '@rocket.chat/core-typings';
import { TableCell, Margins, Box, CheckBox, Throbber } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState, memo } from 'react';
import React, { useState, memo, ReactElement } from 'react';
import { AuthorizationUtils } from '../../../../app/authorization/lib';
import { AuthorizationUtils } from '../../../../../app/authorization/lib';
const RoleCell = ({ grantedRoles = [], _id, name, description, onChange, lineHovered, permissionId }) => {
type RoleCellProps = {
_id: IRole['_id'];
name: IRole['name'];
description: IRole['description'];
onChange: (roleId: IRole['_id'], granted: boolean) => Promise<boolean>;
lineHovered: boolean;
permissionId: string;
grantedRoles: IRole['_id'][];
};
const RoleCell = ({ _id, name, description, onChange, lineHovered, permissionId, grantedRoles = [] }: RoleCellProps): ReactElement => {
const [granted, setGranted] = useState(() => !!grantedRoles.includes(_id));
const [loading, setLoading] = useState(false);
......@@ -20,7 +31,7 @@ const RoleCell = ({ grantedRoles = [], _id, name, description, onChange, lineHov
const isDisabled = !!loading || !!isRestrictedForRole;
return (
<Table.Cell withTruncatedText>
<TableCell withTruncatedText>
<Margins inline='x2'>
<CheckBox checked={granted} onChange={handleChange} disabled={isDisabled} />
{!loading && (
......@@ -30,7 +41,7 @@ const RoleCell = ({ grantedRoles = [], _id, name, description, onChange, lineHov
)}
{loading && <Throbber size='x12' display='inline-block' />}
</Margins>
</Table.Cell>
</TableCell>
);
};
......
import { IRole } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
import { Margins, Box, Icon } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { memo } from 'react';
import React, { memo, ReactElement } from 'react';
import GenericTable from '../../../components/GenericTable';
import GenericTable from '../../../../components/GenericTable';
import { useRoute } from '../../../../contexts/RouterContext';
const RoleHeader = ({ router, _id, name, description, ...props }) => {
const onClick = useMutableCallback(() => {
type RoleHeaderProps = {
_id: IRole['_id'];
name: IRole['name'];
description: IRole['description'];
};
const RoleHeader = ({ _id, name, description }: RoleHeaderProps): ReactElement => {
const router = useRoute('admin-permissions');
const handleEditRole = useMutableCallback(() => {
router.push({
context: 'edit',
_id,
......@@ -14,7 +24,7 @@ const RoleHeader = ({ router, _id, name, description, ...props }) => {
});
return (
<GenericTable.HeaderCell clickable pi='x4' p='x8' {...props}>
<GenericTable.HeaderCell clickable pi='x4' p='x8'>
<Box
className={css`
white-space: nowrap;
......@@ -26,7 +36,7 @@ const RoleHeader = ({ router, _id, name, description, ...props }) => {
borderWidth='x2'
borderRadius='x2'
borderColor='neutral-300'
onClick={onClick}
onClick={handleEditRole}
>
<Margins inline='x2'>
<span>{description || name}</span>
......
export { default } from './PermissionsTable';
import { Box, Field, TextInput, Select, ToggleSwitch } from '@rocket.chat/fuselage';
import React, { useMemo } from 'react';
import { Box, Field, TextInput, Select, ToggleSwitch, SelectOption } from '@rocket.chat/fuselage';
import React, { useMemo, ReactElement } from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import { useTranslation } from '../../../contexts/TranslationContext';
const RoleForm = ({ values, handlers, className, errors, editing = false, isProtected = false }) => {
const t = useTranslation();
const { name, description, scope, mandatory2fa } = values;
type RoleFormProps = {
className?: string;
editing?: boolean;
isProtected?: boolean;
};
const { handleName, handleDescription, handleScope, handleMandatory2fa } = handlers;
const RoleForm = ({ className, editing = false, isProtected = false }: RoleFormProps): ReactElement => {
const t = useTranslation();
const {
register,
control,
formState: { errors },
} = useFormContext();
const options = useMemo(
const options: SelectOption[] = useMemo(
() => [
['Users', t('Global')],
['Subscriptions', t('Rooms')],
......@@ -23,28 +31,36 @@ const RoleForm = ({ values, handlers, className, errors, editing = false, isProt
<Field className={className}>
<Field.Label>{t('Role')}</Field.Label>
<Field.Row>
<TextInput disabled={editing} value={name} onChange={handleName} placeholder={t('Role')} />
<TextInput disabled={editing} placeholder={t('Role')} {...register('name', { required: true })} />
</Field.Row>
<Field.Error>{errors?.name}</Field.Error>
{errors?.name && <Field.Error>{t('error-the-field-is-required', { field: t('Role') })}</Field.Error>}
</Field>
<Field className={className}>
<Field.Label>{t('Description')}</Field.Label>
<Field.Row>
<TextInput value={description} onChange={handleDescription} placeholder={t('Description')} />
<TextInput placeholder={t('Description')} {...register('description')} />
</Field.Row>
<Field.Hint>{'Leave the description field blank if you dont want to show the role'}</Field.Hint>
</Field>
<Field className={className}>
<Field.Label>{t('Scope')}</Field.Label>
<Field.Row>
<Select disabled={isProtected} options={options} value={scope} onChange={handleScope} placeholder={t('Scope')} />
<Controller
name='scope'
control={control}
render={({ field }): ReactElement => <Select {...field} options={options} disabled={isProtected} placeholder={t('Scope')} />}
/>
</Field.Row>
</Field>
<Field className={className}>
<Box display='flex' flexDirection='row'>
<Field.Label>{t('Users must use Two Factor Authentication')}</Field.Label>
<Field.Row>
<ToggleSwitch checked={mandatory2fa} onChange={handleMandatory2fa} />
<Controller
name='mandatory2fa'
control={control}
render={({ field }): ReactElement => <ToggleSwitch {...field} checked={field.value} />}
/>
</Field.Row>
</Box>
</Field>
......
import { IRole } from '@rocket.chat/core-typings';
import { Box, Field, Margins, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState, useRef } from 'react';
import React, { useState, useRef, ReactElement } from 'react';
import Page from '../../../components/Page';
import RoomAutoComplete from '../../../components/RoomAutoComplete';
import UserAutoComplete from '../../../components/UserAutoComplete';
import { useRoute } from '../../../contexts/RouterContext';
import { useEndpoint } from '../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import UsersInRoleTableContainer from './UsersInRoleTableContainer';
import Page from '../../../../components/Page';
import RoomAutoComplete from '../../../../components/RoomAutoComplete';
import UserAutoComplete from '../../../../components/UserAutoComplete';
import { useRoute } from '../../../../contexts/RouterContext';
import { useEndpoint } from '../../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../../contexts/TranslationContext';
import UsersInRoleTable from './UsersInRoleTable';
const UsersInRolePage = ({ data }) => {
const UsersInRolePage = ({ role }: { role: IRole }): ReactElement => {
const t = useTranslation();
const reload = useRef<() => void>(() => undefined);
const [user, setUser] = useState<string | undefined>('');
const [rid, setRid] = useState<string>();
const [userError, setUserError] = useState<string>();
const dispatchToastMessage = useToastMessageDispatch();
const reload = useRef();
const [user, setUser] = useState('');
const [rid, setRid] = useState();
const [userError, setUserError] = useState();
const { _id, name, description } = data;
const { _id, name, description } = role;
const router = useRoute('admin-permissions');
const addUser = useEndpoint('POST', 'roles.addUserToRole');
const handleReturn = useMutableCallback(() => {
......@@ -42,8 +39,8 @@ const UsersInRolePage = ({ data }) => {
try {
await addUser({ roleId: _id, username: user, roomId: rid });
dispatchToastMessage({ type: 'success', message: t('User_added') });
setUser();
reload.current();
setUser(undefined);
reload.current?.();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
......@@ -51,7 +48,7 @@ const UsersInRolePage = ({ data }) => {
const handleUserChange = useMutableCallback((user) => {
if (user !== '') {
setUserError();
setUserError(undefined);
}
return setUser(user);
......@@ -67,7 +64,7 @@ const UsersInRolePage = ({ data }) => {
<Page.Content>
<Box display='flex' flexDirection='column' w='full' mi='neg-x4'>
<Margins inline='x4'>
{data.scope !== 'Users' && (
{role.scope !== 'Users' && (
<Field mbe='x4'>
<Field.Label>{t('Choose_a_room')}</Field.Label>
<Field.Row>
......@@ -91,17 +88,10 @@ const UsersInRolePage = ({ data }) => {
</Margins>
</Box>
<Margins blockStart='x8'>
{(data.scope === 'Users' || rid) && (
<UsersInRoleTableContainer
reloadRef={reload}
scope={data.scope}
rid={rid}
roleId={_id}
roleName={name}
description={description}
/>
{(role.scope === 'Users' || rid) && (
<UsersInRoleTable reloadRef={reload} rid={rid} roleId={_id} roleName={name} description={description} />
)}
{data.scope !== 'Users' && !rid && <Callout type='info'>{t('Select_a_room')}</Callout>}
{role.scope !== 'Users' && !rid && <Callout type='info'>{t('Select_a_room')}</Callout>}
</Margins>
</Page.Content>
</Page>
......
import React from 'react';
import React, { ReactElement } from 'react';
import { useRouteParameter } from '../../../contexts/RouterContext';
import { useRouteParameter } from '../../../../contexts/RouterContext';
import { useRole } from '../hooks/useRole';
import UsersInRolePage from './UsersInRolePage';
import { useRole } from './useRole';
const UsersInRolePageContainer = () => {
const UsersInRolePageWithData = (): ReactElement | null => {
const _id = useRouteParameter('_id');
const role = useRole(_id);
if (!role) {
return null;
}
return <UsersInRolePage data={role} />;
return <UsersInRolePage role={role} />;
};
export default UsersInRolePageContainer;
export default UsersInRolePageWithData;
import { IRole, IRoom, IUserInRole } from '@rocket.chat/core-typings';
import { Tile, Pagination } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { ReactElement } from 'react';
import GenericModal from '../../../../../components/GenericModal';
import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '../../../../../components/GenericTable';
import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination';
import { useSetModal } from '../../../../../contexts/ModalContext';
import { useEndpoint } from '../../../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext';
import { useTranslation } from '../../../../../contexts/TranslationContext';
import UsersInRoleTableRow from './UsersInRoleTableRow';
type UsersInRoleTable = {
users: IUserInRole[];
reload: () => void;
roleName: IRole['name'];
roleId: IRole['_id'];
description: IRole['description'];
total: number;
rid?: IRoom['_id'];
paginationData: ReturnType<typeof usePagination>;
};
const UsersInRoleTable = ({ users, reload, roleName, roleId, description, total, rid, paginationData }: UsersInRoleTable): ReactElement => {
const t = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const removeUser = useEndpoint('POST', 'roles.removeUserFromRole');
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = paginationData;
const closeModal = (): void => setModal();
const handleRemove = useMutableCallback((username) => {
const remove = async (): Promise<void> => {
try {
await removeUser({ roleId, username, scope: rid });
dispatchToastMessage({ type: 'success', message: t('User_removed') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
closeModal();
reload();
}
};
setModal(
<GenericModal variant='danger' onConfirm={remove} onClose={closeModal} onCancel={closeModal} confirmText={t('Delete')}>
{t('The_user_s_will_be_removed_from_role_s', username, description || roleName)}
</GenericModal>,
);
});
return (
<>
{users.length === 0 && (
<Tile fontScale='p2' elevation='0' color='info' textAlign='center'>
{t('No_data_found')}
</Tile>
)}
{users.length > 0 && (
<>
<GenericTable>
<GenericTableHeader>
<GenericTableHeaderCell>{t('Name')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Email')}</GenericTableHeaderCell>
<GenericTableHeaderCell w='x80'></GenericTableHeaderCell>
</GenericTableHeader>
<GenericTableBody>
{users.map((user) => (
<UsersInRoleTableRow onRemove={handleRemove} key={user?._id} user={user} />
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={total}
onSetItemsPerPage={onSetItemsPerPage}
onSetCurrent={onSetCurrent}
{...paginationProps}
/>
</>
)}
</>
);
};
export default UsersInRoleTable;
import { Box, Table, Button, Icon } from '@rocket.chat/fuselage';
import { IUserInRole } from '@rocket.chat/core-typings';
import { Box, TableRow, TableCell, Button, Icon } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { memo } from 'react';
import React, { memo, ReactElement } from 'react';
import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress';
import UserAvatar from '../../../components/avatar/UserAvatar';
import { getUserEmailAddress } from '../../../../../../lib/getUserEmailAddress';
import UserAvatar from '../../../../../components/avatar/UserAvatar';
const UserRow = ({ _id, username, name, avatarETag, emails, onRemove }) => {
const email = getUserEmailAddress({ emails });
type UsersInRoleTableRowProps = {
user: IUserInRole;
onRemove: (username: IUserInRole['username']) => void;
};
const UsersInRoleTableRow = ({ user, onRemove }: UsersInRoleTableRowProps): ReactElement => {
const { _id, name, username, avatarETag } = user;
const email = getUserEmailAddress(user);
const handleRemove = useMutableCallback(() => {
onRemove(username);
});
return (
<Table.Row key={_id} tabIndex={0} role='link'>
<Table.Cell withTruncatedText>
<TableRow key={_id} tabIndex={0} role='link'>
<TableCell withTruncatedText>
<Box display='flex' alignItems='center'>
<UserAvatar size='x40' title={username} username={username} etag={avatarETag} />
<UserAvatar size='x40' username={username ?? ''} etag={avatarETag} />
<Box display='flex' withTruncatedText mi='x8'>
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText>
<Box fontScale='p2m' withTruncatedText color='default'>
......@@ -31,15 +38,15 @@ const UserRow = ({ _id, username, name, avatarETag, emails, onRemove }) => {
</Box>
</Box>
</Box>
</Table.Cell>
<Table.Cell withTruncatedText>{email}</Table.Cell>
<Table.Cell withTruncatedText>
</TableCell>
<TableCell withTruncatedText>{email}</TableCell>
<TableCell withTruncatedText>
<Button small square danger onClick={handleRemove}>
<Icon name='trash' size='x20' />
</Button>
</Table.Cell>
</Table.Row>
</TableCell>
</TableRow>
);
};
export default memo(UserRow);
export default memo(UsersInRoleTableRow);
import { IRole, IRoom, IUserInRole } from '@rocket.chat/core-typings';
import React, { useEffect, useMemo, ReactElement, MutableRefObject } from 'react';
import { usePagination } from '../../../../../components/GenericTable/hooks/usePagination';
import { useEndpointData } from '../../../../../hooks/useEndpointData';
import { AsyncStatePhase } from '../../../../../lib/asyncState';
import UsersInRoleTable from './UsersInRoleTable';
type UsersInRoleTableWithDataProps = {
rid?: IRoom['_id'];
roleId: IRole['_id'];
roleName: IRole['name'];
description: IRole['description'];
reloadRef: MutableRefObject<() => void>;
};
const UsersInRoleTableWithData = ({
rid,
roleId,
roleName,
description,
reloadRef,
}: UsersInRoleTableWithDataProps): ReactElement | null => {
const { itemsPerPage, current, ...paginationData } = usePagination();
const query = useMemo(
() => ({
role: roleId,
...(rid && { roomId: rid }),
...(itemsPerPage && { count: itemsPerPage }),
...(current && { offset: current }),
}),
[itemsPerPage, current, rid, roleId],
);
const { reload, ...result } = useEndpointData('roles.getUsersInRole', query);
useEffect(() => {
reloadRef.current = reload;
}, [reload, reloadRef]);
if (result.phase === AsyncStatePhase.LOADING || result.phase === AsyncStatePhase.REJECTED) {
return null;
}
const users: IUserInRole[] = result.value?.users.map((user) => ({
...user,
createdAt: new Date(user.createdAt),
_updatedAt: new Date(user._updatedAt),
}));
return (
<UsersInRoleTable
users={users}
total={result.value.total}
reload={reload}
roleName={roleName}
roleId={roleId}
description={description}
rid={rid}
paginationData={{ itemsPerPage, current, ...paginationData }}
/>
);
};
export default UsersInRoleTableWithData;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment