Refactor: Admin permissions page (#18932)

Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
pull/18762/head
gabriellsh 5 years ago committed by GitHub
parent ee1001af14
commit aa6e674bb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      app/api/server/v1/roles.js
  2. 2
      app/authorization/client/index.js
  3. 39
      app/authorization/client/route.js
  4. 4
      app/authorization/client/startup.js
  5. 122
      app/authorization/client/stylesheets/permissions.css
  6. 4
      app/authorization/client/views/index.js
  7. 80
      app/authorization/client/views/permissions.html
  8. 205
      app/authorization/client/views/permissions.js
  9. 110
      app/authorization/client/views/permissionsRole.html
  10. 277
      app/authorization/client/views/permissionsRole.js
  11. 1
      app/authorization/server/methods/deleteRole.js
  12. 90
      client/admin/permissions/EditRolePage.js
  13. 46
      client/admin/permissions/NewRolePage.js
  14. 34
      client/admin/permissions/PermissionsContextBar.js
  15. 16
      client/admin/permissions/PermissionsRouter.js
  16. 261
      client/admin/permissions/PermissionsTable.js
  17. 59
      client/admin/permissions/RoleForm.js
  18. 95
      client/admin/permissions/UsersInRole.js
  19. 118
      client/admin/permissions/UsersInRoleTable.js
  20. 6
      client/admin/permissions/useRole.js
  21. 5
      client/admin/routes.js
  22. 5
      client/components/DeleteWarningModal.js
  23. 3
      client/components/GenericTable.js
  24. 32
      client/components/basic/RoomAutoComplete.js
  25. 2
      client/omnichannel/agents/AgentsRoute.js
  26. 2
      client/omnichannel/currentChats/CurrentChatsPage.js
  27. 2
      client/omnichannel/currentChats/CurrentChatsRoute.js
  28. 2
      client/omnichannel/customFields/CustomFieldsTable.js
  29. 2
      client/omnichannel/triggers/TriggersTable.js
  30. 2
      ee/client/omnichannel/BusinessHoursTable.js
  31. 2
      ee/client/omnichannel/MonitorsTable.js
  32. 2
      ee/client/omnichannel/priorities/PrioritiesRoute.js
  33. 2
      ee/client/omnichannel/tags/TagsRoute.js
  34. 2
      ee/client/omnichannel/units/UnitsRoute.js
  35. 1
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -83,6 +83,7 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, {
name: 1, name: 1,
username: 1, username: 1,
emails: 1, emails: 1,
avatarETag: 1,
}; };
if (!role) { if (!role) {
@ -99,7 +100,7 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, {
sort: { username: 1 }, sort: { username: 1 },
skip: offset, skip: offset,
fields, fields,
}).fetch(); });
return API.v1.success({ users }); return API.v1.success({ users: users.fetch(), total: users.count() });
}, },
}); });

