Skip to content
5 changes: 5 additions & 0 deletions .changeset/popular-stingrays-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': minor
---

Improves the search of permissions
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { filterPermissionKeys, mapPermissionKeys } from './mapPermissionKeys';

describe('mapPermissionKeys', () => {
it('should return an empty array if there are no permissions', () => {
const t: any = (string: string) => string;

const result = mapPermissionKeys({ t, permissions: [] });

expect(result).toEqual([]);
});

it('should map the permissions and return an array of objects with _id and i18nLabels', () => {
const t: any = (string: string) => string;

const result = mapPermissionKeys({
t,
permissions: [
{
_id: 'delete-team',
roles: ['admin'],
_updatedAt: new Date(),
},
{
_id: 'delete-user',
roles: ['admin'],
_updatedAt: new Date(),
},
{
_id: 'delete-channel',
roles: ['admin'],
_updatedAt: new Date(),
},
],
});

expect(result).toEqual([
{
_id: 'delete-team',
i18nLabels: ['delete-team'],
},
{
_id: 'delete-user',
i18nLabels: ['delete-user'],
},
{
_id: 'delete-channel',
i18nLabels: ['delete-channel'],
},
]);
});
});

describe('filterPermissionKeys', () => {
it('should return an empty array if there are no permissions', () => {
const result = filterPermissionKeys([], '');

expect(result).toEqual([]);
});

it('should filter the permissions and return an array ids that match with the filter text', () => {
const result = filterPermissionKeys(permissionKeys, 'delete');
expect(result).toEqual(['delete-team', 'delete-user', 'delete-channel']);
});

it('should match case insensitive and in any order', () => {
const result = filterPermissionKeys(permissionKeys, 'team DELETE');
expect(result).toEqual(['delete-team']);
});

it('should return an empty array if there are no matches with the filter text', () => {
const result = filterPermissionKeys(permissionKeys, 'mailer');
expect(result).toEqual([]);
});
});

const permissionKeys = [
{
_id: 'delete-team',
i18nLabels: ['delete-team', 'Delete team'],
},
{
_id: 'delete-user',
i18nLabels: ['delete-user', 'Delete user'],
},
{
_id: 'delete-channel',
i18nLabels: ['delete-channel', 'Delete channel'],
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { IPermission } from '@rocket.chat/core-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { TFunction } from 'i18next';

export const mapPermissionKeys = ({ t, permissions }: { t: TFunction; permissions: IPermission[] }) =>
permissions.map(({ _id, settingId, group, section }) => ({
_id,
i18nLabels: [group && t(group), section && t(section), settingId && t(settingId), t(_id)].filter(Boolean) as string[],
}));

export const filterPermissionKeys = (permissionKeys: { _id: string; i18nLabels: string[] }[], filter: string): string[] => {
const words = escapeRegExp(filter).split(' ').filter(Boolean);
return permissionKeys
.filter(({ _id, i18nLabels }) =>
words.every(
(word) =>
_id.toLocaleLowerCase().includes(word.toLocaleLowerCase()) ||
i18nLabels.join(' ').toLocaleLowerCase().includes(word.toLocaleLowerCase()),
),
)
.map(({ _id }) => _id);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import { Permissions } from '../../../../../app/models/client';
import { filterPermissionKeys, mapPermissionKeys } from '../helpers/mapPermissionKeys';

export const useFilteredPermissions = ({ filter }: { filter: string }) => {
const { t } = useTranslation();

const mappedPermissionKeys = useMemo(() => {
const permissions = Permissions.find().fetch();
return mapPermissionKeys({ t, permissions });
}, [t]);

const debouncedFilter = useDebouncedValue(filter, 400);

return useMemo(() => {
return filterPermissionKeys(mappedPermissionKeys, debouncedFilter);
}, [debouncedFilter, mappedPermissionKeys]);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { IRole, IPermission } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { Mongo } from 'meteor/mongo';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';

import { useFilteredPermissions } from './useFilteredPermissions';
import { CONSTANTS } from '../../../../../app/authorization/lib';
import { Permissions, Roles } from '../../../../../app/models/client';
import { useReactiveValue } from '../../../../hooks/useReactiveValue';
Expand All @@ -14,27 +13,28 @@ export const usePermissionsAndRoles = (
limit = 25,
skip = 0,
): { permissions: IPermission[]; total: number; roleList: IRole[]; reload: () => void } => {
const getFilter = useCallback((): Mongo.Selector<IPermission> => {
const filterRegExp = new RegExp(escapeRegExp(filter), 'i');
const filteredIds = useFilteredPermissions({ filter });

const selector = useMemo(() => {
return {
level: type === 'permissions' ? { $ne: CONSTANTS.SETTINGS_LEVEL } : CONSTANTS.SETTINGS_LEVEL,
_id: filterRegExp,
_id: { $in: filteredIds },
};
}, [type, filter]);
}, [filteredIds, type]);

const getPermissions = useCallback(
() =>
Permissions.find(getFilter(), {
Permissions.find(selector, {
sort: {
_id: 1,
},
skip,
limit,
}),
[limit, skip, getFilter],
[selector, skip, limit],
);
const getTotalPermissions = useCallback(() => Permissions.find(getFilter()).count(), [getFilter]);

const getTotalPermissions = useCallback(() => Permissions.find(selector).count(), [selector]);

const permissions = useReactiveValue(getPermissions);
const permissionsTotal = useReactiveValue(getTotalPermissions);
Expand Down
Loading