diff --git a/app/api/server/v1/roles.js b/app/api/server/v1/roles.js
index 7772f80e29d..dbf967b8483 100644
--- a/app/api/server/v1/roles.js
+++ b/app/api/server/v1/roles.js
@@ -83,6 +83,7 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, {
name: 1,
username: 1,
emails: 1,
+ avatarETag: 1,
};
if (!role) {
@@ -99,7 +100,7 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, {
sort: { username: 1 },
skip: offset,
fields,
- }).fetch();
- return API.v1.success({ users });
+ });
+ return API.v1.success({ users: users.fetch(), total: users.count() });
},
});
diff --git a/app/authorization/client/index.js b/app/authorization/client/index.js
index ca382bacc82..bf484b06455 100644
--- a/app/authorization/client/index.js
+++ b/app/authorization/client/index.js
@@ -3,9 +3,7 @@ import { hasRole } from './hasRole';
import { AuthorizationUtils } from '../lib/AuthorizationUtils';
import './usersNameChanged';
import './requiresPermission.html';
-import './route';
import './startup';
-import './stylesheets/permissions.css';
export {
hasAllPermission,
diff --git a/app/authorization/client/route.js b/app/authorization/client/route.js
deleted file mode 100644
index 2a09c78da6d..00000000000
--- a/app/authorization/client/route.js
+++ /dev/null
@@ -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',
- });
- },
-});
diff --git a/app/authorization/client/startup.js b/app/authorization/client/startup.js
index 2dd50b95fa2..3f4c4637a0b 100644
--- a/app/authorization/client/startup.js
+++ b/app/authorization/client/startup.js
@@ -27,7 +27,9 @@ Meteor.startup(() => {
delete role.type;
Roles.upsert({ _id: role.name }, role);
},
- removed: (role) => Roles.remove({ _id: role.name }),
+ removed: (role) => {
+ Roles.remove({ _id: role.name });
+ },
};
Tracker.autorun((c) => {
diff --git a/app/authorization/client/stylesheets/permissions.css b/app/authorization/client/stylesheets/permissions.css
deleted file mode 100644
index 1561d2ebf53..00000000000
--- a/app/authorization/client/stylesheets/permissions.css
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/app/authorization/client/views/index.js b/app/authorization/client/views/index.js
deleted file mode 100644
index ba54dffa5c0..00000000000
--- a/app/authorization/client/views/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import './permissions.html';
-import './permissions';
-import './permissionsRole.html';
-import './permissionsRole';
diff --git a/app/authorization/client/views/permissions.html b/app/authorization/client/views/permissions.html
deleted file mode 100644
index d332e471849..00000000000
--- a/app/authorization/client/views/permissions.html
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
- |
- {{#each role in allRoles}}
-
-
-
-
- {{role._id}}
-
-
-
-
-
-
- |
- {{/each}}
-
-
-
- {{#each permission in permissions}}
-
- {{permissionName permission}} [ID: {{permission._id}}] |
- {{#each role in allRoles}}
-
- {{#if isRolePermissionEnabled role permission}}
-
- {{/if}}
- |
- {{else}}
-
- | {{_ "No_results_found_for"}} {{filter}} |
-
- {{/each}}
-
- {{/each}}
-
-
-
-
-
-
-
- {{#if hasPermission}}
- {{# header sectionName=pageTitle}}
-
- {{/header}}
-
-
- {{>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}}
-
- {{#if i18nPageTitle}} {{> header sectionName=i18nPageTitle}} {{else}} {{> header sectionName=pageTitle}}
- {{/if}}
- {{_ "Not_authorized"}}
-
- {{/if}}
-
-
-
diff --git a/app/authorization/client/views/permissions.js b/app/authorization/client/views/permissions.js
deleted file mode 100644
index a01a9d182fa..00000000000
--- a/app/authorization/client/views/permissions.js
+++ /dev/null
@@ -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();
- });
-});
diff --git a/app/authorization/client/views/permissionsRole.html b/app/authorization/client/views/permissionsRole.html
deleted file mode 100644
index 74579e16fe5..00000000000
--- a/app/authorization/client/views/permissionsRole.html
+++ /dev/null
@@ -1,110 +0,0 @@
-
-
- {{#if hasPermission}}
- {{#with role}}
-
- {{/with}}
- {{#if editing}}
-
{{_ "Users_in_role"}}
- {{#if $eq role.scope 'Subscriptions'}}
-
- {{/if}}
- {{#if $or ($eq role.scope 'Users') searchRoom}}
-
-
- {{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}}
-
-
- {{_ "Name"}} |
- {{_ "Username"}} |
- {{_ "Email"}} |
- |
-
-
-
- {{#unless hasUsers}}
-
- | {{_ "There_are_no_users_in_this_role"}} |
-
- {{/unless}}
- {{#each userInRole}}
-
-
-
- {{> avatar username=username}}
-
- {{name}}
-
-
- |
- {{username}} |
- {{emailAddress}} |
- |
-
- {{/each}}
-
- {{/table}}
- {{#if hasMore}}
-
- {{/if}}
-
- {{/if}}
- {{/if}}
- {{else}}
- {{_ "Not_authorized"}}
- {{/if}}
-
-
diff --git a/app/authorization/client/views/permissionsRole.js b/app/authorization/client/views/permissionsRole.js
deleted file mode 100644
index 4e16cd666f5..00000000000
--- a/app/authorization/client/views/permissionsRole.js
+++ /dev/null
@@ -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();
- });
-});
diff --git a/app/authorization/server/methods/deleteRole.js b/app/authorization/server/methods/deleteRole.js
index 0b42263c23f..56ab719fe98 100644
--- a/app/authorization/server/methods/deleteRole.js
+++ b/app/authorization/server/methods/deleteRole.js
@@ -35,6 +35,7 @@ Meteor.methods({
method: 'authorization:deleteRole',
});
}
+
const removed = Models.Roles.remove(role.name);
if (removed) {
rolesStreamer.emit('roles', {
diff --git a/client/admin/permissions/EditRolePage.js b/client/admin/permissions/EditRolePage.js
new file mode 100644
index 00000000000..5c98042b6a4
--- /dev/null
+++ b/client/admin/permissions/EditRolePage.js
@@ -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 {t('error-invalid-role')};
+ }
+
+ return ;
+};
+
+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
+
+
+
+
+
+
+
+
+ {!data.protected &&
+
+
+
+ }
+
+
+
+
+
+
+
+ ;
+};
+
+export default EditRolePageContainer;
diff --git a/client/admin/permissions/NewRolePage.js b/client/admin/permissions/NewRolePage.js
new file mode 100644
index 00000000000..7c17c0a1fbe
--- /dev/null
+++ b/client/admin/permissions/NewRolePage.js
@@ -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
+
+
+
+
+
+
+
+
+ ;
+};
+
+export default NewRolePage;
diff --git a/client/admin/permissions/PermissionsContextBar.js b/client/admin/permissions/PermissionsContextBar.js
new file mode 100644
index 00000000000..41225f64bd8
--- /dev/null
+++ b/client/admin/permissions/PermissionsContextBar.js
@@ -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 &&
+
+ {context === 'new' && t('New_role')}
+ {context === 'edit' && t('Role_Editing')}
+
+
+
+ {context === 'new' && }
+ {context === 'edit' && }
+
+ ) || null;
+};
+
+export default PermissionsContextBar;
diff --git a/client/admin/permissions/PermissionsRouter.js b/client/admin/permissions/PermissionsRouter.js
new file mode 100644
index 00000000000..5947a9e9b41
--- /dev/null
+++ b/client/admin/permissions/PermissionsRouter.js
@@ -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 ;
+ }
+
+ return ;
+};
+
+export default PermissionsRouter;
diff --git a/client/admin/permissions/PermissionsTable.js b/client/admin/permissions/PermissionsTable.js
new file mode 100644
index 00000000000..d6f41a688d8
--- /dev/null
+++ b/client/admin/permissions/PermissionsTable.js
@@ -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
+
+
+ {!loading &&
+ {description || _id}
+ }
+ {loading && }
+
+ ;
+});
+
+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
+ {getName(t, permission)}
+ {roleList.map(({ _id, description }) => )}
+ ;
+});
+
+const RoleHeader = React.memo(({ router, _id, description, ...props }) => {
+ const onClick = useMutableCallback(() => {
+ router.push({
+ context: 'edit',
+ _id,
+ });
+ });
+
+ return
+
+
+ {description || _id}
+
+
+
+ ;
+});
+
+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 ;
+};
+
+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
+
+
+
+
+
+
+
+ {t('Permissions')}
+
+
+ {t('Settings')}
+
+
+
+
+
+
+
+ {t('Name')}
+ {roleList.map(({ _id, description }) => )}
+ >}
+ total={total}
+ results={permissions}
+ params={params}
+ setParams={handleParams}
+ fixed={false}
+ >
+ {useCallback((permission) => , [grantRole, removeRole, roleList, t])}
+
+
+
+
+
+ ;
+};
+
+export default PermissionsTable;
diff --git a/client/admin/permissions/RoleForm.js b/client/admin/permissions/RoleForm.js
new file mode 100644
index 00000000000..d91e99e5df9
--- /dev/null
+++ b/client/admin/permissions/RoleForm.js
@@ -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 <>
+
+ {t('Role')}
+
+
+
+
+
+ {t('Description')}
+
+
+
+ {('Leave the description field blank if you dont want to show the role')}
+
+
+ {t('Scope')}
+
+
+
+
+
+ {t('Users must use Two Factor Authentication')}
+
+
+
+
+
+ >;
+};
+
+export default RoleForm;
diff --git a/client/admin/permissions/UsersInRole.js b/client/admin/permissions/UsersInRole.js
new file mode 100644
index 00000000000..237a82b55ed
--- /dev/null
+++ b/client/admin/permissions/UsersInRole.js
@@ -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 ;
+};
+
+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
+
+
+ {/* */}
+
+
+
+
+
+
+ {data.scope !== 'Users' &&
+ {t('Choose_a_room')}
+
+
+
+ }
+
+ {t('Add_user')}
+
+
+
+
+
+
+
+
+
+
+ {(data.scope === 'Users' || rid) && }
+ {data.scope !== 'Users' && !rid && {t('Select_a_room')}}
+
+
+ ;
+};
+
+export default UsersInRolePageContainer;
diff --git a/client/admin/permissions/UsersInRoleTable.js b/client/admin/permissions/UsersInRoleTable.js
new file mode 100644
index 00000000000..df255667d7d
--- /dev/null
+++ b/client/admin/permissions/UsersInRoleTable.js
@@ -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
+
+
+
+
+
+ {name || username}
+ {name && {`@${ username }`} }
+
+
+
+
+ {email}
+
+
+
+ ;
+});
+
+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(
+ {t('The_user_s_will_be_removed_from_role_s', username, roleName)}
+ );
+ });
+
+ return
+
+ {t('Name')}
+
+
+ {t('Email')}
+
+
+
+ >}
+ results={data}
+ params={params}
+ setParams={setParams}
+ total={total}
+ >
+ {(props) => }
+ ;
+}
+
+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 ;
+};
+
+export default UsersInRoleTableContainer;
diff --git a/client/admin/permissions/useRole.js b/client/admin/permissions/useRole.js
new file mode 100644
index 00000000000..4e8ed2d689b
--- /dev/null
+++ b/client/admin/permissions/useRole.js
@@ -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]));
diff --git a/client/admin/routes.js b/client/admin/routes.js
index d8a13b7990f..3ad629b07bc 100644
--- a/client/admin/routes.js
+++ b/client/admin/routes.js
@@ -114,6 +114,11 @@ registerAdminRoute('/federation-dashboard', {
lazyRouteComponent: () => import('./federationDashboard/FederationDashboardRoute'),
});
+registerAdminRoute('/permissions/:context?/:_id?', {
+ name: 'admin-permissions',
+ lazyRouteComponent: () => import('./permissions/PermissionsRouter'),
+});
+
Meteor.startup(() => {
registerAdminRoute('/:group+', {
name: 'admin',
diff --git a/client/omnichannel/DeleteWarningModal.js b/client/components/DeleteWarningModal.js
similarity index 83%
rename from client/omnichannel/DeleteWarningModal.js
rename to client/components/DeleteWarningModal.js
index 6e8ba1d06e7..a82bf473367 100644
--- a/client/omnichannel/DeleteWarningModal.js
+++ b/client/components/DeleteWarningModal.js
@@ -3,7 +3,7 @@ import React from 'react';
import { useTranslation } from '../contexts/TranslationContext';
-const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
+const DeleteWarningModal = ({ onDelete, onCancel, children, ...props }) => {
const t = useTranslation();
return
@@ -11,6 +11,9 @@ const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
{t('Are_you_sure')}
+
+ {children}
+
diff --git a/client/components/GenericTable.js b/client/components/GenericTable.js
index c8398dbb450..6e2fa6844f6 100644
--- a/client/components/GenericTable.js
+++ b/client/components/GenericTable.js
@@ -39,6 +39,7 @@ const LoadingRow = ({ cols }) =>
export const GenericTable = forwardRef(function GenericTable({
children,
results,
+ fixed = true,
total,
renderRow: RenderRow,
header,
@@ -79,7 +80,7 @@ export const GenericTable = forwardRef(function GenericTable({
: <>
-
+
{header &&
{header}
diff --git a/client/components/basic/RoomAutoComplete.js b/client/components/basic/RoomAutoComplete.js
new file mode 100644
index 00000000000..65503c6f907
--- /dev/null
+++ b/client/components/basic/RoomAutoComplete.js
@@ -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 }) => ;
+
+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 <> {label?.name}>}
+ renderItem={({ value, label, ...props }) => } />}
+ options={ options }
+ />;
+});
+
+export default RoomAutoComplete;
diff --git a/client/omnichannel/agents/AgentsRoute.js b/client/omnichannel/agents/AgentsRoute.js
index acac7a3cd65..6192d225ff0 100644
--- a/client/omnichannel/agents/AgentsRoute.js
+++ b/client/omnichannel/agents/AgentsRoute.js
@@ -15,7 +15,7 @@ import AgentInfo from './AgentInfo';
import UserAvatar from '../../components/basic/avatar/UserAvatar';
import { useRouteParameter, useRoute } from '../../contexts/RouterContext';
import VerticalBar from '../../components/basic/VerticalBar';
-import DeleteWarningModal from '../DeleteWarningModal';
+import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
diff --git a/client/omnichannel/currentChats/CurrentChatsPage.js b/client/omnichannel/currentChats/CurrentChatsPage.js
index 01cae3222db..c9a3e887141 100644
--- a/client/omnichannel/currentChats/CurrentChatsPage.js
+++ b/client/omnichannel/currentChats/CurrentChatsPage.js
@@ -11,7 +11,7 @@ import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperime
import { usePermission } from '../../contexts/AuthorizationContext';
import { GenericTable } from '../../components/GenericTable';
import { useMethod } from '../../contexts/ServerContext';
-import DeleteWarningModal from '../DeleteWarningModal';
+import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { AutoCompleteDepartment } from '../../components/basic/AutoCompleteDepartment';
diff --git a/client/omnichannel/currentChats/CurrentChatsRoute.js b/client/omnichannel/currentChats/CurrentChatsRoute.js
index d4ff9f77ef9..c204c27cf90 100644
--- a/client/omnichannel/currentChats/CurrentChatsRoute.js
+++ b/client/omnichannel/currentChats/CurrentChatsRoute.js
@@ -14,7 +14,7 @@ import { useMethod } from '../../contexts/ServerContext';
import { usePermission } from '../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../../components/NotAuthorizedPage';
import CurrentChatsPage from './CurrentChatsPage';
-import DeleteWarningModal from '../DeleteWarningModal';
+import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
diff --git a/client/omnichannel/customFields/CustomFieldsTable.js b/client/omnichannel/customFields/CustomFieldsTable.js
index 1ad532bfef7..733772c5af3 100644
--- a/client/omnichannel/customFields/CustomFieldsTable.js
+++ b/client/omnichannel/customFields/CustomFieldsTable.js
@@ -8,7 +8,7 @@ import { useRoute } from '../../contexts/RouterContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useResizeInlineBreakpoint } from '../../hooks/useResizeInlineBreakpoint';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
-import DeleteWarningModal from '../DeleteWarningModal';
+import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useSetModal } from '../../contexts/ModalContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useMethod } from '../../contexts/ServerContext';
diff --git a/client/omnichannel/triggers/TriggersTable.js b/client/omnichannel/triggers/TriggersTable.js
index cdf8c7e2891..f59f0900faa 100644
--- a/client/omnichannel/triggers/TriggersTable.js
+++ b/client/omnichannel/triggers/TriggersTable.js
@@ -3,7 +3,7 @@ import { useMutableCallback, Button } from '@rocket.chat/fuselage-hooks';
import React, { useState, memo, useMemo } from 'react';
import GenericTable from '../../components/GenericTable';
-import DeleteWarningModal from '../DeleteWarningModal';
+import DeleteWarningModal from '../../components/DeleteWarningModal';
import { useRoute } from '../../contexts/RouterContext';
import { useSetModal } from '../../contexts/ModalContext';
import { useMethod } from '../../contexts/ServerContext';
diff --git a/ee/client/omnichannel/BusinessHoursTable.js b/ee/client/omnichannel/BusinessHoursTable.js
index 86baa7e377b..05ddaee7095 100644
--- a/ee/client/omnichannel/BusinessHoursTable.js
+++ b/ee/client/omnichannel/BusinessHoursTable.js
@@ -7,7 +7,7 @@ import { useRoute } from '../../../client/contexts/RouterContext';
import { useTranslation } from '../../../client/contexts/TranslationContext';
import { useResizeInlineBreakpoint } from '../../../client/hooks/useResizeInlineBreakpoint';
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 { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext';
import { useMethod } from '../../../client/contexts/ServerContext';
diff --git a/ee/client/omnichannel/MonitorsTable.js b/ee/client/omnichannel/MonitorsTable.js
index 285118faadd..12b6a9b5cc6 100644
--- a/ee/client/omnichannel/MonitorsTable.js
+++ b/ee/client/omnichannel/MonitorsTable.js
@@ -3,7 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState, memo, useEffect } from 'react';
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 { useMethod } from '../../../client/contexts/ServerContext';
import { useToastMessageDispatch } from '../../../client/contexts/ToastMessagesContext';
diff --git a/ee/client/omnichannel/priorities/PrioritiesRoute.js b/ee/client/omnichannel/priorities/PrioritiesRoute.js
index 3035dca2d89..1b7507c424e 100644
--- a/ee/client/omnichannel/priorities/PrioritiesRoute.js
+++ b/ee/client/omnichannel/priorities/PrioritiesRoute.js
@@ -14,7 +14,7 @@ import { useRouteParameter, useRoute } from '../../../../client/contexts/RouterC
import VerticalBar from '../../../../client/components/basic/VerticalBar';
import PrioritiesPage from './PrioritiesPage';
import { PriorityEditWithData, PriorityNew } from './EditPriority';
-import DeleteWarningModal from '../../../../client/omnichannel/DeleteWarningModal';
+import DeleteWarningModal from '../../../../client/components/DeleteWarningModal';
import { useSetModal } from '../../../../client/contexts/ModalContext';
import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';
diff --git a/ee/client/omnichannel/tags/TagsRoute.js b/ee/client/omnichannel/tags/TagsRoute.js
index 3a80935b3e4..17d797475c1 100644
--- a/ee/client/omnichannel/tags/TagsRoute.js
+++ b/ee/client/omnichannel/tags/TagsRoute.js
@@ -14,7 +14,7 @@ import { useRouteParameter, useRoute } from '../../../../client/contexts/RouterC
import VerticalBar from '../../../../client/components/basic/VerticalBar';
import TagsPage from './TagsPage';
import { TagEditWithData, TagNew } from './EditTag';
-import DeleteWarningModal from '../../../../client/omnichannel/DeleteWarningModal';
+import DeleteWarningModal from '../../../../client/components/DeleteWarningModal';
import { useSetModal } from '../../../../client/contexts/ModalContext';
import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';
diff --git a/ee/client/omnichannel/units/UnitsRoute.js b/ee/client/omnichannel/units/UnitsRoute.js
index a3863db8269..822aae90e09 100644
--- a/ee/client/omnichannel/units/UnitsRoute.js
+++ b/ee/client/omnichannel/units/UnitsRoute.js
@@ -14,7 +14,7 @@ import { useRouteParameter, useRoute } from '../../../../client/contexts/RouterC
import VerticalBar from '../../../../client/components/basic/VerticalBar';
import UnitsPage from './UnitsPage';
import { UnitEditWithData, UnitNew } from './EditUnit';
-import DeleteWarningModal from '../../../../client/omnichannel/DeleteWarningModal';
+import DeleteWarningModal from '../../../../client/components/DeleteWarningModal';
import { useSetModal } from '../../../../client/contexts/ModalContext';
import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext';
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index 55eef2830ba..93ad091f550 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -3190,6 +3190,7 @@
"Secret_token": "Secret Token",
"Security": "Security",
"Select_a_department": "Select a department",
+ "Select_a_room": "Select a room",
"Select_a_user": "Select a user",
"Select_at_least_two_users": "Select at least two users",
"Select_an_avatar": "Select an avatar",