@ -3,9 +3,7 @@ import { hasRole } from './hasRole';
import { AuthorizationUtils } from '../lib/AuthorizationUtils'; import { AuthorizationUtils } from '../lib/AuthorizationUtils';
import './usersNameChanged'; import './usersNameChanged';
import './requiresPermission.html'; import './requiresPermission.html';
import './route';
import './startup'; import './startup';
import './stylesheets/permissions.css';
export { export {
hasAllPermission, hasAllPermission,

@ -1,39 +0,0 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../../client/admin';
import { t } from '../../utils/client';
registerAdminRoute('/permissions', {
name: 'admin-permissions',
async action(/* params*/) {
await import('./views');
return BlazeLayout.render('main', {
center: 'permissions',
pageTitle: t('Permissions'),
});
},
});
registerAdminRoute('/permissions/:name?/edit', {
name: 'admin-permissions-edit',
async action(/* params*/) {
await import('./views');
return BlazeLayout.render('main', {
center: 'pageContainer',
pageTitle: t('Role_Editing'),
pageTemplate: 'permissionsRole',
});
},
});
registerAdminRoute('/permissions/new', {
name: 'admin-permissions-new',
async action(/* params*/) {
await import('./views');
return BlazeLayout.render('main', {
center: 'pageContainer',
pageTitle: t('Role_Editing'),
pageTemplate: 'permissionsRole',
});
},
});

@ -27,7 +27,9 @@ Meteor.startup(() => {
delete role.type; delete role.type;
Roles.upsert({ _id: role.name }, role); Roles.upsert({ _id: role.name }, role);
}, },
removed: (role) => Roles.remove({ _id: role.name }), removed: (role) => {
Roles.remove({ _id: role.name });
},
}; };
Tracker.autorun((c) => { Tracker.autorun((c) => {

@ -1,122 +0,0 @@
.permissions-manager {
display: flex;
flex-direction: column;
height: 100%;
&.page-container {
padding-bottom: 0 !important;
}
.permission-edit {
display: flex;
flex-direction: column;
height: 100%;
padding: 10px;
align-items: center;
}
.permission-label,
.permission-icon {
display: flex;
align-items: flex-end;
flex-grow: 1;
}
.permission-icon {
width: 30px;
margin-bottom: 0;
flex-grow: 0;
}
.content {
padding: 0 !important;
}
.permission-grid {
overflow-x: scroll;
table-layout: fixed;
border-collapse: collapse;
.id-styler {
white-space: nowrap;
color: #7f7f7f;
font-size: smaller;
}
.edit-icon.role-name-edit-icon {
height: 30px;
}
.role-name {
position: sticky;
top: 0;
width: 70px;
text-align: left;
vertical-align: middle;
background: white;
}
.role-name-edit-icon {
width: 70px;
height: 70px;
text-align: center;
vertical-align: middle;
}
.rotator {
overflow: hidden;
width: 30px;
height: 130px;
padding: 10px 0;
transform: rotate(-180deg);
white-space: nowrap;
text-overflow: ellipsis;
writing-mode: vertical-rl;
}
.admin-table-row {
height: 50px;
}
td {
overflow: hidden;
}
.permission-name {
width: 25%;
padding-left: 14px;
vertical-align: middle;
}
.permission-checkbox {
text-align: center;
vertical-align: middle;
}
.icon-edit {
font-size: 1.5em;
}
}
}

@ -1,4 +0,0 @@
import './permissions.html';
import './permissions';
import './permissionsRole.html';
import './permissionsRole';

@ -1,80 +0,0 @@
<template name="permissionsTable">
<div class="content">
<table class="permission-grid secondary-background-color">
<thead class="content-background-color">
<tr>
<th class=" role-name permission-name border-component-color"></th>
{{#each role in allRoles}}
<th class="edit-icon role-name border-component-color" title="{{role.description}}">
<a class="permission-edit" href="{{pathFor "admin-permissions-edit" name=role._id}}">
<p class="permission-label">
<div class = "rotator">
{{role._id}}
</div>
</p>
<p class="permission-icon">
<i class="icon-edit"></i>
</p>
</a>
</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each permission in permissions}}
<tr class="admin-table-row">
<td class="permission-name border-component-color" title="{{permissionDescription permission}}">{{permissionName permission}}<br><span class = "id-styler">[ID: {{permission._id}}]</span></td>
{{#each role in allRoles}}
<td class="permission-checkbox border-component-color">
{{#if isRolePermissionEnabled role permission}}
<input type="checkbox" name="perm[{{role._id}}][{{permission._id}}]" class="role-permission" value="1" checked="{{granted permission.roles role}}" data-role="{{role._id}}" data-permission="{{permission._id}}">
{{/if}}
</td>
{{else}}
<tr class="table-no-click">
<td>{{_ "No_results_found_for"}} {{filter}}</td>
</tr>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</div>
</template>
<template name="permissions">
<div class="page-container permissions-manager">
<section class="permissions-manager page-container page-home page-static page-list">
{{#if hasPermission}}
{{# header sectionName=pageTitle}}
<div class="rc-header__section-button">
<a href="{{pathFor"admin-permissions-new"}}"
class="rc-button rc-button--primary new-role">{{_ "New_role"}}</a>
</div>
{{/header}}
<form class="search-form" role="form">
<div class="rc-input__wrapper">
<div class="rc-input__icon">
{{> icon block="rc-input__icon-svg" icon="magnifier" }}
</div>
<input id="permissions-filter" type="text" class="rc-input__element" placeholder="{{_ " Search "}}" autofocus dir="auto">
</div>
</form>
{{>tabs tabs=tabsData}}
{{#if $eq tab 'settings'}}
{{> permissionsTable query=filter permissions=settingPermissions allRoles=roles collection='Setting'}}
{{else}}
{{> permissionsTable query=filter permissions=permissions allRoles=roles collection='Chat'}}
{{/if}}
{{/if}}
{{#if hasNoPermission}}
<div class="content">
{{#if i18nPageTitle}} {{> header sectionName=i18nPageTitle}} {{else}} {{> header sectionName=pageTitle}}
{{/if}}
{{_ "Not_authorized"}}
</div>
{{/if}}
</section>
</div>
</template>

@ -1,205 +0,0 @@
import { Meteor } from 'meteor/meteor';
import _ from 'underscore';
import s from 'underscore.string';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
import { Roles } from '../../../models/client';
import { ChatPermissions } from '../lib/ChatPermissions';
import { hasAllPermission } from '../hasPermission';
import { t } from '../../../utils/client';
import { SideNav } from '../../../ui-utils/client/lib/SideNav';
import { CONSTANTS, AuthorizationUtils } from '../../lib';
import { hasAtLeastOnePermission } from '..';
Template.permissions.helpers({
tabsData() {
const {
state,
} = Template.instance();
const permissionsTab = {
label: t('Permissions'),
value: 'permissions',
condition() {
return true;
},
};
const settingsTab = {
label: t('Settings'),
value: 'settings',
condition() {
return true;
},
};
const tabs = [permissionsTab];
const settingsPermissions = hasAllPermission('access-setting-permissions');
if (settingsPermissions) {
tabs.push(settingsTab);
}
switch (settingsPermissions && state.get('tab')) {
case 'settings':
settingsTab.active = true;
break;
case 'permissions':
permissionsTab.active = true;
break;
default:
permissionsTab.active = true;
}
return {
tabs,
onChange(value) {
state.set({
tab: value,
size: 50,
});
},
};
},
roles() {
return Roles.find();
},
permissions() {
const { state } = Template.instance();
const limit = state.get('size');
const filter = new RegExp(s.escapeRegExp(state.get('filter')), 'i');
return ChatPermissions.find(
{
level: { $ne: CONSTANTS.SETTINGS_LEVEL },
_id: filter,
},
{
sort: {
_id: 1,
},
limit,
},
);
},
settingPermissions() {
const { state } = Template.instance();
const limit = state.get('size');
const filter = new RegExp(s.escapeRegExp(state.get('filter')), 'i');
return ChatPermissions.find(
{
_id: filter,
level: CONSTANTS.SETTINGS_LEVEL,
group: { $exists: true },
},
{
limit,
sort: {
group: 1,
section: 1,
},
},
);
},
hasPermission() {
return hasAllPermission('access-permissions');
},
hasNoPermission() {
return !hasAtLeastOnePermission([
'access-permissions',
'access-setting-permissions',
]);
},
filter() {
return Template.instance().state.get('filter');
},
tab() {
return Template.instance().state.get('tab');
},
});
Template.permissions.events({
'keyup #permissions-filter'(e, t) {
e.stopPropagation();
e.preventDefault();
t.state.set('filter', e.currentTarget.value);
},
'scroll .content': _.throttle(({ currentTarget }, i) => {
if (
currentTarget.offsetHeight + currentTarget.scrollTop
>= currentTarget.scrollHeight - 100
) {
return i.state.set('size', i.state.get('size') + 50);
}
}, 300),
});
Template.permissions.onCreated(function() {
this.state = new ReactiveDict({
filter: '',
tab: '',
size: 50,
});
this.autorun(() => {
this.state.get('filter');
this.state.set('size', 50);
});
});
Template.permissionsTable.helpers({
granted(roles, role) {
return (roles && ~roles.indexOf(role._id) && 'checked') || null;
},
permissionName(permission) {
if (permission.level === CONSTANTS.SETTINGS_LEVEL) {
let path = '';
if (permission.group) {
path = `${ t(permission.group) } > `;
}
if (permission.section) {
path = `${ path }${ t(permission.section) } > `;
}
return `${ path }${ t(permission.settingId) }`;
}
return t(permission._id);
},
permissionDescription(permission) {
return t(`${ permission._id }_description`);
},
isRolePermissionEnabled(role, permission) {
return !AuthorizationUtils.isPermissionRestrictedForRole(permission._id, role._id);
},
});
Template.permissionsTable.events({
'click .role-permission'(e) {
const permissionId = e.currentTarget.getAttribute('data-permission');
const role = e.currentTarget.getAttribute('data-role');
const permission = permissionId && ChatPermissions.findOne(permissionId);
const action = ~permission.roles.indexOf(role) ? 'authorization:removeRoleFromPermission' : 'authorization:addPermissionToRole';
return Meteor.call(action, permissionId, role);
},
});
Template.permissions.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,110 +0,0 @@
<template name="permissionsRole">
<div class="permissions-manager">
{{#if hasPermission}}
{{#with role}}
<form id="form-role" class="inline form-role form-inline">
<div class="form-group">
<div class="rc-input">
<div class="rc-input__title">{{_ "Role"}}</div>
{{#if editing}}
<input type="text" class="rc-input__element" name="name" autocomplete="off" value="{{_id}}" disabled>
{{else}}
<input type="text" class="rc-input__element" name="name" autocomplete="off">
{{/if}}
</div>
</div>
<div class="form-group">
<div class="rc-input">
<div class="rc-input__title">{{_ "Description"}}</div>
<input type="text" class="rc-input__element" name="description" autocomplete="off" value="{{description}}">
</div>
</div>
<div class="form-group">
<div class="rc-input__title">{{_ "Scope"}}</div>
<div class="rc-select">
<select name="scope" class="required rc-select__element" disabled="{{protected}}">
<option value="Users" selected="{{$eq scope 'Users'}}">{{_ "Global"}}</option>
<option value="Subscriptions" selected="{{$eq scope 'Subscriptions'}}">{{_ "Rooms"}}</option>
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
</div>
<div>
<label for="mandatory2fa">{{_ "Users must use Two Factor Authentication"}} :</label>
<input id="mandatory2fa" type="checkbox" name="mandatory2fa" checked="{{mandatory2fa}}">
</div>
<div class="alert alert-warning">
<p for="mandatory2fa">{{_ "Leave the description field blank if you dont want to show the role"}}.</p>
</div>
<div class="rc-button__group">
{{#if editable}}
<button name="delete" class="rc-button rc-button--danger delete-role">{{_ "Delete"}}</button>
{{/if}}
<button name="save" class="rc-button rc-button--primary save">{{_ "Save"}}</button>
<a class="rc-button" href="{{pathFor "admin-permissions"}}">{{_ "Back_to_permissions"}}</a>
</div>
</form>
{{/with}}
{{#if editing}}
<h2 class="border-tertiary-background-color">{{_ "Users_in_role"}}</h2>
{{#if $eq role.scope 'Subscriptions'}}
<form id="form-search-room" class="inline">
<label class="rc-input">
<div class="rc-input__title">{{_ "Choose_a_room"}}</div>
{{> inputAutocomplete settings=autocompleteChannelSettings name="room" class="search autocomplete rc-input__element" placeholder=(_ "Enter_a_room_name") autocomplete="off"}}
</label>
</form>
{{/if}}
{{#if $or ($eq role.scope 'Users') searchRoom}}
<form id="form-users" class="inline">
<label class="rc-input">
<div class="rc-input__title">{{_ "Add_user"}}</div>
{{> inputAutocomplete settings=autocompleteUsernameSettings name="username" class="search autocomplete rc-input__element" placeholder=(_ "Enter_a_username") autocomplete="off"}}
</label>
<button name="add" class="rc-button rc-button--primary add">{{_ "Add"}}</button>
</form>
<div class="rc-table-content">
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
<thead>
<tr>
<th width="30%"><div class="table-fake-th">{{_ "Name"}}</div></th>
<th width="25%"><div class="table-fake-th">{{_ "Username"}}</div></th>
<th width="25%"><div class="table-fake-th">{{_ "Email"}}</div></th>
<th width="5%">&nbsp;</th>
</tr>
</thead>
<tbody>
{{#unless hasUsers}}
<tr>
<td colspan="5" class="empty-role">{{_ "There_are_no_users_in_this_role"}}</td>
</tr>
{{/unless}}
{{#each userInRole}}
<tr class="user-info" data-id="{{_id}}">
<td width="30%">
<div class="rc-table-wrapper">
<div class="rc-table-avatar">{{> avatar username=username}}</div>
<div class="rc-table-info">
<span class="rc-table-title">{{name}}</span>
</div>
</div>
</td>
<td><div class="rc-table-wrapper">{{username}}</div></td>
<td><div class="rc-table-wrapper">{{emailAddress}}</div></td>
<td><a href="#remove" class="remove-user"><i class="icon-block"></i></a></td>
</tr>
{{/each}}
</tbody>
{{/table}}
{{#if hasMore}}
<button class="rc-button rc-button--secondary load-more {{isLoading}}">{{_ "Load_more"}}</button>
{{/if}}
</div>
{{/if}}
{{/if}}
{{else}}
{{_ "Not_authorized"}}
{{/if}}
</div>
</template>

@ -1,277 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveDict } from 'meteor/reactive-dict';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import toastr from 'toastr';
import { handleError } from '../../../utils/client/lib/handleError';
import { t } from '../../../utils/lib/tapi18n';
import { Roles } from '../../../models';
import { hasAllPermission } from '../hasPermission';
import { modal } from '../../../ui-utils/client/lib/modal';
import { SideNav } from '../../../ui-utils/client/lib/SideNav';
import { APIClient } from '../../../utils/client';
import { call } from '../../../ui-utils/client';
const PAGE_SIZE = 50;
const loadUsers = async (instance) => {
const offset = instance.state.get('offset');
const rid = instance.searchRoom.get();
const params = {
role: FlowRouter.getParam('name'),
offset,
count: PAGE_SIZE,
...rid && { roomId: rid },
};
instance.state.set('loading', true);
const { users } = await APIClient.v1.get('roles.getUsersInRole', params);
instance.usersInRole.set(instance.usersInRole.curValue.concat(users));
instance.state.set({
loading: false,
hasMore: users.length === PAGE_SIZE,
});
};
Template.permissionsRole.helpers({
role() {
return Roles.findOne({
_id: FlowRouter.getParam('name'),
}) || {};
},
userInRole() {
return Template.instance().usersInRole.get();
},
editing() {
return FlowRouter.getParam('name') != null;
},
emailAddress() {
if (this.emails && this.emails.length > 0) {
return this.emails[0].address;
}
},
hasPermission() {
return hasAllPermission('access-permissions');
},
protected() {
return this.protected;
},
editable() {
return this._id && !this.protected;
},
hasUsers() {
return Template.instance().usersInRole.get().length > 0;
},
hasMore() {
return Template.instance().state.get('hasMore');
},
isLoading() {
const instance = Template.instance();
return (!instance.subscription.ready() || instance.state.get('loading')) && 'btn-loading';
},
searchRoom() {
return Template.instance().searchRoom.get();
},
autocompleteChannelSettings() {
return {
limit: 10,
rules: [
{
collection: 'CachedChannelList',
endpoint: 'rooms.autocomplete.channelAndPrivate',
field: 'name',
template: Template.roomSearch,
noMatchTemplate: Template.roomSearchEmpty,
matchAll: true,
sort: 'name',
selector(match) {
return {
name: match,
};
},
},
],
};
},
autocompleteUsernameSettings() {
const instance = Template.instance();
return {
limit: 10,
rules: [
{
collection: 'CachedUserList',
endpoint: 'users.autocomplete',
field: 'username',
template: Template.userSearch,
noMatchTemplate: Template.userSearchEmpty,
matchAll: true,
filter: {
exceptions: instance.usersInRole.get(),
},
selector(match) {
return {
term: match,
};
},
sort: 'username',
},
],
};
},
});
Template.permissionsRole.events({
async 'click .remove-user'(e, instance) {
e.preventDefault();
modal.open({
title: t('Are_you_sure'),
text: t('The_user_s_will_be_removed_from_role_s', this.username, FlowRouter.getParam('name')),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes'),
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, async () => {
await call('authorization:removeUserFromRole', FlowRouter.getParam('name'), this.username, instance.searchRoom.get());
instance.usersInRole.set(instance.usersInRole.curValue.filter((user) => user.username !== this.username));
modal.open({
title: t('Removed'),
text: t('User_removed'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
});
},
'submit #form-role'(e/* , instance*/) {
e.preventDefault();
const oldBtnValue = e.currentTarget.elements.save.value;
e.currentTarget.elements.save.value = t('Saving');
const roleData = {
description: e.currentTarget.elements.description.value,
scope: e.currentTarget.elements.scope.value,
mandatory2fa: e.currentTarget.elements.mandatory2fa.checked,
};
if (this._id) {
roleData.name = this._id;
} else {
roleData.name = e.currentTarget.elements.name.value;
}
Meteor.call('authorization:saveRole', roleData, (error/* , result*/) => {
e.currentTarget.elements.save.value = oldBtnValue;
if (error) {
return handleError(error);
}
toastr.success(t('Saved'));
if (!this._id) {
return FlowRouter.go('admin-permissions-edit', {
name: roleData.name,
});
}
});
},
async 'submit #form-users'(e, instance) {
e.preventDefault();
if (e.currentTarget.elements.username.value.trim() === '') {
return toastr.error(t('Please_fill_a_username'));
}
const oldBtnValue = e.currentTarget.elements.add.value;
e.currentTarget.elements.add.value = t('Saving');
try {
await call('authorization:addUserToRole', FlowRouter.getParam('name'), e.currentTarget.elements.username.value, instance.searchRoom.get());
instance.usersInRole.set([]);
instance.state.set({
offset: 0,
cache: Date.now(),
});
toastr.success(t('User_added'));
e.currentTarget.reset();
} finally {
e.currentTarget.elements.add.value = oldBtnValue;
}
},
'submit #form-search-room'(e) {
return e.preventDefault();
},
'click .delete-role'(e/* , instance*/) {
e.preventDefault();
if (this.protected) {
return toastr.error(t('error-delete-protected-role'));
}
Meteor.call('authorization:deleteRole', this._id, function(error/* , result*/) {
if (error) {
return handleError(error);
}
toastr.success(t('Role_removed'));
FlowRouter.go('admin-permissions');
});
},
'click .load-more'(e, t) {
t.state.set('offset', t.state.get('offset') + PAGE_SIZE);
},
'autocompleteselect input[name=room]'(event, template, doc) {
template.searchRoom.set(doc._id);
},
});
Template.permissionsRole.onCreated(async function() {
this.state = new ReactiveDict({
offset: 0,
loading: false,
hasMore: true,
cache: 0,
});
this.searchRoom = new ReactiveVar();
this.searchUsername = new ReactiveVar();
this.usersInRole = new ReactiveVar([]);
});
Template.permissionsRole.onRendered(function() {
this.autorun(() => {
this.searchRoom.get();
this.usersInRole.set([]);
this.state.set({ offset: 0 });
});
this.autorun(() => {
this.state.get('cache');
loadUsers(this);
});
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -35,6 +35,7 @@ Meteor.methods({
method: 'authorization:deleteRole', method: 'authorization:deleteRole',
}); });
} }
const removed = Models.Roles.remove(role.name); const removed = Models.Roles.remove(role.name);
if (removed) { if (removed) {
rolesStreamer.emit('roles', { rolesStreamer.emit('roles', {

@ -0,0 +1,90 @@
import React from 'react';
import { Box, Field, FieldGroup, Button, Margins, Callout } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import RoleForm from './RoleForm';
import { useRoute } from '../../contexts/RouterContext';
import { useForm } from '../../hooks/useForm';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useRole } from './useRole';
const EditRolePageContainer = ({ _id }) => {
const t = useTranslation();
const role = useRole(_id);
if (!role) {
return <Callout type='danger'>{t('error-invalid-role')}</Callout>;
}
return <EditRolePage key={_id} data={role} />;
};
const EditRolePage = ({ data }) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const usersInRoleRouter = useRoute('admin-permissions');
const router = useRoute('admin-permissions');
const { values, handlers } = useForm({
name: data.name,
description: data.description || '',
scope: data.scope || 'Users',
mandatory2fa: !!data.mandatory2fa,
});
const saveRole = useMethod('authorization:saveRole');
const deleteRole = useMethod('authorization:deleteRole');
const handleManageUsers = useMutableCallback(() => {
usersInRoleRouter.push({
context: 'users-in-role',
_id: data.name,
});
});
const handleSave = useMutableCallback(async () => {
try {
await saveRole(values);
dispatchToastMessage({ type: 'success', message: t('Saved') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
const handleDelete = useMutableCallback(async () => {
try {
await deleteRole(data.name);
dispatchToastMessage({ type: 'success', message: t('Role_removed') });
router.push({});
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
return <Box w='full' alignSelf='center' mb='neg-x8'>
<Margins block='x8'>
<FieldGroup>
<RoleForm values={values} handlers={handlers} editing isProtected={data.protected}/>
<Field>
<Field.Row>
<Button primary w='full' onClick={handleSave}>{t('Save')}</Button>
</Field.Row>
</Field>
{!data.protected && <Field>
<Field.Row>
<Button danger w='full' onClick={handleDelete}>{t('Delete')}</Button>
</Field.Row>
</Field>}
<Field>
<Field.Row>
<Button w='full' onClick={handleManageUsers}>{t('Users_in_role')}</Button>
</Field.Row>
</Field>
</FieldGroup>
</Margins>
</Box>;
};
export default EditRolePageContainer;

@ -0,0 +1,46 @@
import React from 'react';
import { Box, FieldGroup, ButtonGroup, Button, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import RoleForm from './RoleForm';
import { useForm } from '../../hooks/useForm';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
const NewRolePage = () => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const { values, handlers } = useForm({
name: '',
description: '',
scope: 'Users',
mandatory2fa: false,
});
const saveRole = useMethod('authorization:saveRole');
const handleSave = useMutableCallback(async () => {
try {
await saveRole(values);
dispatchToastMessage({ type: 'success', message: t('Saved') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
return <Box w='full' alignSelf='center' mb='neg-x8'>
<Margins block='x8'>
<FieldGroup>
<RoleForm values={values} handlers={handlers}/>
</FieldGroup>
<ButtonGroup stretch w='full'>
<Button primary onClick={handleSave}>{t('Save')}</Button>
</ButtonGroup>
</Margins>
</Box>;
};
export default NewRolePage;

@ -0,0 +1,34 @@
import React from 'react';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRouteParameter, useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import VerticalBar from '../../components/basic/VerticalBar';
import NewRolePage from './NewRolePage';
import EditRolePage from './EditRolePage';
const PermissionsContextBar = () => {
const t = useTranslation();
const _id = useRouteParameter('_id');
const context = useRouteParameter('context');
const router = useRoute('admin-permissions');
const handleVerticalBarCloseButton = useMutableCallback(() => {
router.push({});
});
return (context && <VerticalBar className={'contextual-bar'}>
<VerticalBar.Header>
{context === 'new' && t('New_role')}
{context === 'edit' && t('Role_Editing')}
<VerticalBar.Close onClick={handleVerticalBarCloseButton} />
</VerticalBar.Header>
<VerticalBar.ScrollableContent>
{context === 'new' && <NewRolePage />}
{context === 'edit' && <EditRolePage _id={_id} />}
</VerticalBar.ScrollableContent>
</VerticalBar>) || null;
};
export default PermissionsContextBar;

@ -0,0 +1,16 @@
import React from 'react';
import { useRouteParameter } from '../../contexts/RouterContext';
import UsersInRole from './UsersInRole';
import PermissionsTable from './PermissionsTable';
const PermissionsRouter = () => {
const context = useRouteParameter('context');
if (context === 'users-in-role') {
return <UsersInRole />;
}
return <PermissionsTable />;
};
export default PermissionsRouter;

@ -0,0 +1,261 @@
import React, { useState, useCallback, useEffect } from 'react';
import { TextInput, Table, Margins, Box, Icon, CheckBox, Throbber, Tabs, Button } from '@rocket.chat/fuselage';
import { useMutableCallback, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { css } from '@rocket.chat/css-in-js';
import Page from '../../components/basic/Page';
import PermissionsContextBar from './PermissionsContextBar';
import { GenericTable } from '../../components/GenericTable';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useRoute } from '../../contexts/RouterContext';
import { ChatPermissions } from '../../../app/authorization/client/lib/ChatPermissions';
import { CONSTANTS, AuthorizationUtils } from '../../../app/authorization/lib';
import { Roles } from '../../../app/models/client';
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 usePermissionsAndRoles = (type = 'permissions', filter = '', limit = 25, skip = 0) => {
const getPermissions = useCallback(() => {
const filterRegExp = new RegExp(filter, 'i');
return ChatPermissions.find(
{
level: type === 'permissions' ? { $ne: CONSTANTS.SETTINGS_LEVEL } : CONSTANTS.SETTINGS_LEVEL,
_id: filterRegExp,
},
{
sort: {
_id: 1,
},
skip,
limit,
},
);
}, [filter, limit, skip, type]);
const getRoles = useMutableCallback(() => Roles.find().fetch(), []);
const permissions = useReactiveValue(getPermissions);
const roles = useReactiveValue(getRoles);
return [permissions.fetch(), permissions.count(false), roles];
};
const RoleCell = React.memo(({ grantedRoles = [], _id, description, onChange, lineHovered, permissionId }) => {
const [granted, setGranted] = useState(() => !!grantedRoles.includes(_id));
const [loading, setLoading] = useState(false);
const isRestrictedForRole = AuthorizationUtils.isPermissionRestrictedForRole(permissionId, _id);
const handleChange = useMutableCallback(async () => {
setLoading(true);
const result = await onChange(_id, granted);
setGranted(result);
setLoading(false);
});
const isDisabled = !!loading || !!isRestrictedForRole;
return <Table.Cell withTruncatedText>
<Margins inline='x2'>
<CheckBox checked={granted} onChange={handleChange} disabled={isDisabled}/>
{!loading && <Box display='inline' color='hint' invisible={!lineHovered}>
{description || _id}
</Box>}
{loading && <Throbber size='x12' display='inline-block'/>}
</Margins>
</Table.Cell>;
});
const getName = (t, permission) => {
if (permission.level === CONSTANTS.SETTINGS_LEVEL) {
let path = '';
if (permission.group) {
path = `${ t(permission.group) } > `;
}
if (permission.section) {
path = `${ path }${ t(permission.section) } > `;
}
return `${ path }${ t(permission.settingId) }`;
}
return t(permission._id);
};
const PermissionRow = React.memo(({ permission, t, roleList, onGrant, onRemove, ...props }) => {
const {
_id,
roles,
} = permission;
const [hovered, setHovered] = useState(false);
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`)}>{getName(t, permission)}</Table.Cell>
{roleList.map(({ _id, description }) => <RoleCell
key={_id}
_id={_id}
description={description}
grantedRoles={roles}
onChange={changeRole}
lineHovered={hovered}
permissionId={_id}
/>)}
</Table.Row>;
});
const RoleHeader = React.memo(({ router, _id, description, ...props }) => {
const onClick = useMutableCallback(() => {
router.push({
context: 'edit',
_id,
});
});
return <GenericTable.HeaderCell clickable pi='x4' p='x8' onClick={onClick} {...props}>
<Box
className={css`white-space: nowrap`}
pb='x8'
pi='x12'
mi='neg-x2'
borderStyle='solid'
borderWidth='x2'
borderRadius='x2'
borderColor='neutral-300'
>
<Margins inline='x2'>
<span>{description || _id}</span>
<Icon name='edit' size='x16'/>
</Margins>
</Box>
</GenericTable.HeaderCell>;
});
const FilterComponent = ({ onChange }) => {
const t = useTranslation();
const [filter, setFilter] = useState('');
const debouncedFilter = useDebouncedValue(filter, 500);
useEffect(() => {
onChange(debouncedFilter);
}, [debouncedFilter, onChange]);
const handleFilter = useMutableCallback(({ currentTarget: { value } }) => {
setFilter(value);
});
return <TextInput value={filter} onChange={handleFilter} placeholder={t('Search')} flexGrow={0}/>;
};
const PermissionsTable = () => {
const t = useTranslation();
const [filter, setFilter] = useState('');
const [type, setType] = useState('permissions');
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 handlePermissionsTab = useMutableCallback(() => {
if (type === 'permissions') { return; }
setType('permissions');
});
const handleSettingsTab = useMutableCallback(() => {
if (type === 'settings') { return; }
setType('settings');
});
const handleAdd = useMutableCallback(() => {
router.push({
context: 'new',
});
});
return <Page flexDirection='row'>
<Page>
<Page.Header title={t('Permissions')}>
<Button small square onClick={handleAdd}>
<Icon name='plus'/>
</Button>
</Page.Header>
<Margins blockEnd='x8'>
<Tabs>
<Tabs.Item selected={type === 'permissions'} onClick={handlePermissionsTab}>
{t('Permissions')}
</Tabs.Item>
<Tabs.Item selected={type === 'settings'} onClick={handleSettingsTab}>
{t('Settings')}
</Tabs.Item>
</Tabs>
</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, description }) => <RoleHeader key={_id} _id={_id} 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>
</Margins>
</Page.Content>
</Page>
<PermissionsContextBar />
</Page>;
};
export default PermissionsTable;

@ -0,0 +1,59 @@
import React, { useMemo } from 'react';
import { Box, Field, TextInput, Select, ToggleSwitch } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
const RoleForm = ({ values, handlers, className, editing = false, isProtected = false }) => {
const t = useTranslation();
const {
name,
description,
scope,
mandatory2fa,
} = values;
const {
handleName,
handleDescription,
handleScope,
handleMandatory2fa,
} = handlers;
const options = useMemo(() => [
['Users', t('Global')],
['Subscriptions', t('Rooms')],
], [t]);
return <>
<Field className={className}>
<Field.Label>{t('Role')}</Field.Label>
<Field.Row>
<TextInput disabled={editing} value={name} onChange={handleName} placeholder={t('Role')}/>
</Field.Row>
</Field>
<Field className={className}>
<Field.Label>{t('Description')}</Field.Label>
<Field.Row>
<TextInput value={description} onChange={handleDescription} placeholder={t('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')}/>
</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}/>
</Field.Row>
</Box>
</Field>
</>;
};
export default RoleForm;

@ -0,0 +1,95 @@
import React, { useState, useRef } from 'react';
import { Box, Field, Margins, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import Page from '../../components/basic/Page';
import UsersInRoleTable from './UsersInRoleTable';
import RoomAutoComplete from '../../components/basic/RoomAutoComplete';
import { UserAutoComplete } from '../../components/basic/AutoComplete';
import { useRoute, useRouteParameter } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useRole } from './useRole';
const UsersInRolePageContainer = () => {
const _id = useRouteParameter('_id');
const role = useRole(_id);
if (!role) {
return null;
}
return <UsersInRolePage data={role} />;
};
const UsersInRolePage = ({ data }) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const reload = useRef();
const [user, setUser] = useState();
const [rid, setRid] = useState();
const { name } = data;
const router = useRoute('admin-permissions');
const addUser = useMethod('authorization:addUserToRole');
const handleReturn = useMutableCallback(() => {
router.push({
context: 'edit',
_id: name,
});
});
const handleAdd = useMutableCallback(async () => {
try {
await addUser(name, user, rid);
dispatchToastMessage({ type: 'success', message: t('User_added') });
setUser();
reload.current();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
return <Page>
<Page.Header title={`${ t('Users_in_role') } "${ name }"`}>
<ButtonGroup>
{/* <Button primary onClick={handleSave}>{t('Save')}</Button> */}
<Button onClick={handleReturn}>{t('Back')}</Button>
</ButtonGroup>
</Page.Header>
<Page.Content>
<Box display='flex' flexDirection='row' w='full' mi='neg-x4'>
<Margins inline='x4'>
{data.scope !== 'Users' && <Field>
<Field.Label>{t('Choose_a_room')}</Field.Label>
<Field.Row>
<RoomAutoComplete value={rid} onChange={setRid} placeholder={t('User')}/>
</Field.Row>
</Field>}
<Field>
<Field.Label>{t('Add_user')}</Field.Label>
<Field.Row>
<UserAutoComplete value={user} onChange={setUser} placeholder={t('User')}/>
</Field.Row>
</Field>
<Box display='flex' flexGrow={1} flexDirection='column' justifyContent='flex-end'>
<Button primary onClick={handleAdd}>{t('Add')}</Button>
</Box>
</Margins>
</Box>
<Margins blockStart='x8'>
{(data.scope === 'Users' || rid) && <UsersInRoleTable reloadRef={reload} scope={data.scope} rid={rid} roleName={data.name}/>}
{data.scope !== 'Users' && !rid && <Callout type='info'>{t('Select_a_room')}</Callout>}
</Margins>
</Page.Content>
</Page>;
};
export default UsersInRolePageContainer;

@ -0,0 +1,118 @@
import React, { useState, useMemo } from 'react';
import { Box, Table, Button, Icon } from '@rocket.chat/fuselage';
import { useMutableCallback, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import UserAvatar from '../../components/basic/avatar/UserAvatar';
import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useMethod } from '../../contexts/ServerContext';
import { GenericTable } from '../../components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useSetModal } from '../../contexts/ModalContext';
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental';
const UserRow = React.memo(({ _id, username, name, avatarETag, emails, onRemove }) => {
const email = emails?.find(({ address }) => !!address).address;
const handleRemove = useMutableCallback(() => {
onRemove(username);
});
return <Table.Row key={_id} tabIndex={0} role='link'>
<Table.Cell withTruncatedText>
<Box display='flex' alignItems='center'>
<UserAvatar size='x40' title={username} username={username} etag={avatarETag}/>
<Box display='flex' withTruncatedText mi='x8'>
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText>
<Box fontScale='p2' withTruncatedText color='default'>{name || username}</Box>
{name && <Box fontScale='p1' color='hint' withTruncatedText> {`@${ username }`} </Box>}
</Box>
</Box>
</Box>
</Table.Cell>
<Table.Cell withTruncatedText>{email}</Table.Cell>
<Table.Cell withTruncatedText>
<Button small square danger onClick={handleRemove}>
<Icon name='trash' size='x20' />
</Button>
</Table.Cell>
</Table.Row>;
});
export function UsersInRoleTable({ data, reload, roleName, total, params, setParams, rid }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const setModal = useSetModal();
const removeUser = useMethod('authorization:removeUserFromRole');
const closeModal = () => setModal();
const onRemove = useMutableCallback((username) => {
const remove = async () => {
try {
await removeUser(roleName, username, rid);
dispatchToastMessage({ type: 'success', message: t('User_removed') });
} catch (error) {
dispatchToastMessage({ type: 'erroor', message: error });
}
closeModal();
reload();
};
setModal(<DeleteWarningModal onCancel={closeModal} onDelete={remove}>
{t('The_user_s_will_be_removed_from_role_s', username, roleName)}
</DeleteWarningModal>);
});
return <GenericTable
header={<>
<GenericTable.HeaderCell >
{t('Name')}
</GenericTable.HeaderCell>
<GenericTable.HeaderCell>
{t('Email')}
</GenericTable.HeaderCell>
<GenericTable.HeaderCell w='x80'>
</GenericTable.HeaderCell>
</>}
results={data}
params={params}
setParams={setParams}
total={total}
>
{(props) => <UserRow onRemove={onRemove} key={props._id} {...props}/>}
</GenericTable>;
}
const UsersInRoleTableContainer = ({ rid, roleName, reloadRef }) => {
const [params, setParams] = useState({ current: 0, itemsPerPage: 25 });
const debouncedParams = useDebouncedValue(params, 500);
const query = useMemo(() => ({
roomId: rid,
role: roleName,
...debouncedParams.itemsPerPage && { count: debouncedParams.itemsPerPage },
...debouncedParams.current && { offset: debouncedParams.current },
}), [debouncedParams, rid, roleName]);
const { data = {}, reload } = useEndpointDataExperimental('roles.getUsersInRole', query);
reloadRef.current = reload;
const tableData = data?.users || [];
return <UsersInRoleTable
data={tableData}
total={data?.total}
reload={reload}
params={params}
setParams={setParams}
roleName={roleName}
rid={rid}
/>;
};
export default UsersInRoleTableContainer;

@ -0,0 +1,6 @@
import { useCallback } from 'react';
import { Roles } from '../../../app/models/client';
import { useReactiveValue } from '../../hooks/useReactiveValue';
export const useRole = (_id) => useReactiveValue(useCallback(() => Roles.findOne({ _id }), [_id]));

@ -114,6 +114,11 @@ registerAdminRoute('/federation-dashboard', {
lazyRouteComponent: () => import('./federationDashboard/FederationDashboardRoute'), lazyRouteComponent: () => import('./federationDashboard/FederationDashboardRoute'),
}); });
registerAdminRoute('/permissions/:context?/:_id?', {
name: 'admin-permissions',
lazyRouteComponent: () => import('./permissions/PermissionsRouter'),
});
Meteor.startup(() => { Meteor.startup(() => {
registerAdminRoute('/:group+', { registerAdminRoute('/:group+', {
name: 'admin', name: 'admin',

@ -3,7 +3,7 @@ import React from 'react';
import { useTranslation } from '../contexts/TranslationContext'; import { useTranslation } from '../contexts/TranslationContext';
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => { const DeleteWarningModal = ({ onDelete, onCancel, children, ...props }) => {
const t = useTranslation(); const t = useTranslation();
return <Modal {...props}> return <Modal {...props}>
<Modal.Header> <Modal.Header>
@ -11,6 +11,9 @@ const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
<Modal.Title>{t('Are_you_sure')}</Modal.Title> <Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/> <Modal.Close onClick={onCancel}/>
</Modal.Header> </Modal.Header>
<Modal.Content>
{children}
</Modal.Content>
<Modal.Footer> <Modal.Footer>
<ButtonGroup align='end'> <ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button> <Button ghost onClick={onCancel}>{t('Cancel')}</Button>

@ -39,6 +39,7 @@ const LoadingRow = ({ cols }) => <Table.Row>
export const GenericTable = forwardRef(function GenericTable({ export const GenericTable = forwardRef(function GenericTable({
children, children,
results, results,
fixed = true,
total, total,
renderRow: RenderRow, renderRow: RenderRow,
header, header,
@ -79,7 +80,7 @@ export const GenericTable = forwardRef(function GenericTable({
: <> : <>
<Scrollable> <Scrollable>
<Box mi='neg-x24' pi='x24' flexGrow={1} ref={ref}> <Box mi='neg-x24' pi='x24' flexGrow={1} ref={ref}>
<Table fixed sticky> <Table fixed={fixed} sticky>
{header && <Table.Head> {header && <Table.Head>
<Table.Row> <Table.Row>
{header} {header}

@ -0,0 +1,32 @@
import React, { useMemo, useState } from 'react';
import { AutoComplete, Option, Options } from '@rocket.chat/fuselage';
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental';
import RoomAvatar from './avatar/RoomAvatar';
const query = (term = '') => ({ selector: JSON.stringify({ name: term }) });
const Avatar = ({ value, type, avatarETag, ...props }) => <RoomAvatar size={Options.AvatarSize} room={{ type, _id: value, avatarETag }} {...props} />;
const RoomAutoComplete = React.memo((props) => {
const [filter, setFilter] = useState('');
const { data } = useEndpointDataExperimental('rooms.autocomplete.channelAndPrivate', useMemo(() => query(filter), [filter]));
const options = useMemo(() => (data && data.items.map(({ name, _id, avatarETag, t }) => ({
value: _id,
label: { name, avatarETag, type: t },
}))) || [], [data]);
return <AutoComplete
{...props}
filter={filter}
setFilter={setFilter}
renderSelected={({
value,
label,
}) => <><RoomAvatar size='x20' room={{ type: label?.type || 'c', _id: value, ...label }} /> {label?.name}</>}
renderItem={({ value, label, ...props }) => <Option key={value} {...props} label={label.name} avatar={<Avatar value={value} {...label} />} />}
options={ options }
/>;
});
export default RoomAutoComplete;

@ -15,7 +15,7 @@ import AgentInfo from './AgentInfo';
import UserAvatar from '../../components/basic/avatar/UserAvatar'; import UserAvatar from '../../components/basic/avatar/UserAvatar';
import { useRouteParameter, useRoute } from '../../contexts/RouterContext'; import { useRouteParameter, useRoute } from '../../contexts/RouterContext';
import VerticalBar from '../../components/basic/VerticalBar'; import VerticalBar from '../../components/basic/VerticalBar';
import DeleteWarningModal from '../DeleteWarningModal'; import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useSetModal } from '../../contexts/ModalContext'; import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';

@ -11,7 +11,7 @@ import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperime
import { usePermission } from '../../contexts/AuthorizationContext'; import { usePermission } from '../../contexts/AuthorizationContext';
import { GenericTable } from '../../components/GenericTable'; import { GenericTable } from '../../components/GenericTable';
import { useMethod } from '../../contexts/ServerContext'; import { useMethod } from '../../contexts/ServerContext';
import DeleteWarningModal from '../DeleteWarningModal'; import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useSetModal } from '../../contexts/ModalContext'; import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { AutoCompleteDepartment } from '../../components/basic/AutoCompleteDepartment'; import { AutoCompleteDepartment } from '../../components/basic/AutoCompleteDepartment';

@ -14,7 +14,7 @@ import { useMethod } from '../../contexts/ServerContext';
import { usePermission } from '../../contexts/AuthorizationContext'; import { usePermission } from '../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../../components/NotAuthorizedPage'; import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import CurrentChatsPage from './CurrentChatsPage'; import CurrentChatsPage from './CurrentChatsPage';
import DeleteWarningModal from '../DeleteWarningModal'; import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useSetModal } from '../../contexts/ModalContext'; import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';

@ -8,7 +8,7 @@ import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext'; import { useTranslation } from '../../contexts/TranslationContext';
import { useResizeInlineBreakpoint } from '../../hooks/useResizeInlineBreakpoint'; import { useResizeInlineBreakpoint } from '../../hooks/useResizeInlineBreakpoint';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental'; import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import DeleteWarningModal from '../DeleteWarningModal'; import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useSetModal } from '../../contexts/ModalContext'; import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useMethod } from '../../contexts/ServerContext'; import { useMethod } from '../../contexts/ServerContext';

@ -3,7 +3,7 @@ import { useMutableCallback, Button } from '@rocket.chat/fuselage-hooks';
import React, { useState, memo, useMemo } from 'react'; import React, { useState, memo, useMemo } from 'react';
import GenericTable from '../../components/GenericTable'; import GenericTable from '../../components/GenericTable';
import DeleteWarningModal from '../DeleteWarningModal'; import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useRoute } from '../../contexts/RouterContext'; import { useRoute } from '../../contexts/RouterContext';
import { useSetModal } from '../../contexts/ModalContext'; import { useSetModal } from '../../contexts/ModalContext';
import { useMethod } from '../../contexts/ServerContext'; import { useMethod } from '../../contexts/ServerContext';

@ -7,7 +7,7 @@ import { useRoute } from '../../../client/contexts/RouterContext';
import { useTranslation } from '../../../client/contexts/TranslationContext'; import { useTranslation } from '../../../client/contexts/TranslationContext';
import { useResizeInlineBreakpoint } from '../../../client/hooks/useResizeInlineBreakpoint'; import { useResizeInlineBreakpoint } from '../../../client/hooks/useResizeInlineBreakpoint';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../client/hooks/useEndpointDataExperimental'; import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../client/hooks/useEndpointDataExperimental';
import DeleteWarningModal from '../../../client/omnichannel/DeleteWarningModal'; import DeleteWarningModal from '../../../client/components/DeleteWarningModal';
import { useSetModal } from '../../../client/contexts/ModalContext'; import { useSetModal } from '../../../client/contexts/ModalContext';
import { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext';
import { useMethod } from '../../../client/contexts/ServerContext'; import { useMethod } from '../../../client/contexts/ServerContext';

@ -3,7 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState, memo, useEffect } from 'react'; import React, { useState, memo, useEffect } from 'react';
import GenericTable from '../../../client/components/GenericTable'; import GenericTable from '../../../client/components/GenericTable';
import DeleteWarningModal from '../../../client/omnichannel/DeleteWarningModal'; import DeleteWarningModal from '../../../client/components/DeleteWarningModal';
import { useSetModal } from '../../../client/contexts/ModalContext'; import { useSetModal } from '../../../client/contexts/ModalContext';
import { useMethod } from '../../../client/contexts/ServerContext'; import { useMethod } from '../../../client/contexts/ServerContext';
import { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext';

@ -14,7 +14,7 @@ import { useRouteParameter, useRoute } from '../../../../client/contexts/RouterC
import VerticalBar from '../../../../client/components/basic/VerticalBar'; import VerticalBar from '../../../../client/components/basic/VerticalBar';
import PrioritiesPage from './PrioritiesPage'; import PrioritiesPage from './PrioritiesPage';
import { PriorityEditWithData, PriorityNew } from './EditPriority'; import { PriorityEditWithData, PriorityNew } from './EditPriority';
import DeleteWarningModal from '../../../../client/omnichannel/DeleteWarningModal'; import DeleteWarningModal from '../../../../client/components/DeleteWarningModal';
import { useSetModal } from '../../../../client/contexts/ModalContext'; import { useSetModal } from '../../../../client/contexts/ModalContext';
import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';

@ -14,7 +14,7 @@ import { useRouteParameter, useRoute } from '../../../../client/contexts/RouterC
import VerticalBar from '../../../../client/components/basic/VerticalBar'; import VerticalBar from '../../../../client/components/basic/VerticalBar';
import TagsPage from './TagsPage'; import TagsPage from './TagsPage';
import { TagEditWithData, TagNew } from './EditTag'; import { TagEditWithData, TagNew } from './EditTag';
import DeleteWarningModal from '../../../../client/omnichannel/DeleteWarningModal'; import DeleteWarningModal from '../../../../client/components/DeleteWarningModal';
import { useSetModal } from '../../../../client/contexts/ModalContext'; import { useSetModal } from '../../../../client/contexts/ModalContext';
import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';

@ -14,7 +14,7 @@ import { useRouteParameter, useRoute } from '../../../../client/contexts/RouterC
import VerticalBar from '../../../../client/components/basic/VerticalBar'; import VerticalBar from '../../../../client/components/basic/VerticalBar';
import UnitsPage from './UnitsPage'; import UnitsPage from './UnitsPage';
import { UnitEditWithData, UnitNew } from './EditUnit'; import { UnitEditWithData, UnitNew } from './EditUnit';
import DeleteWarningModal from '../../../../client/omnichannel/DeleteWarningModal'; import DeleteWarningModal from '../../../../client/components/DeleteWarningModal';
import { useSetModal } from '../../../../client/contexts/ModalContext'; import { useSetModal } from '../../../../client/contexts/ModalContext';
import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';

@ -3190,6 +3190,7 @@
"Secret_token": "Secret Token", "Secret_token": "Secret Token",
"Security": "Security", "Security": "Security",
"Select_a_department": "Select a department", "Select_a_department": "Select a department",
"Select_a_room": "Select a room",
"Select_a_user": "Select a user", "Select_a_user": "Select a user",
"Select_at_least_two_users": "Select at least two users", "Select_at_least_two_users": "Select at least two users",
"Select_an_avatar": "Select an avatar", "Select_an_avatar": "Select an avatar",

Loading…
Cancel
Save