add role update (#11217)

## Context
This PR introduces the new Create and Edit role components, behind the
PERMISSIONS_ENABLED_V2 feature flag.
This commit is contained in:
Weiko
2025-03-31 17:57:14 +02:00
committed by GitHub
parent 3c9bf2294f
commit 06ff16e086
58 changed files with 1527 additions and 624 deletions

View File

@ -1,175 +0,0 @@
import { RolePermissionsObjectsTableHeader } from '@/settings/roles/role-permissions/components/RolePermissionsObjectsTableHeader';
import { RolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/components/RolePermissionsSettingsTableHeader';
import { RolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/components/RolePermissionsSettingsTableRow';
import { RolePermissionsObjectPermission } from '@/settings/roles/types/RolePermissionsObjectPermission';
import { RolePermissionsSettingPermission } from '@/settings/roles/types/RolePermissionsSettingPermission';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import {
H2Title,
IconCode,
IconEye,
IconHierarchy,
IconKey,
IconLockOpen,
IconPencil,
IconServer,
IconSettings,
IconTrash,
IconTrashX,
IconUsers,
Section,
} from 'twenty-ui';
import { Role } from '~/generated-metadata/graphql';
import { SettingPermissionType } from '~/generated/graphql';
import { RolePermissionsObjectsTableRow } from './RolePermissionsObjectsTableRow';
const StyledRolePermissionsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(8)};
`;
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 RolePermissionsProps = {
role: Pick<
Role,
| 'id'
| 'canUpdateAllSettings'
| 'canReadAllObjectRecords'
| 'canUpdateAllObjectRecords'
| 'canSoftDeleteAllObjectRecords'
| 'canDestroyAllObjectRecords'
>;
};
export const RolePermissions = ({ role }: RolePermissionsProps) => {
const objectPermissionsConfig: RolePermissionsObjectPermission[] = [
{
key: 'seeRecords',
label: 'See Records on All Objects',
Icon: IconEye,
value: role.canReadAllObjectRecords,
},
{
key: 'editRecords',
label: 'Edit Records on All Objects',
Icon: IconPencil,
value: role.canUpdateAllObjectRecords,
},
{
key: 'deleteRecords',
label: 'Delete Records on All Objects',
Icon: IconTrash,
value: role.canSoftDeleteAllObjectRecords,
},
{
key: 'destroyRecords',
label: 'Destroy Records on All Objects',
Icon: IconTrashX,
value: role.canDestroyAllObjectRecords,
},
];
const settingsPermissionsConfig: RolePermissionsSettingPermission[] = [
{
key: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
name: 'API Keys & Webhooks',
description: 'Manage API keys and webhooks',
value: role.canUpdateAllSettings,
Icon: IconCode,
},
{
key: SettingPermissionType.WORKSPACE,
name: 'Workspace',
description: 'Set global workspace preferences',
value: role.canUpdateAllSettings,
Icon: IconSettings,
},
{
key: SettingPermissionType.WORKSPACE_MEMBERS,
name: 'Users',
description: 'Add or remove users',
value: role.canUpdateAllSettings,
Icon: IconUsers,
},
{
key: SettingPermissionType.ROLES,
name: 'Roles',
description: 'Define user roles and access levels',
value: role.canUpdateAllSettings,
Icon: IconLockOpen,
},
{
key: SettingPermissionType.DATA_MODEL,
name: 'Data Model',
description: 'Edit CRM data structure and fields',
value: role.canUpdateAllSettings,
Icon: IconHierarchy,
},
{
key: SettingPermissionType.ADMIN_PANEL,
name: 'Admin Panel',
description: 'Admin settings and system tools',
value: role.canUpdateAllSettings,
Icon: IconServer,
},
{
key: SettingPermissionType.SECURITY,
name: 'Security',
description: 'Manage security policies',
value: role.canUpdateAllSettings,
Icon: IconKey,
},
];
return (
<StyledRolePermissionsContainer>
<Section>
<H2Title
title={t`Objects`}
description={t`Ability to interact with each object`}
/>
<StyledTable>
<RolePermissionsObjectsTableHeader
allPermissions={objectPermissionsConfig.every(
(permission) => permission.value,
)}
/>
<StyledTableRows>
{objectPermissionsConfig.map((permission) => (
<RolePermissionsObjectsTableRow
key={permission.key}
permission={permission}
/>
))}
</StyledTableRows>
</StyledTable>
</Section>
<Section>
<H2Title title={t`Settings`} description={t`Settings permissions`} />
<StyledTable>
<RolePermissionsSettingsTableHeader
allPermissions={role.canUpdateAllSettings}
/>
<StyledTableRows>
{settingsPermissionsConfig.map((permission) => (
<RolePermissionsSettingsTableRow
key={permission.key}
permission={permission}
/>
))}
</StyledTableRows>
</StyledTable>
</Section>
</StyledRolePermissionsContainer>
);
};

View File

@ -1,35 +0,0 @@
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 { Checkbox } from 'twenty-ui';
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 RolePermissionsObjectsTableHeaderProps = {
allPermissions: boolean;
};
export const RolePermissionsObjectsTableHeader = ({
allPermissions,
}: RolePermissionsObjectsTableHeaderProps) => (
<TableRow>
<StyledNameHeader>{t`Name`}</StyledNameHeader>
<StyledActionsHeader aria-label={t`Actions`}>
<Checkbox
checked={allPermissions}
indeterminate={!allPermissions}
disabled
/>
</StyledActionsHeader>
</TableRow>
);

View File

@ -0,0 +1,202 @@
import { SettingsRolePermissionsObjectsTableHeader } from '@/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableHeader';
import { SettingsRolePermissionsObjectsTableRow } from '@/settings/roles/role-permissions/components/SettingsRolePermissionsObjectsTableRow';
import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableHeader';
import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/components/SettingsRolePermissionsSettingsTableRow';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/types/SettingsRolePermissionsObjectPermission';
import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/types/SettingsRolePermissionsSettingPermission';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import {
H2Title,
IconCode,
IconEye,
IconHierarchy,
IconKey,
IconLockOpen,
IconPencil,
IconServer,
IconSettings,
IconTrash,
IconTrashX,
IconUsers,
Section,
} from 'twenty-ui';
import { SettingPermissionType } from '~/generated-metadata/graphql';
const StyledRolePermissionsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(8)};
`;
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 SettingsRolePermissionsProps = {
roleId: string;
isEditable: boolean;
};
export const SettingsRolePermissions = ({
roleId,
isEditable,
}: SettingsRolePermissionsProps) => {
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const objectPermissionsConfig: SettingsRolePermissionsObjectPermission[] = [
{
key: 'seeRecords',
label: t`See Records on All Objects`,
Icon: IconEye,
value: settingsDraftRole.canReadAllObjectRecords,
setValue: (value: boolean) => {
setSettingsDraftRole({
...settingsDraftRole,
canReadAllObjectRecords: value,
});
},
},
{
key: 'editRecords',
label: t`Edit Records on All Objects`,
Icon: IconPencil,
value: settingsDraftRole.canUpdateAllObjectRecords,
setValue: (value: boolean) => {
setSettingsDraftRole({
...settingsDraftRole,
canUpdateAllObjectRecords: value,
});
},
},
{
key: 'deleteRecords',
label: t`Delete Records on All Objects`,
Icon: IconTrash,
value: settingsDraftRole.canSoftDeleteAllObjectRecords,
setValue: (value: boolean) => {
setSettingsDraftRole({
...settingsDraftRole,
canSoftDeleteAllObjectRecords: value,
});
},
},
{
key: 'destroyRecords',
label: t`Destroy Records on All Objects`,
Icon: IconTrashX,
value: settingsDraftRole.canDestroyAllObjectRecords,
setValue: (value: boolean) => {
setSettingsDraftRole({
...settingsDraftRole,
canDestroyAllObjectRecords: value,
});
},
},
];
const settingsPermissionsConfig: SettingsRolePermissionsSettingPermission[] =
[
{
key: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
name: t`API Keys & Webhooks`,
description: t`Manage API keys and webhooks`,
value: settingsDraftRole.canUpdateAllSettings,
Icon: IconCode,
},
{
key: SettingPermissionType.WORKSPACE,
name: t`Workspace`,
description: t`Set global workspace preferences`,
value: settingsDraftRole.canUpdateAllSettings,
Icon: IconSettings,
},
{
key: SettingPermissionType.WORKSPACE_MEMBERS,
name: t`Users`,
description: t`Add or remove users`,
value: settingsDraftRole.canUpdateAllSettings,
Icon: IconUsers,
},
{
key: SettingPermissionType.ROLES,
name: t`Roles`,
description: t`Define user roles and access levels`,
value: settingsDraftRole.canUpdateAllSettings,
Icon: IconLockOpen,
},
{
key: SettingPermissionType.DATA_MODEL,
name: t`Data Model`,
description: t`Edit CRM data structure and fields`,
value: settingsDraftRole.canUpdateAllSettings,
Icon: IconHierarchy,
},
{
key: SettingPermissionType.ADMIN_PANEL,
name: t`Admin Panel`,
description: t`Admin settings and system tools`,
value: settingsDraftRole.canUpdateAllSettings,
Icon: IconServer,
},
{
key: SettingPermissionType.SECURITY,
name: t`Security`,
description: t`Manage security policies`,
value: settingsDraftRole.canUpdateAllSettings,
Icon: IconKey,
},
];
return (
<StyledRolePermissionsContainer>
<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>
<Section>
<H2Title title={t`Settings`} description={t`Settings permissions`} />
<StyledTable>
<SettingsRolePermissionsSettingsTableHeader
allPermissions={settingsDraftRole.canUpdateAllSettings}
/>
<StyledTableRows>
{settingsPermissionsConfig.map((permission) => (
<SettingsRolePermissionsSettingsTableRow
key={permission.key}
permission={permission}
/>
))}
</StyledTableRows>
</StyledTable>
</Section>
</StyledRolePermissionsContainer>
);
};

View File

@ -0,0 +1,68 @@
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/types/SettingsRolePermissionsObjectPermission';
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';
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>
);
};

