snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import type { IRole, IPermission } from '@rocket.chat/core-typings';
import { GenericTableRow, GenericTableCell } from '@rocket.chat/ui-client';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import type { TFunction } from 'i18next';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import RoleCell from './RoleCell';
import { CONSTANTS } from '../../../../../app/authorization/lib';
import { useChangeRole } from '../hooks/useChangeRole';
const getName = (t: TFunction, permission: IPermission): string => {
if (permission.level === CONSTANTS.SETTINGS_LEVEL) {
let path = '';
if (permission.group) {
path = `${t(permission.group as TranslationKey)} > `;
}
if (permission.section) {
path = `${path}${t(permission.section as TranslationKey)} > `;
}
return `${path}${t(permission.settingId as TranslationKey)}`;
}
return t(permission._id as TranslationKey);
};
export type PermissionRowProps = {
permission: IPermission;
roleList: IRole[];
onGrant: (permissionId: IPermission['_id'], roleId: IRole['_id']) => Promise<void>;
onRemove: (permissionId: IPermission['_id'], roleId: IRole['_id']) => Promise<void>;
};
const PermissionRow = ({ permission, roleList, onGrant, onRemove }: PermissionRowProps) => {
const { t } = useTranslation();
const { _id: permissionId, roles } = permission;
const changeRole = useChangeRole({ onGrant, onRemove, permissionId });
const permissionName = getName(t, permission);
return (
<GenericTableRow key={permissionId} role='link' action tabIndex={0}>
<GenericTableCell maxWidth='x300' withTruncatedText title={t(`${permissionId}_description` as TranslationKey)}>
{permissionName}
</GenericTableCell>
{roleList.map(({ _id: roleId, name, description }) => (
<RoleCell
key={roleId}
_id={roleId}
name={name}
description={description}
grantedRoles={roles}
onChange={changeRole}
permissionId={permissionId}
permissionName={permissionName}
/>
))}
</GenericTableRow>
);
};
export default memo(PermissionRow);

View File

