Role page various fixes (#12324)

Various fixes from fast follows

- Sort roles by alphabetical order
- Change some tooltips
- During role creation, role should have all permissions enabled by
default
- Changed Permission icons design and refactored duplicating logic in a
dedicated component
- Changed "Revoked by" design
- Display role icon in default role picker
- Workspace member avatar was missing in role list and member picker
- Set "seeded" member role as editable for new workspaces
- Various css fixes
This commit is contained in:
Weiko
2025-05-27 17:58:55 +02:00
committed by GitHub
parent 8051646567
commit f210d274bf
16 changed files with 214 additions and 137 deletions

View File

@ -7,7 +7,7 @@ import { Select } from '@/ui/input/components/Select';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { H2Title, IconUserPin } from 'twenty-ui/display'; import { H2Title, IconUserPin, useIcons } from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout'; import { Card, Section } from 'twenty-ui/layout';
import { import {
Role, Role,
@ -51,10 +51,18 @@ export const SettingsRoleDefaultRole = ({
}); });
}; };
const { getIcon } = useIcons();
if (!currentWorkspace || !defaultRole) { if (!currentWorkspace || !defaultRole) {
return null; return null;
} }
const options = roles.map((role) => ({
label: role.label,
value: role.id,
Icon: getIcon(role.icon),
}));
return ( return (
<Section> <Section>
<H2Title <H2Title
@ -71,10 +79,7 @@ export const SettingsRoleDefaultRole = ({
selectSizeVariant="small" selectSizeVariant="small"
withSearchInput withSearchInput
dropdownId="default-role-select" dropdownId="default-role-select"
options={roles.map((role) => ({ options={options}
label: role.label,
value: role.id,
}))}
value={defaultRole?.id ?? ''} value={defaultRole?.id ?? ''}
onChange={(value) => onChange={(value) =>
updateDefaultRole(value as string, currentWorkspace) updateDefaultRole(value as string, currentWorkspace)

View File

@ -9,11 +9,12 @@ import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableCell } from '@/ui/layout/table/components/TableCell';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { H2Title, IconPlus } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { FeatureFlagKey } from '~/generated/graphql'; import { FeatureFlagKey } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { Button } from 'twenty-ui/input'; import { sortByAscString } from '~/utils/array/sortByAscString';
import { H2Title, IconPlus } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
const StyledCreateRoleSection = styled(Section)` const StyledCreateRoleSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light}; border-top: 1px solid ${({ theme }) => theme.border.color.light};
@ -40,6 +41,10 @@ export const SettingsRolesList = () => {
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector); const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
const sortedSettingsAllRoles = [...settingsAllRoles].sort((a, b) =>
sortByAscString(a.label, b.label),
);
return ( return (
<Section> <Section>
<H2Title <H2Title
@ -49,10 +54,10 @@ export const SettingsRolesList = () => {
<Table> <Table>
<SettingsRolesTableHeader /> <SettingsRolesTableHeader />
<StyledTableRows> <StyledTableRows>
{settingsAllRoles.length === 0 ? ( {sortedSettingsAllRoles.length === 0 ? (
<StyledNoRoles>{t`No roles found`}</StyledNoRoles> <StyledNoRoles>{t`No roles found`}</StyledNoRoles>
) : ( ) : (
settingsAllRoles.map((role) => ( sortedSettingsAllRoles.map((role) => (
<SettingsRolesTableRow key={role.id} role={role} /> <SettingsRolesTableRow key={role.id} role={role} />
)) ))
)} )}

View File

@ -111,11 +111,8 @@ export const SettingsRolesTableRow = ({ role }: SettingsRolesTableRowProps) => {
<TableCell align={'left'}> <TableCell align={'left'}>
<StyledAssignedText>{role.workspaceMembers.length}</StyledAssignedText> <StyledAssignedText>{role.workspaceMembers.length}</StyledAssignedText>
</TableCell> </TableCell>
<TableCell align={'right'}> <TableCell align={'right'} color={theme.font.color.tertiary}>
<IconChevronRight <IconChevronRight size={theme.icon.size.md} />
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</TableCell> </TableCell>
</StyledTableRow> </StyledTableRow>
); );

View File

@ -243,6 +243,7 @@ export const SettingsRoleAssignment = ({
<Dropdown <Dropdown
dropdownId="role-member-select" dropdownId="role-member-select"
dropdownHotkeyScope={{ scope: 'roleAssignment' }} dropdownHotkeyScope={{ scope: 'roleAssignment' }}
dropdownOffset={{ x: 0, y: 4 }}
clickableComponent={ clickableComponent={
<> <>
<div id="assign-member"> <div id="assign-member">
@ -256,7 +257,7 @@ export const SettingsRoleAssignment = ({
</div> </div>
<AppTooltip <AppTooltip
anchorSelect="#assign-member" anchorSelect="#assign-member"
content={t`No more members to assign`} content={t`The workspace needs at least one Admin`}
delay={TooltipDelay.noDelay} delay={TooltipDelay.noDelay}
hidden={!allWorkspaceMembersHaveThisRole} hidden={!allWorkspaceMembersHaveThisRole}
/> />

View File

@ -1,8 +1,8 @@
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';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { WorkspaceMember } from '~/generated-metadata/graphql';
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui/display'; import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui/display';
import { WorkspaceMember } from '~/generated-metadata/graphql';
const StyledIconWrapper = styled.div` const StyledIconWrapper = styled.div`
align-items: center; align-items: center;

View File

@ -1,6 +1,6 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { SearchRecord } from '~/generated-metadata/graphql';
import { MenuItem, MenuItemAvatar } from 'twenty-ui/navigation'; import { MenuItem, MenuItemAvatar } from 'twenty-ui/navigation';
import { SearchRecord } from '~/generated-metadata/graphql';
type SettingsRoleAssignmentWorkspaceMemberPickerDropdownContentProps = { type SettingsRoleAssignmentWorkspaceMemberPickerDropdownContentProps = {
loading: boolean; loading: boolean;
@ -34,6 +34,7 @@ export const SettingsRoleAssignmentWorkspaceMemberPickerDropdownContent = ({
size: 'md', size: 'md',
placeholder: workspaceMember.label ?? '', placeholder: workspaceMember.label ?? '',
placeholderColorSeed: workspaceMember.recordId, placeholderColorSeed: workspaceMember.recordId,
avatarUrl: workspaceMember.imageUrl,
}} }}
text={workspaceMember.label} text={workspaceMember.label}
/> />

View File

@ -1,34 +1,12 @@
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 { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } 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 { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated/graphql'; import { ObjectPermission } from '~/generated/graphql';
const StyledIconWrapper = styled.div<{ isForbidden?: boolean }>`
align-items: center;
background: ${({ theme, isForbidden }) =>
isForbidden ? theme.adaptiveColors.orange1 : theme.adaptiveColors.blue1};
border: 1px solid
${({ theme, isForbidden }) =>
isForbidden ? theme.adaptiveColors.orange3 : theme.adaptiveColors.blue3};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
height: ${({ theme }) => theme.spacing(4)};
justify-content: center;
width: ${({ theme }) => theme.spacing(4)};
`;
const StyledIcon = styled.div<{ isForbidden?: boolean }>`
align-items: center;
display: flex;
color: ${({ theme, isForbidden }) =>
isForbidden ? theme.color.orange : theme.color.blue};
justify-content: center;
`;
const StyledSettingsRolePermissionsObjectLevelOverrideCell = styled.div` const StyledSettingsRolePermissionsObjectLevelOverrideCell = styled.div`
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
@ -43,8 +21,6 @@ export const SettingsRolePermissionsObjectLevelOverrideCell = ({
objectPermission, objectPermission,
roleId, roleId,
}: SettingsRolePermissionsObjectLevelOverrideCellProps) => { }: SettingsRolePermissionsObjectLevelOverrideCellProps) => {
const theme = useTheme();
const settingsDraftRole = useRecoilValue( const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId), settingsDraftRoleFamilyState(roleId),
); );
@ -52,44 +28,33 @@ export const SettingsRolePermissionsObjectLevelOverrideCell = ({
const permissionMappings = const permissionMappings =
SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING; SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING;
type ObjectPermissionKey = keyof typeof permissionMappings; const isOverridden = (permission: SettingsRoleObjectPermissionKey) => {
const isOverridden = (permission: ObjectPermissionKey) => {
const rolePermission = permissionMappings[permission]; const rolePermission = permissionMappings[permission];
return ( return (
isDefined(objectPermission[permission]) && isDefined(objectPermission[permission]) &&
!!settingsDraftRole[rolePermission as keyof typeof settingsDraftRole] !== !!settingsDraftRole[rolePermission] !== !!objectPermission[permission]
!!objectPermission[permission]
); );
}; };
return ( return (
<StyledSettingsRolePermissionsObjectLevelOverrideCell> <StyledSettingsRolePermissionsObjectLevelOverrideCell>
{(Object.keys(permissionMappings) as ObjectPermissionKey[]).map( {(
(permission) => { Object.keys(permissionMappings) as SettingsRoleObjectPermissionKey[]
const { Icon, IconForbidden: IconOverride } = ).map((permission) => {
SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission]; const permissionValue = objectPermission[permission];
const permissionValue = objectPermission[permission];
if (!isOverridden(permission)) { if (!isOverridden(permission)) {
return null; return null;
} }
return ( return (
<StyledIconWrapper <PermissionIcon
key={permission} key={permission}
isForbidden={permissionValue === false} permission={permission}
> state={permissionValue === false ? 'revoked' : 'granted'}
<StyledIcon isForbidden={permissionValue === false}> />
{permissionValue === false && ( );
<IconOverride size={theme.icon.size.sm} /> })}
)}
{permissionValue === true && <Icon size={theme.icon.size.sm} />}
</StyledIcon>
</StyledIconWrapper>
);
},
)}
</StyledSettingsRolePermissionsObjectLevelOverrideCell> </StyledSettingsRolePermissionsObjectLevelOverrideCell>
); );
}; };

View File

@ -130,6 +130,7 @@ export const SettingsRolePermissionsObjectLevelSection = ({
disabled={!isEditable} disabled={!isEditable}
/> />
} }
dropdownOffset={{ x: 0, y: 4 }}
dropdownComponents={ dropdownComponents={
<SettingsRolePermissionsObjectLevelObjectPickerDropdownContent <SettingsRolePermissionsObjectLevelObjectPickerDropdownContent
excludedObjectMetadataIds={ excludedObjectMetadataIds={

View File

@ -1,11 +1,11 @@
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 { 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 { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig'; import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { SettingsRolePermissionsObjectLevelPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission'; import { SettingsRolePermissionsObjectLevelPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
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';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -13,25 +13,36 @@ 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 StyledLabel = styled.span` const StyledTableRow = styled(TableRow)`
color: ${({ theme }) => theme.font.color.primary}; align-items: center;
`; display: flex;
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)` const StyledPermissionCell = styled(TableCell)`
align-items: center; align-items: center;
display: flex; display: flex;
flex: 1; flex: 1;
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)}; padding-left: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledPermissionContent = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledPermissionLabel = styled.span`
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledOverrideInfo = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledCheckboxCell = styled(TableCell)` const StyledCheckboxCell = styled(TableCell)`
align-items: center; align-items: center;
display: flex; display: flex;
@ -39,11 +50,6 @@ const StyledCheckboxCell = styled(TableCell)`
padding-right: ${({ theme }) => theme.spacing(4)}; padding-right: ${({ theme }) => theme.spacing(4)};
`; `;
const StyledTableRow = styled(TableRow)`
align-items: center;
display: flex;
`;
type OverridableCheckboxType = 'no_cta' | 'default' | 'override'; type OverridableCheckboxType = 'no_cta' | 'default' | 'override';
type SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps = { type SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps = {
@ -60,17 +66,12 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
settingsDraftRoleObjectPermissions, settingsDraftRoleObjectPermissions,
roleId, roleId,
}: SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps) => { }: SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps) => {
const theme = useTheme();
const settingsDraftRole = useRecoilValue( const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId), settingsDraftRoleFamilyState(roleId),
); );
const label = permission.label; const label = permission.label;
const { Icon } =
SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission.key];
const permissionMappings = const permissionMappings =
SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING; SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING;
@ -120,13 +121,21 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow =
return ( return (
<StyledTableRow> <StyledTableRow>
<StyledPermissionCell> <StyledPermissionCell>
<Icon size={theme.icon.size.sm} /> <StyledPermissionContent>
<StyledLabel>{label}</StyledLabel> <PermissionIcon
{isRevoked ? ( permission={permission.key as SettingsRoleObjectPermissionKey}
<StyledOverrideInfo> state={isRevoked ? 'revoked' : 'granted'}
{t`Revoked for this object`} />
</StyledOverrideInfo> <StyledPermissionLabel>{label}</StyledPermissionLabel>
) : null} </StyledPermissionContent>
<StyledOverrideInfo>
{isRevoked ? (
<>
{' · '}
{t`Revoked for this object`}
</>
) : null}
</StyledOverrideInfo>
</StyledPermissionCell> </StyledPermissionCell>
<StyledCheckboxCell> <StyledCheckboxCell>
<OverridableCheckbox <OverridableCheckbox

View File

@ -0,0 +1,51 @@
import {
SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG,
SettingsRoleObjectPermissionKey,
} from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
type PermissionIconProps = {
permission: SettingsRoleObjectPermissionKey;
state: 'granted' | 'revoked';
};
const StyledIconWrapper = styled.div<{ isRevoked?: boolean }>`
align-items: center;
background: ${({ theme, isRevoked }) =>
isRevoked ? theme.adaptiveColors.orange1 : theme.adaptiveColors.blue1};
border: 1px solid
${({ theme, isRevoked }) =>
isRevoked ? theme.adaptiveColors.orange3 : theme.adaptiveColors.blue3};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
height: ${({ theme }) => theme.spacing(4)};
justify-content: center;
width: ${({ theme }) => theme.spacing(4)};
`;
const StyledIcon = styled.div<{ isRevoked?: boolean }>`
align-items: center;
display: flex;
color: ${({ theme, isRevoked }) =>
isRevoked ? theme.color.orange : theme.color.blue};
justify-content: center;
`;
export const PermissionIcon = ({ permission, state }: PermissionIconProps) => {
const theme = useTheme();
const { Icon, IconForbidden } =
SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission];
const isRevoked = state === 'revoked';
return (
<StyledIconWrapper isRevoked={isRevoked}>
<StyledIcon isRevoked={isRevoked}>
{isRevoked && <IconForbidden size={theme.icon.size.sm} />}
{!isRevoked && <Icon size={theme.icon.size.sm} />}
</StyledIcon>
</StyledIconWrapper>
);
};

View File

@ -1,32 +1,37 @@
import { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig'; import { PermissionIcon } from '@/settings/roles/role-permissions/objects-permissions/components/PermissionIcon';
import { SettingsRoleObjectPermissionKey } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission'; import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
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';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { Checkbox, CheckboxAccent } from 'twenty-ui/input'; 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)` const StyledPermissionCell = styled(TableCell)`
align-items: center; align-items: center;
display: flex; display: flex;
flex: 1; flex: 1;
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)}; padding-left: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledPermissionContent = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledPermissionLabel = styled.span`
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledOverrideInfo = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledCheckboxCell = styled(TableCell)` const StyledCheckboxCell = styled(TableCell)`
align-items: center; align-items: center;
display: flex; display: flex;
@ -48,25 +53,30 @@ export const SettingsRolePermissionsObjectsTableRow = ({
permission, permission,
isEditable, isEditable,
}: SettingsRolePermissionsObjectsTableRowProps) => { }: SettingsRolePermissionsObjectsTableRowProps) => {
const theme = useTheme();
const revokedBy = permission.revokedBy; const revokedBy = permission.revokedBy;
const isRevoked = revokedBy && revokedBy > 0; const isRevoked =
revokedBy !== undefined && revokedBy !== null && revokedBy > 0;
const label = permission.label; const label = permission.label;
const pluralizedObject = pluralize('object', revokedBy); const pluralizedObject = pluralize('object', revokedBy);
const { Icon } = SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission.key];
return ( return (
<StyledTableRow> <StyledTableRow>
<StyledPermissionCell> <StyledPermissionCell>
<Icon size={theme.icon.size.sm} /> <StyledPermissionContent>
<StyledLabel>{label}</StyledLabel> <PermissionIcon
{isRevoked ? ( permission={permission.key as SettingsRoleObjectPermissionKey}
<StyledOverrideInfo> state={isRevoked ? 'revoked' : 'granted'}
{t`Revoked on ${revokedBy} ${pluralizedObject}`} />
</StyledOverrideInfo> <StyledPermissionLabel>{label}</StyledPermissionLabel>
) : null} </StyledPermissionContent>
<StyledOverrideInfo>
{isRevoked ? (
<>
{' · '}
{t`Revoked on ${revokedBy} ${pluralizedObject}`}
</>
) : null}
</StyledOverrideInfo>
</StyledPermissionCell> </StyledPermissionCell>
<StyledCheckboxCell> <StyledCheckboxCell>
<Checkbox <Checkbox

View File

@ -14,8 +14,14 @@ type SettingsRoleObjectPermissionIconConfig = {
IconForbidden: IconComponent; IconForbidden: IconComponent;
}; };
export type SettingsRoleObjectPermissionKey =
| 'canReadObjectRecords'
| 'canUpdateObjectRecords'
| 'canSoftDeleteObjectRecords'
| 'canDestroyObjectRecords';
export const SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG: Record< export const SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG: Record<
string, SettingsRoleObjectPermissionKey,
SettingsRoleObjectPermissionIconConfig SettingsRoleObjectPermissionIconConfig
> = { > = {
canReadObjectRecords: { canReadObjectRecords: {

View File

@ -34,11 +34,11 @@ export const SettingsRoleCreateEffect = ({
label: '', label: '',
description: '', description: '',
icon: 'IconUser', icon: 'IconUser',
canUpdateAllSettings: false, canUpdateAllSettings: true,
canReadAllObjectRecords: false, canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false, canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: false, canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: false, canDestroyAllObjectRecords: true,
isEditable: true, isEditable: true,
workspaceMembers: [], workspaceMembers: [],
}; };

View File

@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -26,6 +27,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor
ObjectPermissionModule, ObjectPermissionModule,
SettingPermissionModule, SettingPermissionModule,
WorkspacePermissionsCacheModule, WorkspacePermissionsCacheModule,
FileModule,
], ],
providers: [RoleService, RoleResolver], providers: [RoleService, RoleResolver],
exports: [RoleService], exports: [RoleService],

View File

@ -8,8 +8,12 @@ import {
Resolver, Resolver,
} from '@nestjs/graphql'; } from '@nestjs/graphql';
import { buildSignedPath } from 'twenty-shared/utils';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { extractFilenameFromPath } from 'src/engine/core-modules/file/utils/extract-file-id-from-path.utils';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto'; import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -47,6 +51,7 @@ export class RoleResolver {
private readonly featureFlagService: FeatureFlagService, private readonly featureFlagService: FeatureFlagService,
private readonly objectPermissionService: ObjectPermissionService, private readonly objectPermissionService: ObjectPermissionService,
private readonly settingPermissionService: SettingPermissionService, private readonly settingPermissionService: SettingPermissionService,
private readonly fileService: FileService,
) {} ) {}
@Query(() => [RoleDTO]) @Query(() => [RoleDTO])
@ -180,10 +185,29 @@ export class RoleResolver {
@Parent() role: RoleDTO, @Parent() role: RoleDTO,
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
): Promise<WorkspaceMemberWorkspaceEntity[]> { ): Promise<WorkspaceMemberWorkspaceEntity[]> {
return this.userRoleService.getWorkspaceMembersAssignedToRole( const workspaceMembers =
role.id, await this.userRoleService.getWorkspaceMembersAssignedToRole(
workspace.id, role.id,
workspace.id,
);
await Promise.all(
workspaceMembers.map(async (workspaceMember) => {
if (workspaceMember && workspaceMember.avatarUrl) {
const avatarUrlToken = this.fileService.encodeFileToken({
filename: extractFilenameFromPath(workspaceMember.avatarUrl),
workspaceId: workspace.id,
});
workspaceMember.avatarUrl = buildSignedPath({
path: workspaceMember.avatarUrl,
token: avatarUrlToken,
});
}
}),
); );
return workspaceMembers;
} }
private async validatePermissionsV2EnabledOrThrow(workspace: Workspace) { private async validatePermissionsV2EnabledOrThrow(workspace: Workspace) {

View File

@ -218,7 +218,7 @@ export class RoleService {
canUpdateAllObjectRecords: true, canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true, canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true, canDestroyAllObjectRecords: true,
isEditable: false, isEditable: true,
workspaceId, workspaceId,
}); });
} }