View File

@ -1,6 +1,7 @@
import { RolePermissionsObjectPermission } from '@/settings/roles/types/RolePermissionsObjectPermission';
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/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 { Checkbox } from 'twenty-ui';
@ -46,25 +47,33 @@ const StyledTableRow = styled(TableRow)`
display: flex;
`;
type RolePermissionsObjectsTableRowProps = {
permission: RolePermissionsObjectPermission;
type SettingsRolePermissionsObjectsTableRowProps = {
permission: SettingsRolePermissionsObjectPermission;
isEditable: boolean;
};
export const RolePermissionsObjectsTableRow = ({
export const SettingsRolePermissionsObjectsTableRow = ({
permission,
}: RolePermissionsObjectsTableRowProps) => {
isEditable,
}: SettingsRolePermissionsObjectsTableRowProps) => {
const theme = useTheme();
return (
<StyledTableRow key={permission.key}>
<StyledTableRow>
<StyledPermissionCell>
<StyledIconWrapper>
<StyledIcon>
<permission.Icon size={14} />
<permission.Icon size={theme.icon.size.sm} />
</StyledIcon>
</StyledIconWrapper>
<StyledLabel>{permission.label}</StyledLabel>
</StyledPermissionCell>
<StyledCheckboxCell>
<Checkbox checked={permission.value} disabled />
<Checkbox
checked={permission.value}
onChange={() => permission.setValue(!permission.value)}
disabled={!isEditable}
/>
</StyledCheckboxCell>
</StyledTableRow>
);

View File

@ -6,7 +6,10 @@ import { Checkbox } from 'twenty-ui';
const StyledNameHeader = styled(TableHeader)`
flex: 1;
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledTypeHeader = styled(TableHeader)`
flex: 1;
`;
const StyledActionsHeader = styled(TableHeader)`
@ -16,22 +19,24 @@ const StyledActionsHeader = styled(TableHeader)`
padding-right: ${({ theme }) => theme.spacing(4)};
`;
const StyledTypeHeader = styled(TableHeader)`
flex: 1;
`;
type RolePermissionsSettingsTableHeaderProps = {
type SettingsRolePermissionsSettingsTableHeaderProps = {
allPermissions: boolean;
onToggleAll?: () => void;
};
export const RolePermissionsSettingsTableHeader = ({
export const SettingsRolePermissionsSettingsTableHeader = ({
allPermissions,
}: RolePermissionsSettingsTableHeaderProps) => (
onToggleAll,
}: SettingsRolePermissionsSettingsTableHeaderProps) => (
<TableRow gridAutoColumns="3fr 4fr 24px">
<StyledNameHeader>{t`Name`}</StyledNameHeader>
<StyledTypeHeader>{t`Description`}</StyledTypeHeader>
<StyledActionsHeader aria-label={t`Actions`}>
<Checkbox checked={allPermissions} disabled />
<Checkbox
checked={allPermissions}
disabled={!onToggleAll}
onChange={onToggleAll}
/>
</StyledActionsHeader>
</TableRow>
);

View File

@ -1,4 +1,4 @@
import { RolePermissionsSettingPermission } from '@/settings/roles/types/RolePermissionsSettingPermission';
import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/types/SettingsRolePermissionsSettingPermission';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
@ -33,13 +33,13 @@ const StyledIconContainer = styled.div`
justify-content: center;
`;
type RolePermissionsSettingsTableRowProps = {
permission: RolePermissionsSettingPermission;
type SettingsRolePermissionsSettingsTableRowProps = {
permission: SettingsRolePermissionsSettingPermission;
};
export const RolePermissionsSettingsTableRow = ({
export const SettingsRolePermissionsSettingsTableRow = ({
permission,
}: RolePermissionsSettingsTableRowProps) => {
}: SettingsRolePermissionsSettingsTableRowProps) => {
const theme = useTheme();
return (
@ -47,7 +47,7 @@ export const RolePermissionsSettingsTableRow = ({
<StyledPermissionCell>
<StyledIconContainer>
<permission.Icon
size={16}
size={theme.icon.size.md}
color={theme.font.color.primary}
stroke={theme.icon.stroke.sm}
/>

View File

@ -0,0 +1,59 @@
import { SettingsRolePermissions } from '@/settings/roles/role-permissions/components/SettingsRolePermissions';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
import { PENDING_ROLE_ID } from '~/pages/settings/roles/SettingsRoleCreate';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { getRolesMock } from '~/testing/mock-data/roles';
const SettingsRolePermissionsWrapper = (
args: React.ComponentProps<typeof SettingsRolePermissions>,
) => {
const setDraftRole = useSetRecoilState(
settingsDraftRoleFamilyState(args.roleId),
);
const role = getRolesMock().find((role) => role.id === args.roleId);
if (isDefined(role)) {
setDraftRole(role);
}
return (
<SettingsRolePermissions
roleId={args.roleId}
isEditable={args.isEditable}
/>
);
};
const meta: Meta<typeof SettingsRolePermissionsWrapper> = {
title: 'Modules/Settings/Roles/RolePermissions/SettingsRolePermissions',
component: SettingsRolePermissionsWrapper,
decorators: [RouterDecorator, ComponentDecorator, I18nFrontDecorator],
};
export default meta;
type Story = StoryObj<typeof SettingsRolePermissionsWrapper>;
export const Default: Story = {
args: {
roleId: '1',
isEditable: true,
},
};
export const ReadOnly: Story = {
args: {
roleId: '1',
isEditable: false,
},
};
export const PendingRole: Story = {
args: {
roleId: PENDING_ROLE_ID,
},
};