@@ -0,0 +1,99 @@
import type { IPermission, IRole } from '@rocket.chat/core-typings';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { composeStories } from '@storybook/react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';
import PermissionsTable from './PermissionsTable';
import * as stories from './PermissionsTable.stories';
import { createMockedPagination } from '../../../../../tests/mocks/data';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />, { wrapper: mockAppRoot().build() });
expect(baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />, { wrapper: mockAppRoot().build() });
// TODO: Needed to skip `label` because fuselage‘s `CheckBox` has a a11y empty label issue
// TODO: Needed to skip `button-name` because fuselage‘s `Pagination` buttons are missing names
const results = await axe(container, { rules: { 'label': { enabled: false }, 'button-name': { enabled: false } } });
expect(results).toHaveNoViolations();
});
const paginationData = createMockedPagination();
const defaultPermissions: IPermission[] = [
{
_id: 'access-permissions',
_updatedAt: new Date('2023-01-01'),
roles: ['admin'],
},
];
const roles: IRole[] = [
{
description: 'Owner of the workspace',
name: 'owner',
protected: true,
scope: 'Users',
_id: 'owner',
_updatedAt: new Date(),
},
{
description: 'Administrator',
name: 'admin',
protected: true,
scope: 'Users',
_id: 'admin',
_updatedAt: new Date(),
},
];
test('should display modal if the permission is access-permissions and has only one granted role', async () => {
render(
<PermissionsTable
permissions={defaultPermissions}
total={defaultPermissions.length}
setFilter={() => undefined}
roleList={roles}
paginationData={paginationData}
/>,
{
wrapper: mockAppRoot().build(),
},
);
await userEvent.click(screen.getByRole('checkbox', { name: 'access-permissions - Administrator' }));
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
test('should NOT display modal if the permission is access-permissions and has more than one granted role', async () => {
const morePermissions: IPermission[] = [
{
_id: 'access-permissions',
_updatedAt: new Date('2023-01-01'),
roles: ['admin', 'owner'],
},
];
render(
<PermissionsTable
permissions={morePermissions}
total={morePermissions.length}
setFilter={() => undefined}
roleList={roles}
paginationData={paginationData}
/>,
{
wrapper: mockAppRoot().build(),
},
);
await userEvent.click(screen.getByRole('checkbox', { name: 'access-permissions - Administrator' }));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

View File

@@ -0,0 +1,130 @@
import type { IPermission, IRole } from '@rocket.chat/core-typings';
import { Margins } from '@rocket.chat/fuselage';
import { PageContent } from '@rocket.chat/ui-client';
import type { Meta } from '@storybook/react';
import PermissionsTable from './PermissionsTable';
import { createMockedPagination } from '../../../../../tests/mocks/data';
export default {
component: PermissionsTable,
decorators: [
(fn) => (
<PageContent marginBlock='neg-x8'>
<Margins block={8}>{fn()}</Margins>
</PageContent>
),
],
} satisfies Meta<typeof PermissionsTable>;
const roles: IRole[] = [
{
description: 'Owner of the workspace',
name: 'owner',
protected: true,
scope: 'Users',
_id: 'owner',
_updatedAt: new Date(),
},
{
description: 'Administrator',
name: 'admin',
protected: true,
scope: 'Users',
_id: 'admin',
_updatedAt: new Date(),
},
{
description: 'Leader',
name: 'leader',
protected: false,
scope: 'Subscriptions',
_id: 'leader',
_updatedAt: new Date(),
},
{
description: 'Moderator',
name: 'moderator',
protected: false,
scope: 'Subscriptions',
_id: 'moderator',
_updatedAt: new Date(),
},
{
description: 'User',
name: 'user',
protected: true,
scope: 'Users',
_id: 'user',
_updatedAt: new Date(),
},
{
description: 'Guest',
name: 'guest',
protected: true,
scope: 'Users',
_id: 'guest',
_updatedAt: new Date(),
},
{
description: 'Bot',
name: 'bot',
protected: true,
scope: 'Users',
_id: 'bot',
_updatedAt: new Date(),
},
{
description: 'App',
name: 'app',
protected: true,
scope: 'Users',
_id: 'app',
_updatedAt: new Date(),
},
];
const permissions: IPermission[] = [
{
_id: '0',
_updatedAt: new Date('2023-01-01'),
roles: ['admin'],
},
{
_id: '1',
_updatedAt: new Date('2023-01-01'),
roles: ['user'],
},
{
_id: '2',
_updatedAt: new Date('2023-01-01'),
roles: ['user'],
},
{
_id: '3',
_updatedAt: new Date('2023-01-01'),
roles: ['user'],
},
];
const paginationData = createMockedPagination(permissions.length, 10);
export const Default = {
args: {
total: 10,
permissions,
roleList: roles,
setFilter: () => undefined,
paginationData,
},
};
export const Empty = {
args: {
total: 0,
permissions: [],
roleList: [],
setFilter: () => undefined,
paginationData,
},
};

View File

@@ -0,0 +1,112 @@
import type { IPermission, IRole } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
import { Pagination, Palette } from '@rocket.chat/fuselage';
import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '@rocket.chat/ui-client';
import type { usePagination } from '@rocket.chat/ui-client';
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import PermissionRow from './PermissionRow';
import PermissionsTableFilter from './PermissionsTableFilter';
import RoleHeader from './RoleHeader';
import GenericNoResults from '../../../../components/GenericNoResults';
export type PermissionsTableProps = {
roleList: IRole[];
permissions: IPermission[];
setFilter: (filter: string) => void;
total: number;
paginationData: ReturnType<typeof usePagination>;
};
const PermissionsTable = ({ roleList, permissions, setFilter, total, paginationData }: PermissionsTableProps) => {
const { t } = useTranslation();
const addPermissionRole = useEndpoint('POST', '/v1/permissions.addRole');
const removePermissionRole = useEndpoint('POST', '/v1/permissions.removeRole');
const grantRole = useCallback(
async (permissionId: IPermission['_id'], role: IRole['_id']) => {
await addPermissionRole({ permissionId, role });
},
[addPermissionRole],
);
const removeRole = useCallback(
async (permissionId: IPermission['_id'], role: IRole['_id']) => {
await removePermissionRole({ permissionId, role });
},
[removePermissionRole],
);
const { current, itemsPerPage, setCurrent, setItemsPerPage, ...paginationProps } = paginationData;
const tableCustomStyle = css`
// Makes the first column of the table sticky
tr > th {
&:first-child {
position: sticky;
left: 0;
background-color: ${Palette.surface['surface-light']};
z-index: 12;
}
}
tr > td {
&:first-child {
position: sticky;
left: 0;
box-shadow: -1px 0 0 ${Palette.stroke['stroke-light']} inset;
background-color: ${Palette.surface['surface-light']};
z-index: 11;
}
}
tr:hover {
td {
&:first-child {
background-color: ${Palette.surface['surface-hover']};
}
}
td > :nth-child(2) {
visibility: visible;
opacity: 1;
}
}
`;
return (
<>
<PermissionsTableFilter onChange={setFilter} />
{permissions?.length === 0 && <GenericNoResults />}
{permissions?.length > 0 && (
<>
<GenericTable className={[tableCustomStyle]} fixed={false}>
<GenericTableHeader>
<GenericTableHeaderCell width='x120'>{t('Name')}</GenericTableHeaderCell>
{roleList?.map(({ _id, name, description }) => (
<RoleHeader key={_id} _id={_id} name={name} description={description} />
))}
</GenericTableHeader>
<GenericTableBody>
{permissions.map((permission) => (
<PermissionRow key={permission._id} permission={permission} roleList={roleList} onGrant={grantRole} onRemove={removeRole} />
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={total}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
{...paginationProps}
/>
</>
)}
</>
);
};
export default PermissionsTable;

View File

@@ -0,0 +1,31 @@
import { Icon, TextInput } from '@rocket.chat/fuselage';
import { useStableCallback, useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import type { ChangeEvent } from 'react';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
/**
*
* TODO: Replaced this by FilterByText, it has the same render
*/
const PermissionsTableFilter = ({ onChange }: { onChange: (debouncedFilter: string) => void }) => {
const { t } = useTranslation();
const [filter, setFilter] = useState('');
const debouncedFilter = useDebouncedValue(filter, 500);
useEffect(() => {
onChange(debouncedFilter);
}, [debouncedFilter, onChange]);
const handleFilter = useStableCallback(({ currentTarget: { value } }: ChangeEvent<HTMLInputElement>) => {
setFilter(value);
});
return (
<div>
<TextInput value={filter} onChange={handleFilter} placeholder={t('Search')} endAddon={<Icon name='magnifier' size='x20' />} />
</div>
);
};
export default PermissionsTableFilter;

View File

@@ -0,0 +1,72 @@
import type { IRole } from '@rocket.chat/core-typings';
import { Margins, Box, CheckBox, Throbber } from '@rocket.chat/fuselage';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { GenericModal, GenericTableCell } from '@rocket.chat/ui-client';
import { useSetModal } from '@rocket.chat/ui-contexts';
import { useState, memo } from 'react';
import { useTranslation } from 'react-i18next';
import { AuthorizationUtils, confirmationRequiredPermissions } from '../../../../../app/authorization/lib';
export type RoleCellProps = {
_id: IRole['_id'];
name: IRole['name'];
description: IRole['description'];
onChange: (roleId: IRole['_id'], granted: boolean) => Promise<boolean>;
permissionId: string;
permissionName: string;
grantedRoles: IRole['_id'][];
};
const RoleCell = ({ _id, name, description, onChange, permissionId, permissionName, grantedRoles = [] }: RoleCellProps) => {
const { t } = useTranslation();
const setModal = useSetModal();
const [granted, setGranted] = useState(() => !!grantedRoles.includes(_id));
const [loading, setLoading] = useState(false);
const isRestrictedForRole = AuthorizationUtils.isPermissionRestrictedForRole(permissionId, _id);
const shouldDisplayConfirmation = confirmationRequiredPermissions.includes(permissionId) && grantedRoles.length === 1 && granted;
const handleChange = useStableCallback(() => {
if (shouldDisplayConfirmation) {
const handleSubmit = () => {
handleConfirmChange();
setModal(null);
};
return setModal(
<GenericModal onConfirm={handleSubmit} onCancel={() => setModal(null)} confirmText={t('Remove')} variant='danger'>
{t('Last_role_in_permission_warning')}
</GenericModal>,
);
}
return handleConfirmChange();
});
const handleConfirmChange = useStableCallback(async () => {
setLoading(true);
const result = await onChange(_id, granted);
setGranted(result);
setLoading(false);
});
const isDisabled = !!loading || !!isRestrictedForRole;
const checkboxLabel = `${permissionName} - ${description || name}`;
return (
<GenericTableCell withTruncatedText>
<Margins inline={2}>
<CheckBox aria-label={checkboxLabel} checked={granted} onChange={handleChange} disabled={isDisabled} />
{!loading && (
<Box display='inline' color='hint' invisible>
{description || name}
</Box>
)}
{loading && <Throbber size='x12' display='inline-block' />}
</Margins>
</GenericTableCell>
);
};
export default memo(RoleCell);

View File

@@ -0,0 +1,33 @@
import type { IRole } from '@rocket.chat/core-typings';
import { Button } from '@rocket.chat/fuselage';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { GenericTableHeaderCell } from '@rocket.chat/ui-client';
import { useRoute } from '@rocket.chat/ui-contexts';
import { memo } from 'react';
export type RoleHeaderProps = {
_id: IRole['_id'];
name: IRole['name'];
description: IRole['description'];
};
const RoleHeader = ({ _id, name, description }: RoleHeaderProps) => {
const router = useRoute('admin-permissions');
const handleEditRole = useStableCallback(() => {
router.push({
context: 'edit',
_id,
});
});
return (
<GenericTableHeaderCell paddingInline={4} padding={8}>
<Button small icon='edit' secondary onClick={handleEditRole}>
{description || name}
</Button>
</GenericTableHeaderCell>
);
};
export default memo(RoleHeader);

View File

@@ -0,0 +1 @@
export { default } from './PermissionsTable';