[permissions][FE] followup design fixes 4 (#12737)

## Context
- Whole row is now clickable
- Fix padding on role tables
- Fix tab being persistant between roles
- Change various texts/descriptions
- Add un/check all on settings permissions
- Fix flash between role detail and roles
- Add "Granted for X object(s)"
- Swap permissions and assignment tabs position
- add tooltip for object level permission actions
- Add the inherited info on object-level permissions
This commit is contained in:
Weiko
2025-06-20 13:53:19 +02:00
committed by GitHub
parent 57abc246ef
commit 7687f4f285
22 changed files with 385 additions and 138 deletions

View File

@ -19,7 +19,7 @@ export const SettingsRolesContainer = () => {
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector); const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState); const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
if (settingsRolesIsLoading) { if (settingsRolesIsLoading && !settingsAllRoles) {
return null; return null;
} }

View File

@ -60,9 +60,13 @@ export const SettingsRoleDefaultRole = ({
const options = roles.map((role) => ({ const options = roles.map((role) => ({
label: role.label, label: role.label,
value: role.id, value: role.id,
Icon: getIcon(role.icon), Icon: getIcon(role.icon) ?? IconUserPin,
})); }));
if (options.length === 0) {
return null;
}
return ( return (
<Section> <Section>
<H2Title <H2Title

View File

@ -49,7 +49,7 @@ export const SettingsRolesList = () => {
<Section> <Section>
<H2Title <H2Title
title={t`All roles`} title={t`All roles`}
description={t`Assign roles to specify each member's access permissions`} description={t`Assign roles to manage each members access and permissions`}
/> />
<Table> <Table>
<SettingsRolesTableHeader /> <SettingsRolesTableHeader />

View File

@ -113,7 +113,7 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
[objectMetadataItems, searchFilter, excludedObjectMetadataIds], [objectMetadataItems, searchFilter, excludedObjectMetadataIds],
); );
const basicObjects = filteredObjectMetadataItems.filter( const standardObjects = filteredObjectMetadataItems.filter(
(item) => !item.isCustom, (item) => !item.isCustom,
); );
const customObjects = filteredObjectMetadataItems.filter( const customObjects = filteredObjectMetadataItems.filter(
@ -135,11 +135,14 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
</StyledSearchContainer> </StyledSearchContainer>
</Section> </Section>
{basicObjects.length > 0 && ( {standardObjects.length > 0 && (
<Section> <Section>
<H2Title title={t`Basics`} description={t`All the basic objects`} /> <H2Title
title={t`Standard`}
description={t`All the standard objects`}
/>
<StyledContainer> <StyledContainer>
{basicObjects.map((objectMetadataItem) => { {standardObjects.map((objectMetadataItem) => {
const Icon = getIcon(objectMetadataItem.icon); const Icon = getIcon(objectMetadataItem.icon);
return ( return (
<StyledCardContainer <StyledCardContainer
@ -151,7 +154,7 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
<SettingsCard <SettingsCard
Icon={ Icon={
<Icon <Icon
size={theme.icon.size.xl} size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm} stroke={theme.icon.stroke.sm}
/> />
} }
@ -181,7 +184,7 @@ export const SettingsRolePermissionsObjectLevelObjectPicker = ({
key={objectMetadataItem.id} key={objectMetadataItem.id}
Icon={ Icon={
<Icon <Icon
size={theme.icon.size.xl} size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm} stroke={theme.icon.stroke.sm}
/> />
} }

View File

@ -1,60 +1,83 @@
import { objectPermissionKeyToHumanReadable } from '@/settings/roles/role-permissions/object-level-permissions/utils/objectPermissionKeyToHumanReadableText';
import { PermissionIcon } from '@/settings/roles/role-permissions/objects-permissions/components/PermissionIcon'; import { PermissionIcon } from '@/settings/roles/role-permissions/objects-permissions/components/PermissionIcon';
import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping'; import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping';
import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig'; import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { AppTooltip, TooltipDelay } from 'twenty-ui/display';
import { ObjectPermission } from '~/generated/graphql'; import { ObjectPermission } from '~/generated/graphql';
const StyledSettingsRolePermissionsObjectLevelOverrideCell = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`; `;
type SettingsRolePermissionsObjectLevelOverrideCellProps = { type SettingsRolePermissionsObjectLevelOverrideCellProps = {
objectPermission: ObjectPermission; objectPermissions: ObjectPermission;
objectPermissionKey: SettingsRoleObjectPermissionKey;
roleId: string; roleId: string;
objectLabel: string;
}; };
export const SettingsRolePermissionsObjectLevelOverrideCell = ({ export const SettingsRolePermissionsObjectLevelOverrideCell = ({
objectPermission, objectPermissions,
objectPermissionKey,
roleId, roleId,
objectLabel,
}: SettingsRolePermissionsObjectLevelOverrideCellProps) => { }: SettingsRolePermissionsObjectLevelOverrideCellProps) => {
const settingsDraftRole = useRecoilValue( const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId), settingsDraftRoleFamilyState(roleId),
); );
const roleLabel = settingsDraftRole.label;
const permissionMappings = const permissionMappings =
SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING; SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING;
const isOverridden = (permission: SettingsRoleObjectPermissionKey) => { const permissionValue = objectPermissions[objectPermissionKey];
const rolePermission = permissionMappings[permission];
const isOverridden = (
objectPermissionKey: SettingsRoleObjectPermissionKey,
) => {
const rolePermission = permissionMappings[objectPermissionKey];
return ( return (
isDefined(objectPermission[permission]) && isDefined(permissionValue) &&
!!settingsDraftRole[rolePermission] !== !!objectPermission[permission] !!settingsDraftRole[rolePermission] !== !!permissionValue
); );
}; };
if (!isOverridden(objectPermissionKey)) {
return null;
}
const humanReadableAction =
objectPermissionKeyToHumanReadable(objectPermissionKey);
const containerId = `object-level-permission-override-${roleId}-${objectPermissionKey}`;
return ( return (
<StyledSettingsRolePermissionsObjectLevelOverrideCell> <>
{( <StyledContainer id={containerId}>
Object.keys(permissionMappings) as SettingsRoleObjectPermissionKey[] <PermissionIcon
).map((permission) => { permission={objectPermissionKey}
const permissionValue = objectPermission[permission]; state={permissionValue === false ? 'revoked' : 'granted'}
/>
if (!isOverridden(permission)) { </StyledContainer>
return null; <AppTooltip
anchorSelect={`#${containerId}`}
content={
permissionValue === false
? t`${roleLabel} can't ${humanReadableAction} ${objectLabel} records`
: t`${roleLabel} can ${humanReadableAction} ${objectLabel} records`
} }
delay={TooltipDelay.shortDelay}
return ( noArrow
<PermissionIcon place="bottom"
key={permission} positionStrategy="fixed"
permission={permission} />
state={permissionValue === false ? 'revoked' : 'granted'} </>
/>
);
})}
</StyledSettingsRolePermissionsObjectLevelOverrideCell>
); );
}; };

View File

@ -0,0 +1,43 @@
import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping';
import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import styled from '@emotion/styled';
import { ObjectPermission } from '~/generated/graphql';
import { SettingsRolePermissionsObjectLevelOverrideCell } from './SettingsRolePermissionsObjectLevelOverrideCell';
const StyledSettingsRolePermissionsObjectLevelOverrideCell = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
type SettingsRolePermissionsObjectLevelOverrideCellContainerProps = {
objectPermissions: ObjectPermission;
roleId: string;
objectLabel: string;
};
export const SettingsRolePermissionsObjectLevelOverrideCellContainer = ({
objectPermissions,
roleId,
objectLabel,
}: SettingsRolePermissionsObjectLevelOverrideCellContainerProps) => {
const permissionMappings =
SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING;
return (
<StyledSettingsRolePermissionsObjectLevelOverrideCell>
{(
Object.keys(permissionMappings) as SettingsRoleObjectPermissionKey[]
).map((permissionKey) => {
return (
<SettingsRolePermissionsObjectLevelOverrideCell
key={permissionKey}
objectPermissions={objectPermissions}
objectPermissionKey={permissionKey}
roleId={roleId}
objectLabel={objectLabel}
/>
);
})}
</StyledSettingsRolePermissionsObjectLevelOverrideCell>
);
};

View File

@ -84,8 +84,8 @@ export const SettingsRolePermissionsObjectLevelSection = ({
return ( return (
<Section> <Section>
<H2Title <H2Title
title={t`Object-Level Permissions`} title={t`Object-Level`}
description={t`Ability to interact with specific objects`} description={t`Actions users can perform on specific objects`}
/> />
<Table> <Table>
<SettingsRolePermissionsObjectLevelTableHeader /> <SettingsRolePermissionsObjectLevelTableHeader />
@ -103,7 +103,7 @@ export const SettingsRolePermissionsObjectLevelSection = ({
/> />
)) ))
) : ( ) : (
<StyledNoOverride>{t`No overrides found`}</StyledNoOverride> <StyledNoOverride>{t`No permissions found`}</StyledNoOverride>
)} )}
</StyledTableRows> </StyledTableRows>
</Table> </Table>

View File

@ -1,5 +1,5 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelOverrideCell } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCell'; import { SettingsRolePermissionsObjectLevelOverrideCellContainer } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCellContainer';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
@ -44,6 +44,8 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
const Icon = getIcon(objectMetadataItem.icon); const Icon = getIcon(objectMetadataItem.icon);
const objectLabel = objectMetadataItem.labelPlural;
return ( return (
<TableRow <TableRow
to={getSettingsPath(SettingsPath.RoleObjectLevel, { to={getSettingsPath(SettingsPath.RoleObjectLevel, {
@ -60,14 +62,15 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({
stroke={theme.icon.stroke.sm} stroke={theme.icon.stroke.sm}
/> />
)} )}
<StyledNameLabel title={objectMetadataItem.labelPlural}> <StyledNameLabel title={objectLabel}>
<OverflowingTextWithTooltip text={objectMetadataItem.labelPlural} /> <OverflowingTextWithTooltip text={objectLabel} />
</StyledNameLabel> </StyledNameLabel>
</StyledNameTableCell> </StyledNameTableCell>
<TableCell> <TableCell>
<SettingsRolePermissionsObjectLevelOverrideCell <SettingsRolePermissionsObjectLevelOverrideCellContainer
objectPermission={objectPermission} objectPermissions={objectPermission}
roleId={roleId} roleId={roleId}
objectLabel={objectLabel}
/> />
</TableCell> </TableCell>
<TableCell align={'right'}> <TableCell align={'right'}>

View File

@ -120,8 +120,8 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
return ( return (
<Section> <Section>
<H2Title <H2Title
title={t`Object-Level Permissions`} title={t`Object-Level`}
description={t`Ability to interact with this specific object`} description={t`Actions users can perform on this object`}
/> />
<StyledTable> <StyledTable>
<SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader /> <SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader />

View File

@ -1,4 +1,5 @@
import { OverridableCheckbox } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox'; import { OverridableCheckbox } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox';
import { objectPermissionKeyToHumanReadable } from '@/settings/roles/role-permissions/object-level-permissions/utils/objectPermissionKeyToHumanReadableText';
import { PermissionIcon } from '@/settings/roles/role-permissions/objects-permissions/components/PermissionIcon'; import { PermissionIcon } from '@/settings/roles/role-permissions/objects-permissions/components/PermissionIcon';
import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping'; import { SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping';
import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig'; import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
@ -13,9 +14,10 @@ import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated-metadata/graphql'; import { ObjectPermission } from '~/generated-metadata/graphql';
import type { Role } from '~/generated/graphql'; import type { Role } from '~/generated/graphql';
const StyledTableRow = styled(TableRow)` const StyledTableRow = styled(TableRow)<{ isDisabled: boolean }>`
align-items: center; align-items: center;
display: flex; display: flex;
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
`; `;
const StyledPermissionCell = styled(TableCell)` const StyledPermissionCell = styled(TableCell)`
@ -93,6 +95,15 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
settingsDraftRoleGlobalPermissionValue === true && settingsDraftRoleGlobalPermissionValue === true &&
isChecked === false; isChecked === false;
const isGranted =
isDefined(settingsDraftRoleObjectPermissionValue) &&
settingsDraftRoleGlobalPermissionValue === false &&
isChecked === true;
const isGrantedAndInherited =
settingsDraftRoleObjectPermissionValue !== false &&
settingsDraftRoleGlobalPermissionValue === true;
let checkboxType: OverridableCheckboxType; let checkboxType: OverridableCheckboxType;
if ( if (
@ -118,8 +129,12 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
} }
}; };
const humanReadableAction = objectPermissionKeyToHumanReadable(
permission.key as SettingsRoleObjectPermissionKey,
);
return ( return (
<StyledTableRow> <StyledTableRow onClick={handleCheckboxChange} isDisabled={!isEditable}>
<StyledPermissionCell> <StyledPermissionCell>
<StyledPermissionContent> <StyledPermissionContent>
<PermissionIcon <PermissionIcon
@ -134,10 +149,20 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
{' · '} {' · '}
{t`Revoked for this object`} {t`Revoked for this object`}
</> </>
) : isGranted ? (
<>
{' · '}
{t`Granted for this object`}
</>
) : isGrantedAndInherited ? (
<>
{' · '}
{t`This role can ${humanReadableAction} all records`}
</>
) : null} ) : null}
</StyledOverrideInfo> </StyledOverrideInfo>
</StyledPermissionCell> </StyledPermissionCell>
<StyledCheckboxCell> <StyledCheckboxCell onClick={(e) => e.stopPropagation()}>
<OverridableCheckbox <OverridableCheckbox
onChange={handleCheckboxChange} onChange={handleCheckboxChange}
disabled={!isEditable} disabled={!isEditable}

View File

@ -0,0 +1,15 @@
import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { t } from '@lingui/core/macro';
export const objectPermissionKeyToHumanReadable = (
objectPermissionKey: SettingsRoleObjectPermissionKey,
) => {
const permissionAction: Record<SettingsRoleObjectPermissionKey, string> = {
canReadObjectRecords: t`see`,
canUpdateObjectRecords: t`update`,
canSoftDeleteObjectRecords: t`delete`,
canDestroyObjectRecords: t`destroy`,
};
return permissionAction[objectPermissionKey];
};

View File

@ -36,6 +36,12 @@ export const SettingsRolePermissionsObjectsSection = ({
{ {
key: 'canReadObjectRecords', key: 'canReadObjectRecords',
label: t`See Records on All Objects`, label: t`See Records on All Objects`,
grantedBy:
objectPermissions?.filter(
(permission) =>
permission.canReadObjectRecords === true &&
settingsDraftRole.canReadAllObjectRecords === false,
)?.length ?? 0,
revokedBy: revokedBy:
objectPermissions?.filter( objectPermissions?.filter(
(permission) => (permission) =>
@ -60,6 +66,12 @@ export const SettingsRolePermissionsObjectsSection = ({
{ {
key: 'canUpdateObjectRecords', key: 'canUpdateObjectRecords',
label: t`Edit Records on All Objects`, label: t`Edit Records on All Objects`,
grantedBy:
objectPermissions?.filter(
(permission) =>
permission.canUpdateObjectRecords === true &&
settingsDraftRole.canUpdateAllObjectRecords === false,
)?.length ?? 0,
revokedBy: revokedBy:
objectPermissions?.filter( objectPermissions?.filter(
(permission) => (permission) =>
@ -82,6 +94,12 @@ export const SettingsRolePermissionsObjectsSection = ({
{ {
key: 'canSoftDeleteObjectRecords', key: 'canSoftDeleteObjectRecords',
label: t`Delete Records on All Objects`, label: t`Delete Records on All Objects`,
grantedBy:
objectPermissions?.filter(
(permission) =>
permission.canSoftDeleteObjectRecords === true &&
settingsDraftRole.canSoftDeleteAllObjectRecords === false,
)?.length ?? 0,
revokedBy: revokedBy:
objectPermissions?.filter( objectPermissions?.filter(
(permission) => (permission) =>
@ -104,6 +122,12 @@ export const SettingsRolePermissionsObjectsSection = ({
{ {
key: 'canDestroyObjectRecords', key: 'canDestroyObjectRecords',
label: t`Destroy Records on All Objects`, label: t`Destroy Records on All Objects`,
grantedBy:
objectPermissions?.filter(
(permission) =>
permission.canDestroyObjectRecords === true &&
settingsDraftRole.canDestroyAllObjectRecords === false,
)?.length ?? 0,
revokedBy: revokedBy:
objectPermissions?.filter( objectPermissions?.filter(
(permission) => (permission) =>
@ -128,8 +152,8 @@ export const SettingsRolePermissionsObjectsSection = ({
return ( return (
<Section> <Section>
<H2Title <H2Title
title={t`Objects`} title={t`All objects`}
description={t`Actions you can perform on all objects`} description={t`Actions users can perform on all objects`}
/> />
<StyledTable> <StyledTable>
<SettingsRolePermissionsObjectsTableHeader <SettingsRolePermissionsObjectsTableHeader

View File

@ -15,7 +15,7 @@ const StyledActionsHeader = styled(TableHeader)`
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(4)}; padding-right: ${({ theme }) => theme.spacing(1)};
`; `;
type SettingsRolePermissionsObjectsTableHeaderProps = { type SettingsRolePermissionsObjectsTableHeaderProps = {

View File

@ -36,12 +36,13 @@ const StyledCheckboxCell = styled(TableCell)`
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(4)}; padding-right: ${({ theme }) => theme.spacing(1)};
`; `;
const StyledTableRow = styled(TableRow)` const StyledTableRow = styled(TableRow)<{ isDisabled: boolean }>`
align-items: center; align-items: center;
display: flex; display: flex;
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
`; `;
type SettingsRolePermissionsObjectsTableRowProps = { type SettingsRolePermissionsObjectsTableRowProps = {
@ -54,13 +55,21 @@ export const SettingsRolePermissionsObjectsTableRow = ({
isEditable, isEditable,
}: SettingsRolePermissionsObjectsTableRowProps) => { }: SettingsRolePermissionsObjectsTableRowProps) => {
const revokedBy = permission.revokedBy; const revokedBy = permission.revokedBy;
const grantedBy = permission.grantedBy;
const isRevoked = const isRevoked =
revokedBy !== undefined && revokedBy !== null && revokedBy > 0; revokedBy !== undefined && revokedBy !== null && revokedBy > 0;
const label = permission.label; const label = permission.label;
const pluralizedObject = pluralize('object', revokedBy); const pluralizedRevokedObject = pluralize('object', revokedBy);
const pluralizedGrantedObject = pluralize('object', grantedBy);
const isDisabled = !isEditable;
const handleRowClick = () => {
if (isDisabled) return;
permission.setValue(!permission.value);
};
return ( return (
<StyledTableRow> <StyledTableRow onClick={handleRowClick} isDisabled={isDisabled}>
<StyledPermissionCell> <StyledPermissionCell>
<StyledPermissionContent> <StyledPermissionContent>
<PermissionIcon <PermissionIcon
@ -70,19 +79,24 @@ export const SettingsRolePermissionsObjectsTableRow = ({
<StyledPermissionLabel>{label}</StyledPermissionLabel> <StyledPermissionLabel>{label}</StyledPermissionLabel>
</StyledPermissionContent> </StyledPermissionContent>
<StyledOverrideInfo> <StyledOverrideInfo>
{isRevoked ? ( {isRevoked && revokedBy > 0 ? (
<> <>
{' · '} {' · '}
{t`Revoked on ${revokedBy} ${pluralizedObject}`} {t`Revoked for ${revokedBy} ${pluralizedRevokedObject}`}
</>
) : grantedBy && grantedBy > 0 ? (
<>
{' · '}
{t`Granted for ${grantedBy} ${pluralizedGrantedObject}`}
</> </>
) : null} ) : null}
</StyledOverrideInfo> </StyledOverrideInfo>
</StyledPermissionCell> </StyledPermissionCell>
<StyledCheckboxCell> <StyledCheckboxCell onClick={(e) => e.stopPropagation()}>
<Checkbox <Checkbox
checked={permission.value ?? false} checked={permission.value ?? false}
onChange={() => permission.setValue(!permission.value)} onChange={() => permission.setValue(!permission.value)}
disabled={!isEditable} disabled={isDisabled}
accent={isRevoked ? CheckboxAccent.Orange : CheckboxAccent.Blue} accent={isRevoked ? CheckboxAccent.Orange : CheckboxAccent.Blue}
/> />
</StyledCheckboxCell> </StyledCheckboxCell>

View File

@ -4,6 +4,7 @@ export type SettingsRolePermissionsObjectPermission = {
label: string | ReactNode; label: string | ReactNode;
value?: boolean; value?: boolean;
setValue: (value: boolean) => void; setValue: (value: boolean) => void;
grantedBy?: number;
revokedBy?: number; revokedBy?: number;
}; };

View File

@ -130,7 +130,11 @@ export const SettingsRolePermissionsSettingsSection = ({
containAnimation={false} containAnimation={false}
> >
<StyledTable> <StyledTable>
<SettingsRolePermissionsSettingsTableHeader /> <SettingsRolePermissionsSettingsTableHeader
roleId={roleId}
settingsPermissionsConfig={settingsPermissionsConfig}
isEditable={isEditable}
/>
<StyledTableRows> <StyledTableRows>
{settingsPermissionsConfig.map((permission) => ( {settingsPermissionsConfig.map((permission) => (
<SettingsRolePermissionsSettingsTableRow <SettingsRolePermissionsSettingsTableRow

View File

@ -1,11 +1,77 @@
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { Checkbox } from 'twenty-ui/input';
export const SettingsRolePermissionsSettingsTableHeader = () => ( import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission';
<TableRow gridAutoColumns="3fr 4fr 24px"> import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
<TableHeader>{t`Name`}</TableHeader> import { useRecoilState } from 'recoil';
<TableHeader>{t`Description`}</TableHeader> import { v4 } from 'uuid';
<TableHeader></TableHeader>
</TableRow> type SettingsRolePermissionsSettingsTableHeaderProps = {
); roleId: string;
settingsPermissionsConfig: SettingsRolePermissionsSettingPermission[];
isEditable: boolean;
};
const StyledActionsHeader = styled(TableHeader)`
align-items: center;
display: flex;
justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(1)};
`;
export const SettingsRolePermissionsSettingsTableHeader = ({
roleId,
settingsPermissionsConfig,
isEditable,
}: SettingsRolePermissionsSettingsTableHeaderProps) => {
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const allSettingsPermissionsEnabled = settingsPermissionsConfig.every(
(permission) =>
settingsDraftRole.settingPermissions?.some(
(settingPermission) => settingPermission.setting === permission.key,
),
);
const someSettingsPermissionsEnabled = settingsPermissionsConfig.some(
(permission) =>
settingsDraftRole.settingPermissions?.some(
(settingPermission) => settingPermission.setting === permission.key,
),
);
return (
<TableRow gridAutoColumns="3fr 4fr 24px">
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Description`}</TableHeader>
<StyledActionsHeader aria-label={t`Actions`}>
<Checkbox
checked={allSettingsPermissionsEnabled}
indeterminate={
someSettingsPermissionsEnabled && !allSettingsPermissionsEnabled
}
disabled={!isEditable}
aria-label={t`Toggle all settings permissions`}
onChange={() => {
const newValue = !allSettingsPermissionsEnabled;
setSettingsDraftRole({
...settingsDraftRole,
settingPermissions: newValue
? settingsPermissionsConfig.map((permission) => ({
id: v4(),
setting: permission.key,
roleId,
}))
: [],
});
}}
/>
</StyledActionsHeader>
</TableRow>
);
};

View File

@ -10,6 +10,10 @@ import { Checkbox } from 'twenty-ui/input';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledTableRow = styled(TableRow)<{ isDisabled: boolean }>`
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
`;
const StyledName = styled.span` const StyledName = styled.span`
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
`; `;
@ -29,7 +33,7 @@ const StyledCheckboxCell = styled(TableCell)`
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding-right: ${({ theme }) => theme.spacing(4)}; padding-right: ${({ theme }) => theme.spacing(1)};
`; `;
const StyledIconContainer = styled.div` const StyledIconContainer = styled.div`
@ -63,6 +67,10 @@ export const SettingsRolePermissionsSettingsTableRow = ({
(settingPermission) => settingPermission.setting === permission.key, (settingPermission) => settingPermission.setting === permission.key,
) ?? false; ) ?? false;
const isChecked = isSettingPermissionEnabled || canUpdateAllSettings;
const isDisabled =
!isEditable || canUpdateAllSettings || !isPermissionsV2Enabled;
const handleChange = (value: boolean) => { const handleChange = (value: boolean) => {
const currentPermissions = settingsDraftRole.settingPermissions ?? []; const currentPermissions = settingsDraftRole.settingPermissions ?? [];
@ -88,8 +96,18 @@ export const SettingsRolePermissionsSettingsTableRow = ({
} }
}; };
const handleRowClick = () => {
if (isDisabled) return;
handleChange(!isChecked);
};
return ( return (
<TableRow key={permission.key} gridAutoColumns="3fr 4fr 24px"> <StyledTableRow
key={permission.key}
gridAutoColumns="3fr 4fr 24px"
onClick={handleRowClick}
isDisabled={isDisabled}
>
<StyledPermissionCell> <StyledPermissionCell>
<StyledIconContainer> <StyledIconContainer>
<permission.Icon <permission.Icon
@ -103,15 +121,13 @@ export const SettingsRolePermissionsSettingsTableRow = ({
<StyledPermissionCell> <StyledPermissionCell>
<StyledDescription>{permission.description}</StyledDescription> <StyledDescription>{permission.description}</StyledDescription>
</StyledPermissionCell> </StyledPermissionCell>
<StyledCheckboxCell> <StyledCheckboxCell onClick={(e) => e.stopPropagation()}>
<Checkbox <Checkbox
checked={isSettingPermissionEnabled || canUpdateAllSettings} checked={isChecked}
disabled={ disabled={isDisabled}
!isEditable || canUpdateAllSettings || !isPermissionsV2Enabled
}
onChange={(event) => handleChange(event.target.checked)} onChange={(event) => handleChange(event.target.checked)}
/> />
</StyledCheckboxCell> </StyledCheckboxCell>
</TableRow> </StyledTableRow>
); );
}; };

View File

@ -57,7 +57,7 @@ const ROLE_BASIC_KEYS: Array<keyof Role> = [
export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const activeTabId = useRecoilComponentValueV2( const activeTabId = useRecoilComponentValueV2(
activeTabIdComponentState, activeTabIdComponentState,
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID, SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID + '-' + roleId,
); );
const isPermissionsV2Enabled = useIsFeatureEnabled( const isPermissionsV2Enabled = useIsFeatureEnabled(
@ -94,16 +94,16 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
const isRoleEditable = isPermissionsV2Enabled && settingsDraftRole.isEditable; const isRoleEditable = isPermissionsV2Enabled && settingsDraftRole.isEditable;
const tabs = [ const tabs = [
{
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT,
title: t`Assignment`,
Icon: IconUserPlus,
},
{ {
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS, id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS,
title: t`Permissions`, title: t`Permissions`,
Icon: IconLockOpen, Icon: IconLockOpen,
}, },
{
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT,
title: t`Assignment`,
Icon: IconUserPlus,
},
{ {
id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS, id: SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS,
title: t`Settings`, title: t`Settings`,
@ -135,7 +135,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
if (isCreateMode) { if (isCreateMode) {
const roleId = v4(); const roleId = v4();
createRole({ const { data } = await createRole({
variables: { variables: {
createRoleInput: { createRoleInput: {
id: roleId, id: roleId,
@ -152,60 +152,63 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
settingsDraftRole.canDestroyAllObjectRecords, settingsDraftRole.canDestroyAllObjectRecords,
}, },
}, },
onCompleted: async (data) => { refetchQueries: [getOperationName(GET_ROLES) ?? ''],
if (isDefined(dirtyFields.workspaceMembers)) { });
await addWorkspaceMembersToRole({
if (!data) {
return;
}
if (isDefined(dirtyFields.workspaceMembers)) {
await addWorkspaceMembersToRole({
roleId: data.createOneRole.id,
workspaceMemberIds: settingsDraftRole.workspaceMembers.map(
(member) => member.id,
),
});
}
if (isDefined(dirtyFields.settingPermissions)) {
await upsertSettingPermissions({
variables: {
upsertSettingPermissionsInput: {
roleId: data.createOneRole.id, roleId: data.createOneRole.id,
workspaceMemberIds: settingsDraftRole.workspaceMembers.map( settingPermissionKeys:
(member) => member.id, settingsDraftRole.settingPermissions?.map(
), (settingPermission) => settingPermission.setting,
}); ) ?? [],
} },
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isDefined(dirtyFields.settingPermissions)) { if (isDefined(dirtyFields.objectPermissions)) {
await upsertSettingPermissions({ await upsertObjectPermissions({
variables: { variables: {
upsertSettingPermissionsInput: { upsertObjectPermissionsInput: {
roleId: data.createOneRole.id, roleId: data.createOneRole.id,
settingPermissionKeys: objectPermissions:
settingsDraftRole.settingPermissions?.map( settingsDraftRole.objectPermissions?.map(
(settingPermission) => settingPermission.setting, (objectPermission) => ({
) ?? [], objectMetadataId: objectPermission.objectMetadataId,
}, canReadObjectRecords: objectPermission.canReadObjectRecords,
}, canUpdateObjectRecords:
refetchQueries: [getOperationName(GET_ROLES) ?? ''], objectPermission.canUpdateObjectRecords,
}); canSoftDeleteObjectRecords:
} objectPermission.canSoftDeleteObjectRecords,
canDestroyObjectRecords:
objectPermission.canDestroyObjectRecords,
}),
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
if (isDefined(dirtyFields.objectPermissions)) { navigateSettings(SettingsPath.RoleDetail, {
await upsertObjectPermissions({ roleId: data.createOneRole.id,
variables: {
upsertObjectPermissionsInput: {
roleId: data.createOneRole.id,
objectPermissions:
settingsDraftRole.objectPermissions?.map(
(objectPermission) => ({
objectMetadataId: objectPermission.objectMetadataId,
canReadObjectRecords:
objectPermission.canReadObjectRecords,
canUpdateObjectRecords:
objectPermission.canUpdateObjectRecords,
canSoftDeleteObjectRecords:
objectPermission.canSoftDeleteObjectRecords,
canDestroyObjectRecords:
objectPermission.canDestroyObjectRecords,
}),
) ?? [],
},
},
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
});
}
navigateSettings(SettingsPath.RoleDetail, {
roleId: data.createOneRole.id,
});
},
}); });
} else { } else {
if (isDefined(dirtyFields.settingPermissions)) { if (isDefined(dirtyFields.settingPermissions)) {
@ -244,6 +247,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
}, },
}, },
}, },
refetchQueries: [getOperationName(GET_ROLES) ?? ''],
}); });
} }
@ -302,7 +306,9 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
<TabList <TabList
tabs={tabs} tabs={tabs}
className="tab-list" className="tab-list"
componentInstanceId={SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID} componentInstanceId={
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID + '-' + roleId
}
/> />
{activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT && ( {activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT && (
<SettingsRoleAssignment roleId={roleId} isCreateMode={isCreateMode} /> <SettingsRoleAssignment roleId={roleId} isCreateMode={isCreateMode} />

View File

@ -18,7 +18,7 @@ export const SettingsRoleCreateEffect = ({
); );
const setActiveTabId = useSetRecoilComponentStateV2( const setActiveTabId = useSetRecoilComponentStateV2(
activeTabIdComponentState, activeTabIdComponentState,
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID, SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID + '-' + roleId,
); );
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);

View File

@ -22,7 +22,7 @@ export const SettingsRoleEditEffect = ({
const role = useRecoilValue(settingsPersistedRoleFamilyState(roleId)); const role = useRecoilValue(settingsPersistedRoleFamilyState(roleId));
const setActiveTabId = useSetRecoilComponentStateV2( const setActiveTabId = useSetRecoilComponentStateV2(
activeTabIdComponentState, activeTabIdComponentState,
SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID, SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID + '-' + roleId,
); );
const updateDraftRoleIfNeeded = useRecoilCallback( const updateDraftRoleIfNeeded = useRecoilCallback(
@ -45,7 +45,7 @@ export const SettingsRoleEditEffect = ({
return; return;
} }
setActiveTabId(SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT); setActiveTabId(SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.PERMISSIONS);
updateDraftRoleIfNeeded(role); updateDraftRoleIfNeeded(role);
setIsInitialized(true); setIsInitialized(true);
}, [isInitialized, role, setActiveTabId, updateDraftRoleIfNeeded]); }, [isInitialized, role, setActiveTabId, updateDraftRoleIfNeeded]);

View File

@ -1,8 +1,8 @@
export const SETTINGS_ROLE_DETAIL_TABS = { export const SETTINGS_ROLE_DETAIL_TABS = {
COMPONENT_INSTANCE_ID: 'settings-role-detail-tabs', COMPONENT_INSTANCE_ID: 'settings-role-detail-tabs',
TABS_IDS: { TABS_IDS: {
ASSIGNMENT: 'assignment',
PERMISSIONS: 'permissions', PERMISSIONS: 'permissions',
ASSIGNMENT: 'assignment',
SETTINGS: 'settings', SETTINGS: 'settings',
}, },
} as const; } as const;