Add object level permission permissions to role page (ReadOnly) (#11568)

## Context
This PR adds the display of object-level permissions. A following PR
will add the ability to update those permissions.
The PR contains the SettingsRoleObjectLevel page but it's not fully
implemented yet (save won't trigger the corresponding mutation)

<img width="616" alt="Screenshot 2025-04-14 at 18 02 40"
src="https://github.com/user-attachments/assets/f8c58193-31f3-468a-a96d-f06a9f2e1423"
/>
This commit is contained in:
Weiko
2025-04-15 18:46:36 +02:00
committed by GitHub
parent c23942ce6f
commit 43af5ceb5e
41 changed files with 1092 additions and 268 deletions
@@ -0,0 +1,135 @@
import { SettingsRolePermissionsObjectsTableHeader } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsTableHeader';
import { SettingsRolePermissionsObjectsTableRow } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsTableRow';
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
const StyledTable = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledTableRows = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
type SettingsRolePermissionsObjectsSectionProps = {
roleId: string;
isEditable: boolean;
};
export const SettingsRolePermissionsObjectsSection = ({
roleId,
isEditable,
}: SettingsRolePermissionsObjectsSectionProps) => {
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const objectPermissions = settingsDraftRole.objectPermissions;
const objectPermissionsConfig: SettingsRolePermissionsObjectPermission[] = [
{
key: 'canReadObjectRecords',
label: t`See Records on All Objects`,
overriddenBy:
objectPermissions?.filter(
(permission) =>
isDefined(permission.canReadObjectRecords) &&
permission.canReadObjectRecords !==
settingsDraftRole.canReadAllObjectRecords,
)?.length ?? 0,
value: settingsDraftRole.canReadAllObjectRecords,
setValue: (value: boolean) => {
setSettingsDraftRole({
...settingsDraftRole,
canReadAllObjectRecords: value,
});
},
},
{
key: 'canUpdateObjectRecords',
label: t`Edit Records on All Objects`,
overriddenBy:
objectPermissions?.filter(
(permission) =>
isDefined(permission.canUpdateObjectRecords) &&
permission.canUpdateObjectRecords !==
settingsDraftRole.canUpdateAllObjectRecords,
)?.length ?? 0,
value: settingsDraftRole.canUpdateAllObjectRecords,
setValue: (value: boolean) => {
setSettingsDraftRole({
...settingsDraftRole,
canUpdateAllObjectRecords: value,
});
},
},
{
key: 'canSoftDeleteObjectRecords',
label: t`Delete Records on All Objects`,
overriddenBy:
objectPermissions?.filter(
(permission) =>
isDefined(permission.canSoftDeleteObjectRecords) &&
permission.canSoftDeleteObjectRecords !==
settingsDraftRole.canSoftDeleteAllObjectRecords,
)?.length ?? 0,
value: settingsDraftRole.canSoftDeleteAllObjectRecords,
setValue: (value: boolean) => {
setSettingsDraftRole({
...settingsDraftRole,
canSoftDeleteAllObjectRecords: value,
});
},
},
{
key: 'canDestroyObjectRecords',
label: t`Destroy Records on All Objects`,
overriddenBy:
objectPermissions?.filter(
(permission) =>
isDefined(permission.canDestroyObjectRecords) &&
permission.canDestroyObjectRecords !==
settingsDraftRole.canDestroyAllObjectRecords,
)?.length ?? 0,
value: settingsDraftRole.canDestroyAllObjectRecords,
setValue: (value: boolean) => {
setSettingsDraftRole({
...settingsDraftRole,
canDestroyAllObjectRecords: value,
});
},
},
];
return (
<Section>
<H2Title
title={t`Objects`}
description={t`Ability to interact with each object`}
/>
<StyledTable>
<SettingsRolePermissionsObjectsTableHeader
roleId={roleId}
objectPermissionsConfig={objectPermissionsConfig}
isEditable={isEditable}
/>
<StyledTableRows>
{objectPermissionsConfig.map((permission) => (
<SettingsRolePermissionsObjectsTableRow
key={permission.key}
permission={permission}
isEditable={isEditable}
/>
))}
</StyledTableRows>
</StyledTable>
</Section>
);
};
@@ -0,0 +1,68 @@
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { Checkbox } from 'twenty-ui/input';
const StyledNameHeader = styled(TableHeader)`
flex: 1;
`;
const StyledActionsHeader = styled(TableHeader)`
align-items: center;
display: flex;
justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(4)};
`;
type SettingsRolePermissionsObjectsTableHeaderProps = {
roleId: string;
objectPermissionsConfig: SettingsRolePermissionsObjectPermission[];
isEditable: boolean;
};
export const SettingsRolePermissionsObjectsTableHeader = ({
roleId,
objectPermissionsConfig,
isEditable,
}: SettingsRolePermissionsObjectsTableHeaderProps) => {
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const allPermissionsEnabled = objectPermissionsConfig.every(
(permission) => permission.value,
);
const somePermissionsEnabled = objectPermissionsConfig.some(
(permission) => permission.value,
);
return (
<TableRow>
<StyledNameHeader>{t`Name`}</StyledNameHeader>
<StyledActionsHeader aria-label={t`Actions`}>
<Checkbox
checked={allPermissionsEnabled}
indeterminate={somePermissionsEnabled && !allPermissionsEnabled}
disabled={!isEditable}
aria-label={t`Toggle all object permissions`}
onChange={() => {
const newValue = !allPermissionsEnabled;
setSettingsDraftRole({
...settingsDraftRole,
canReadAllObjectRecords: newValue,
canUpdateAllObjectRecords: newValue,
canSoftDeleteAllObjectRecords: newValue,
canDestroyAllObjectRecords: newValue,
});
}}
/>
</StyledActionsHeader>
</TableRow>
);
};
@@ -0,0 +1,81 @@
import { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import pluralize from 'pluralize';
import { Checkbox, CheckboxAccent } from 'twenty-ui/input';
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledOverrideInfo = styled.span`
background: ${({ theme }) => theme.adaptiveColors.orange1};
border-radius: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.color.orange};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledPermissionCell = styled(TableCell)`
align-items: center;
display: flex;
flex: 1;
gap: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledCheckboxCell = styled(TableCell)`
align-items: center;
display: flex;
justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(4)};
`;
const StyledTableRow = styled(TableRow)`
align-items: center;
display: flex;
`;
type SettingsRolePermissionsObjectsTableRowProps = {
permission: SettingsRolePermissionsObjectPermission;
isEditable: boolean;
};
export const SettingsRolePermissionsObjectsTableRow = ({
permission,
isEditable,
}: SettingsRolePermissionsObjectsTableRowProps) => {
const theme = useTheme();
const isOverriddenBy = permission.overriddenBy;
const isOverridden = isOverriddenBy && isOverriddenBy > 0;
const label = permission.label;
const pluralizedObject = pluralize('object', isOverriddenBy);
const { Icon } = SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission.key];
return (
<StyledTableRow>
<StyledPermissionCell>
<Icon size={theme.icon.size.sm} />
<StyledLabel>{label}</StyledLabel>
{isOverridden ? (
<StyledOverrideInfo>
{t`Overridden on ${isOverriddenBy} ${pluralizedObject}`}
</StyledOverrideInfo>
) : null}
</StyledPermissionCell>
<StyledCheckboxCell>
<Checkbox
checked={permission.value ?? false}
onChange={() => permission.setValue(!permission.value)}
disabled={!isEditable}
accent={isOverridden ? CheckboxAccent.Orange : CheckboxAccent.Blue}
/>
</StyledCheckboxCell>
</StyledTableRow>
);
};
@@ -0,0 +1,37 @@
import {
IconComponent,
IconEye,
IconEyeOff,
IconPencil,
IconPencilOff,
IconTrash,
IconTrashOff,
IconTrashX,
} from 'twenty-ui/display';
type SettingsRoleObjectPermissionIconConfig = {
Icon: IconComponent;
IconForbidden: IconComponent;
};
export const SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG: Record<
string,
SettingsRoleObjectPermissionIconConfig
> = {
canReadObjectRecords: {
Icon: IconEye,
IconForbidden: IconEyeOff,
},
canUpdateObjectRecords: {
Icon: IconPencil,
IconForbidden: IconPencilOff,
},
canSoftDeleteObjectRecords: {
Icon: IconTrash,
IconForbidden: IconTrashOff,
},
canDestroyObjectRecords: {
Icon: IconTrashX,
IconForbidden: IconTrashX,
},
};
@@ -0,0 +1,8 @@
import { ReactNode } from 'react';
export type SettingsRolePermissionsObjectPermission = {
key: string;
label: string | ReactNode;
value?: boolean | null;
setValue: (value: boolean) => void;
overriddenBy?: number;
};