diff --git a/.changeset/popular-stingrays-trade.md b/.changeset/popular-stingrays-trade.md new file mode 100644 index 0000000000000..5f0e5eff75ec3 --- /dev/null +++ b/.changeset/popular-stingrays-trade.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Improves the search of permissions diff --git a/apps/meteor/client/views/admin/permissions/helpers/mapPermissionKeys.spec.ts b/apps/meteor/client/views/admin/permissions/helpers/mapPermissionKeys.spec.ts new file mode 100644 index 0000000000000..9786dbd879c9a --- /dev/null +++ b/apps/meteor/client/views/admin/permissions/helpers/mapPermissionKeys.spec.ts @@ -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'], + }, +]; diff --git a/apps/meteor/client/views/admin/permissions/helpers/mapPermissionKeys.ts b/apps/meteor/client/views/admin/permissions/helpers/mapPermissionKeys.ts new file mode 100644 index 0000000000000..8a5a094162a2a --- /dev/null +++ b/apps/meteor/client/views/admin/permissions/helpers/mapPermissionKeys.ts @@ -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); +}; diff --git a/apps/meteor/client/views/admin/permissions/hooks/useFilteredPermissions.ts b/apps/meteor/client/views/admin/permissions/hooks/useFilteredPermissions.ts new file mode 100644 index 0000000000000..9f0b86c29efe1 --- /dev/null +++ b/apps/meteor/client/views/admin/permissions/hooks/useFilteredPermissions.ts @@ -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]); +}; diff --git a/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts b/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts index f56390b888166..4580a9a6570d7 100644 --- a/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts +++ b/apps/meteor/client/views/admin/permissions/hooks/usePermissionsAndRoles.ts @@ -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'; @@ -14,27 +13,28 @@ export const usePermissionsAndRoles = ( limit = 25, skip = 0, ): { permissions: IPermission[]; total: number; roleList: IRole[]; reload: () => void } => { - const getFilter = useCallback((): Mongo.Selector => { - 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);