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

View File

@ -1891,6 +1891,7 @@ export type Role = {
id: Scalars['String']['output'];
isEditable: Scalars['Boolean']['output'];
label: Scalars['String']['output'];
objectPermissions?: Maybe<Array<ObjectPermission>>;
settingPermissions?: Maybe<Array<SettingPermission>>;
workspaceMembers: Array<WorkspaceMember>;
};

View File

@ -1693,6 +1693,7 @@ export type Role = {
id: Scalars['String'];
isEditable: Scalars['Boolean'];
label: Scalars['String'];
objectPermissions?: Maybe<Array<ObjectPermission>>;
settingPermissions?: Maybe<Array<SettingPermission>>;
workspaceMembers: Array<WorkspaceMember>;
};
@ -2668,6 +2669,8 @@ export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', updateLabPublicFeatureFlag: { __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean } };
export type ObjectPermissionFragmentFragment = { __typename?: 'ObjectPermission', id: string, objectMetadataId: string, roleId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null };
export type RoleFragmentFragment = { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean };
export type SettingPermissionFragmentFragment = { __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string };
@ -2704,7 +2707,7 @@ export type UpsertSettingPermissionsMutation = { __typename?: 'Mutation', upsert
export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, settingPermissions?: Array<{ __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string }> | null }> };
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, settingPermissions?: Array<{ __typename?: 'SettingPermission', id: string, setting: SettingPermissionType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', id: string, objectMetadataId: string, roleId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null }> };
export type CreateApprovedAccessDomainMutationVariables = Exact<{
input: CreateApprovedAccessDomainInput;
@ -3019,6 +3022,17 @@ export const AvailableSsoIdentityProvidersFragmentFragmentDoc = gql`
}
}
`;
export const ObjectPermissionFragmentFragmentDoc = gql`
fragment ObjectPermissionFragment on ObjectPermission {
id
objectMetadataId
roleId
canReadObjectRecords
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
}
`;
export const SettingPermissionFragmentFragmentDoc = gql`
fragment SettingPermissionFragment on SettingPermission {
id
@ -4984,11 +4998,15 @@ export const GetRolesDocument = gql`
settingPermissions {
...SettingPermissionFragment
}
objectPermissions {
...ObjectPermissionFragment
}
}
}
${RoleFragmentFragmentDoc}
${WorkspaceMemberQueryFragmentFragmentDoc}
${SettingPermissionFragmentFragmentDoc}`;
${SettingPermissionFragmentFragmentDoc}
${ObjectPermissionFragmentFragmentDoc}`;
/**
* __useGetRolesQuery__

View File

@ -313,6 +313,12 @@ const SettingsRoleEdit = lazy(() =>
})),
);
const SettingsRoleObjectLevel = lazy(() =>
import('~/pages/settings/roles/SettingsRoleObjectLevel').then((module) => ({
default: module.SettingsRoleObjectLevel,
})),
);
type SettingsRoutesProps = {
isFunctionSettingsEnabled?: boolean;
isAdminPageEnabled?: boolean;
@ -402,6 +408,10 @@ export const SettingsRoutes = ({
path={SettingsPath.RoleCreate}
element={<SettingsRoleCreate />}
/>
<Route
path={SettingsPath.RoleObjectLevel}
element={<SettingsRoleObjectLevel />}
/>
</Route>
<Route
element={

View File

@ -7,15 +7,21 @@ import { Select } from '@/ui/input/components/Select';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { H2Title, IconUserPin } from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import {
Role,
UpdateWorkspaceMutation,
useUpdateWorkspaceMutation,
} from '~/generated/graphql';
import { Card, Section } from 'twenty-ui/layout';
import { H2Title, IconUserPin } from 'twenty-ui/display';
export const SettingsRoleDefaultRole = ({ roles }: { roles: Role[] }) => {
type SettingsRoleDefaultRoleProps = {
roles: Role[];
};
export const SettingsRoleDefaultRole = ({
roles,
}: SettingsRoleDefaultRoleProps) => {
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(

View File

@ -4,8 +4,6 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import React from 'react';
import { Role } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import {
AppTooltip,
Avatar,
@ -14,6 +12,8 @@ import {
TooltipDelay,
useIcons,
} from 'twenty-ui/display';
import { Role } from '~/generated-metadata/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledAssignedText = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
@ -52,15 +52,13 @@ const StyledTableRow = styled(TableRow)`
}
`;
export const SettingsRolesTableRow = ({ role }: { role: Role }) => {
type SettingsRolesTableRowProps = {
role: Role;
};
export const SettingsRolesTableRow = ({ role }: SettingsRolesTableRowProps) => {
const theme = useTheme();
const navigateSettings = useNavigateSettings();
const handleRoleClick = (roleId: string) => {
navigateSettings(SettingsPath.RoleDetail, { roleId });
};
const { getIcon } = useIcons();
const Icon = getIcon(role.icon ?? 'IconUser');
@ -68,7 +66,7 @@ export const SettingsRolesTableRow = ({ role }: { role: Role }) => {
<StyledTableRow
key={role.id}
gridAutoColumns="332px 3fr 2fr 1fr"
onClick={() => handleRoleClick(role.id)}
to={getSettingsPath(SettingsPath.RoleDetail, { roleId: role.id })}
>
<TableCell>
<StyledNameCell>

View File

@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const OBJECT_PERMISSION_FRAGMENT = gql`
fragment ObjectPermissionFragment on ObjectPermission {
id
objectMetadataId
roleId
canReadObjectRecords
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
}
`;

View File

@ -1,3 +1,4 @@
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { SETTING_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/settingPermissionFragment';
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
@ -7,6 +8,7 @@ export const GET_ROLES = gql`
${WORKSPACE_MEMBER_QUERY_FRAGMENT}
${ROLE_FRAGMENT}
${SETTING_PERMISSION_FRAGMENT}
${OBJECT_PERMISSION_FRAGMENT}
query GetRoles {
getRoles {
...RoleFragment
@ -16,6 +18,9 @@ export const GET_ROLES = gql`
settingPermissions {
...SettingPermissionFragment
}
objectPermissions {
...ObjectPermissionFragment
}
}
}
`;

View File

@ -1,34 +1,9 @@
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
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 { SettingsRolePermissionsObjectLevelSection } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection';
import { SettingsRolePermissionsObjectsSection } from '@/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsSection';
import { SettingsRolePermissionsSettingsSection } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsSection';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
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,
} from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import {
FeatureFlagKey,
SettingPermissionType,
} from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledRolePermissionsContainer = styled.div`
display: flex;
@ -36,19 +11,6 @@ const StyledRolePermissionsContainer = styled.div`
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)};
`;
const StyledCard = styled(Card)`
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
type SettingsRolePermissionsProps = {
roleId: string;
isEditable: boolean;
@ -58,168 +20,26 @@ 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`,
Icon: IconCode,
},
{
key: SettingPermissionType.WORKSPACE,
name: t`Workspace`,
description: t`Set global workspace preferences`,
Icon: IconSettings,
},
{
key: SettingPermissionType.WORKSPACE_MEMBERS,
name: t`Users`,
description: t`Add or remove users`,
Icon: IconUsers,
},
{
key: SettingPermissionType.ROLES,
name: t`Roles`,
description: t`Define user roles and access levels`,
Icon: IconLockOpen,
},
{
key: SettingPermissionType.DATA_MODEL,
name: t`Data Model`,
description: t`Edit CRM data structure and fields`,
Icon: IconHierarchy,
},
{
key: SettingPermissionType.ADMIN_PANEL,
name: t`Admin Panel`,
description: t`Admin settings and system tools`,
Icon: IconServer,
},
{
key: SettingPermissionType.SECURITY,
name: t`Security`,
description: t`Manage security policies`,
Icon: IconKey,
},
];
const isPermissionsV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
);
return (
<StyledRolePermissionsContainer>
<Section>
<H2Title
title={t`Objects`}
description={t`Ability to interact with each object`}
<SettingsRolePermissionsObjectsSection
roleId={roleId}
isEditable={isEditable}
/>
{isPermissionsV2Enabled && (
<SettingsRolePermissionsObjectLevelSection
roleId={roleId}
isEditable={isEditable}
/>
<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`} />
{isPermissionsV2Enabled && (
<StyledCard rounded>
<SettingsOptionCardContentToggle
Icon={IconSettings}
title={t`Settings All Access`}
description={t`Ability to edit all settings`}
checked={settingsDraftRole.canUpdateAllSettings}
disabled={!isEditable}
onChange={() => {
setSettingsDraftRole({
...settingsDraftRole,
canUpdateAllSettings: !settingsDraftRole.canUpdateAllSettings,
});
}}
/>
</StyledCard>
)}
<StyledTable>
<SettingsRolePermissionsSettingsTableHeader />
<StyledTableRows>
{settingsPermissionsConfig.map((permission) => (
<SettingsRolePermissionsSettingsTableRow
key={permission.key}
roleId={roleId}
permission={permission}
isEditable={isEditable}
/>
))}
</StyledTableRows>
</StyledTable>
</Section>
)}
<SettingsRolePermissionsSettingsSection
roleId={roleId}
isEditable={isEditable}
/>
</StyledRolePermissionsContainer>
);
};

View File

@ -0,0 +1,58 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { t } from '@lingui/core/macro';
import { ChangeEvent, useState } from 'react';
import { useIcons } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
type SettingsRolePermissionsObjectLevelObjectPickerDropdownContentProps = {
excludedObjectMetadataIds: string[];
onSelect: (objectMetadataId: string) => void;
};
export const SettingsRolePermissionsObjectLevelObjectPickerDropdownContent = ({
excludedObjectMetadataIds,
onSelect,
}: SettingsRolePermissionsObjectLevelObjectPickerDropdownContentProps) => {
const [searchFilter, setSearchFilter] = useState('');
const { objectMetadataItems } = useFilteredObjectMetadataItems();
const { getIcon } = useIcons();
const handleSearchFilterChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearchFilter(event.target.value);
};
const filteredObjectMetadataItems = objectMetadataItems.filter(
(objectMetadataItem) =>
objectMetadataItem.labelSingular
.toLowerCase()
.includes(searchFilter.toLowerCase()) &&
!excludedObjectMetadataIds.includes(objectMetadataItem.id),
);
return (
<DropdownMenu>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleSearchFilterChange}
placeholder={t`Search`}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredObjectMetadataItems.map((objectMetadataItem) => (
<MenuItem
key={objectMetadataItem.id}
text={objectMetadataItem.labelSingular}
LeftIcon={getIcon(objectMetadataItem.icon)}
onClick={() => onSelect(objectMetadataItem.id)}
/>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
);
};

View File

@ -0,0 +1,96 @@
import { SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG } from '@/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectPermissionIconConfig';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
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`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
type SettingsRolePermissionsObjectLevelOverrideCellProps = {
objectPermission: ObjectPermission;
};
export const SettingsRolePermissionsObjectLevelOverrideCell = ({
objectPermission,
}: SettingsRolePermissionsObjectLevelOverrideCellProps) => {
const theme = useTheme();
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(objectPermission.roleId),
);
const permissionMappings = {
canReadObjectRecords: 'canReadAllObjectRecords',
canUpdateObjectRecords: 'canUpdateAllObjectRecords',
canSoftDeleteObjectRecords: 'canSoftDeleteAllObjectRecords',
canDestroyObjectRecords: 'canDestroyAllObjectRecords',
} as const;
type ObjectPermissionKey = keyof typeof permissionMappings;
const isOverridden = (permission: ObjectPermissionKey) => {
const rolePermission = permissionMappings[permission];
return (
isDefined(objectPermission[permission]) &&
!!settingsDraftRole[rolePermission as keyof typeof settingsDraftRole] !==
!!objectPermission[permission]
);
};
return (
<StyledSettingsRolePermissionsObjectLevelOverrideCell>
{(Object.keys(permissionMappings) as ObjectPermissionKey[]).map(
(permission) => {
const { Icon, IconForbidden: IconOverride } =
SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission];
const permissionValue = objectPermission[permission];
if (!isOverridden(permission)) {
return null;
}
return (
<StyledIconWrapper
key={permission}
isForbidden={permissionValue === false}
>
<StyledIcon isForbidden={permissionValue === false}>
{permissionValue === false && (
<IconOverride size={theme.icon.size.sm} />
)}
{permissionValue === true && <Icon size={theme.icon.size.sm} />}
</StyledIcon>
</StyledIconWrapper>
);
},
)}
</StyledSettingsRolePermissionsObjectLevelOverrideCell>
);
};

View File

@ -0,0 +1,113 @@
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelTableHeader } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader';
import { SettingsRolePermissionsObjectLevelTableRow } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
// const StyledCreateObjectOverrideSection = styled(Section)`
// border-top: 1px solid ${({ theme }) => theme.border.color.light};
// display: flex;
// justify-content: flex-end;
// padding-top: ${({ theme }) => theme.spacing(2)};
// padding-bottom: ${({ theme }) => theme.spacing(2)};
// `;
const StyledTableRows = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
type SettingsRolePermissionsObjectLevelSectionProps = {
roleId: string;
isEditable: boolean;
};
const StyledNoOverride = styled(TableCell)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const SettingsRolePermissionsObjectLevelSection = ({
roleId,
}: SettingsRolePermissionsObjectLevelSectionProps) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const objectMetadataItems = useObjectMetadataItems();
const objectMetadataMap = objectMetadataItems.objectMetadataItems.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<string, ObjectMetadataItem>,
);
const objectPermissions = settingsDraftRole.objectPermissions;
// const handleSelectObjectMetadata = (objectMetadataId: string) => {
// setSettingsDraftRole((draftRole) => ({
// ...draftRole,
// objectPermissions: [
// ...(draftRole.objectPermissions ?? []),
// { objectMetadataId, roleId, id: v4() },
// ],
// }));
// };
return (
<Section>
<H2Title
title={t`Object-Level Permissions`}
description={t`Set additional object-level permissions`}
/>
<Table>
<SettingsRolePermissionsObjectLevelTableHeader />
<StyledTableRows>
{isDefined(objectPermissions) && objectPermissions?.length > 0 ? (
objectPermissions?.map((objectPermission) => (
<SettingsRolePermissionsObjectLevelTableRow
key={objectPermission.id}
objectPermission={objectPermission}
objectMetadataItem={
objectMetadataMap[objectPermission.objectMetadataId]
}
/>
))
) : (
<StyledNoOverride>{t`No overrides found`}</StyledNoOverride>
)}
</StyledTableRows>
</Table>
{/* <StyledCreateObjectOverrideSection>
<Dropdown
dropdownId="role-object-select"
dropdownHotkeyScope={{ scope: 'roleObject' }}
clickableComponent={
<Button
Icon={IconPlus}
title={t`Add Object`}
variant="secondary"
size="small"
disabled={!isEditable}
/>
}
dropdownComponents={
<SettingsRolePermissionsObjectLevelObjectPickerDropdownContent
excludedObjectMetadataIds={[]}
onSelect={handleSelectObjectMetadata}
/>
}
/>
</StyledCreateObjectOverrideSection> */}
</Section>
);
};

View File

@ -0,0 +1,11 @@
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro';
export const SettingsRolePermissionsObjectLevelTableHeader = () => (
<TableRow>
<TableHeader>{t`Object`}</TableHeader>
<TableHeader>{t`Permission overrides`}</TableHeader>
<TableHeader></TableHeader>
</TableRow>
);

View File

@ -0,0 +1,73 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelOverrideCell } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCell';
import { SettingsPath } from '@/types/SettingsPath';
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 { IconChevronRight, useIcons } from 'twenty-ui/display';
import { ObjectPermission } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledNameLabel = styled.div`
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
type SettingsRolePermissionsObjectLevelTableRowProps = {
objectPermission: ObjectPermission;
objectMetadataItem: ObjectMetadataItem;
};
export const SettingsRolePermissionsObjectLevelTableRow = ({
objectPermission,
objectMetadataItem,
}: SettingsRolePermissionsObjectLevelTableRowProps) => {
const { getIcon } = useIcons();
const theme = useTheme();
if (!objectMetadataItem) {
throw new Error('Object metadata item not found');
}
const Icon = getIcon(objectMetadataItem.icon);
return (
<TableRow
to={getSettingsPath(SettingsPath.RoleObjectLevel, {
roleId: objectPermission.roleId,
objectMetadataId: objectPermission.objectMetadataId,
})}
>
<StyledNameTableCell>
{!!Icon && (
<Icon
style={{ minWidth: theme.icon.size.md }}
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
)}
<StyledNameLabel title={objectMetadataItem.labelPlural}>
{objectMetadataItem.labelPlural}
</StyledNameLabel>
</StyledNameTableCell>
<TableCell>
<SettingsRolePermissionsObjectLevelOverrideCell
objectPermission={objectPermission}
/>
</TableCell>
<TableCell align={'right'}>
<IconChevronRight
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</TableCell>
</TableRow>
);
};

View File

@ -0,0 +1,94 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevel } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { H3Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledObjectTypeTag = styled(SettingsDataModelObjectTypeTag)`
box-sizing: border-box;
height: ${({ theme }) => theme.spacing(5)};
margin-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledTitleContainer = styled.div`
display: flex;
`;
type SettingsRolePermissionsObjectLevelObjectFormProps = {
roleId: string;
objectMetadataId: string;
};
export const SettingsRolePermissionsObjectLevelObjectForm = ({
roleId,
objectMetadataId,
}: SettingsRolePermissionsObjectLevelObjectFormProps) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const objectMetadata = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const objectMetadataItem = objectMetadata.objectMetadataItem;
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
return (
<SubMenuTopBarContainer
title={
<StyledTitleContainer>
<H3Title title={objectMetadataItem.labelPlural} />
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} />
</StyledTitleContainer>
}
links={[
{
children: 'Workspace',
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: 'Roles',
href: getSettingsPath(SettingsPath.Roles),
},
{
children: settingsDraftRole.label,
href: getSettingsPath(SettingsPath.RoleDetail, {
roleId,
}),
},
{
children: `Permissions · ${objectMetadataItem.labelSingular}`,
},
]}
actionButton={
<Button
title={t`Back`}
variant="primary"
size="small"
accent="blue"
to={getSettingsPath(SettingsPath.RoleDetail, {
roleId,
})}
/>
}
>
<SettingsPageContainer>
<SettingsRolePermissionsObjectLevelObjectFormObjectLevel
objectMetadataItem={objectMetadataItem}
roleId={roleId}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,100 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader';
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 { useRecoilValue } from 'recoil';
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 SettingsRolePermissionsObjectLevelObjectFormObjectLevelProps = {
roleId: string;
objectMetadataItem: ObjectMetadataItem;
};
export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({
roleId,
objectMetadataItem,
}: SettingsRolePermissionsObjectLevelObjectFormObjectLevelProps) => {
const settingsDraftRole = useRecoilValue(
settingsDraftRoleFamilyState(roleId),
);
const settingsDraftRoleObjectPermissions =
settingsDraftRole.objectPermissions?.find(
(permission) => permission.objectMetadataId === objectMetadataItem.id,
);
if (!settingsDraftRoleObjectPermissions) {
return null;
}
const objectLabel = objectMetadataItem.labelPlural;
const objectPermissionsConfig: SettingsRolePermissionsObjectPermission[] = [
{
key: 'canReadObjectRecords',
label: t`See Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canReadObjectRecords,
setValue: (_value: boolean) => {
// TODO: Implement
},
},
{
key: 'canUpdateObjectRecords',
label: t`Edit Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canUpdateObjectRecords,
setValue: (_value: boolean) => {
// TODO: Implement
},
},
{
key: 'canSoftDeleteObjectRecords',
label: t`Delete Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canSoftDeleteObjectRecords,
setValue: (_value: boolean) => {
// TODO: Implement
},
},
{
key: 'canDestroyObjectRecords',
label: t`Destroy Records on ${objectLabel}`,
value: settingsDraftRoleObjectPermissions.canDestroyObjectRecords,
setValue: (_value: boolean) => {
// TODO: Implement
},
},
];
return (
<Section>
<H2Title
title={t`Object-Level Permissions`}
description={t`Ability to interact with this specific object`}
/>
<StyledTable>
<SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader />
<StyledTableRows>
{objectPermissionsConfig.map((permission) => (
<SettingsRolePermissionsObjectsTableRow
key={permission.key}
permission={permission}
isEditable={settingsDraftRole.isEditable}
/>
))}
</StyledTableRows>
</StyledTable>
</Section>
);
};

View File

@ -0,0 +1,11 @@
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { t } from '@lingui/core/macro';
export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader =
() => (
<TableRow gridAutoColumns="1fr 24px">
<TableHeader>{t`Name`}</TableHeader>
<TableHeader aria-label={t`Actions`}></TableHeader>
</TableRow>
);

View File

@ -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>
);
};

View File

@ -1,5 +1,5 @@
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission';
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';

View File

@ -1,32 +1,24 @@
import { SettingsRolePermissionsObjectPermission } from '@/settings/roles/types/SettingsRolePermissionsObjectPermission';
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 { Checkbox } from 'twenty-ui/input';
const StyledIconWrapper = styled.div`
align-items: center;
background: ${({ theme }) => theme.adaptiveColors.blue1};
border: 1px solid ${({ theme }) => 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`
align-items: center;
display: flex;
color: ${({ theme }) => theme.color.blue};
justify-content: center;
`;
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;
@ -58,21 +50,30 @@ export const SettingsRolePermissionsObjectsTableRow = ({
}: 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>
<StyledIconWrapper>
<StyledIcon>
<permission.Icon size={theme.icon.size.sm} />
</StyledIcon>
</StyledIconWrapper>
<StyledLabel>{permission.label}</StyledLabel>
<Icon size={theme.icon.size.sm} />
<StyledLabel>{label}</StyledLabel>
{isOverridden ? (
<StyledOverrideInfo>
{t`Overridden on ${isOverriddenBy} ${pluralizedObject}`}
</StyledOverrideInfo>
) : null}
</StyledPermissionCell>
<StyledCheckboxCell>
<Checkbox
checked={permission.value}
checked={permission.value ?? false}
onChange={() => permission.setValue(!permission.value)}
disabled={!isEditable}
accent={isOverridden ? CheckboxAccent.Orange : CheckboxAccent.Blue}
/>
</StyledCheckboxCell>
</StyledTableRow>

View File

@ -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,
},
};

View File

@ -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;
};

View File

@ -0,0 +1,130 @@
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { SettingsRolePermissionsSettingsTableHeader } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableHeader';
import { SettingsRolePermissionsSettingsTableRow } from '@/settings/roles/role-permissions/settings-permissions/components/SettingsRolePermissionsSettingsTableRow';
import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import {
H2Title,
IconCode,
IconHierarchy,
IconKey,
IconLockOpen,
IconSettings,
IconUsers,
} from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import {
FeatureFlagKey,
SettingPermissionType,
} from '~/generated-metadata/graphql';
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)};
`;
const StyledCard = styled(Card)`
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
type SettingsRolePermissionsSettingsSectionProps = {
roleId: string;
isEditable: boolean;
};
export const SettingsRolePermissionsSettingsSection = ({
roleId,
isEditable,
}: SettingsRolePermissionsSettingsSectionProps) => {
const isPermissionsV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
);
const [settingsDraftRole, setSettingsDraftRole] = useRecoilState(
settingsDraftRoleFamilyState(roleId),
);
const settingsPermissionsConfig: SettingsRolePermissionsSettingPermission[] =
[
{
key: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
name: t`API Keys & Webhooks`,
description: t`Manage API keys and webhooks`,
Icon: IconCode,
},
{
key: SettingPermissionType.WORKSPACE,
name: t`Workspace`,
description: t`Set global workspace preferences`,
Icon: IconSettings,
},
{
key: SettingPermissionType.WORKSPACE_MEMBERS,
name: t`Users`,
description: t`Add or remove users`,
Icon: IconUsers,
},
{
key: SettingPermissionType.ROLES,
name: t`Roles`,
description: t`Define user roles and access levels`,
Icon: IconLockOpen,
},
{
key: SettingPermissionType.DATA_MODEL,
name: t`Data Model`,
description: t`Edit CRM data structure and fields`,
Icon: IconHierarchy,
},
{
key: SettingPermissionType.SECURITY,
name: t`Security`,
description: t`Manage security policies`,
Icon: IconKey,
},
];
return (
<Section>
<H2Title title={t`Settings`} description={t`Settings permissions`} />
{isPermissionsV2Enabled && (
<StyledCard rounded>
<SettingsOptionCardContentToggle
Icon={IconSettings}
title={t`Settings All Access`}
description={t`Ability to edit all settings`}
checked={settingsDraftRole.canUpdateAllSettings}
disabled={!isEditable}
onChange={() => {
setSettingsDraftRole({
...settingsDraftRole,
canUpdateAllSettings: !settingsDraftRole.canUpdateAllSettings,
});
}}
/>
</StyledCard>
)}
<StyledTable>
<SettingsRolePermissionsSettingsTableHeader />
<StyledTableRows>
{settingsPermissionsConfig.map((permission) => (
<SettingsRolePermissionsSettingsTableRow
key={permission.key}
roleId={roleId}
permission={permission}
isEditable={isEditable}
/>
))}
</StyledTableRows>
</StyledTable>
</Section>
);
};

View File

@ -1,5 +1,5 @@
import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/settings-permissions/types/SettingsRolePermissionsSettingPermission';
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { SettingsRolePermissionsSettingPermission } from '@/settings/roles/types/SettingsRolePermissionsSettingPermission';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';

View File

@ -10,6 +10,8 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
import { SettingsPath } from '@/types/SettingsPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState';
@ -78,6 +80,8 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
settingsPersistedRoleFamilyState(roleId),
);
const { enqueueSnackBar } = useSnackBar();
if (!isDefined(settingsRolesIsLoading)) {
return <></>;
}
@ -110,6 +114,13 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => {
settingsPersistedRole,
);
if (isDefined(dirtyFields.label) && dirtyFields.label === '') {
enqueueSnackBar(t`Role name cannot be empty`, {
variant: SnackBarVariant.Error,
});
return;
}
if (isCreateMode) {
const roleId = v4();

View File

@ -5,7 +5,13 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
import { useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';
export const SettingsRoleCreateEffect = ({ roleId }: { roleId: string }) => {
type SettingsRoleCreateEffectProps = {
roleId: string;
};
export const SettingsRoleCreateEffect = ({
roleId,
}: SettingsRoleCreateEffectProps) => {
const setSettingsDraftRole = useSetRecoilState(
settingsDraftRoleFamilyState(roleId),
);

View File

@ -4,10 +4,6 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr
import { TitleInput } from '@/ui/input/components/TitleInput';
import styled from '@emotion/styled';
type SettingsRoleLabelContainerProps = {
roleId: string;
};
const ROLE_LABEL_EDIT_HOTKEY_SCOPE = 'role-label-edit';
const StyledHeaderTitle = styled.div`
@ -21,6 +17,10 @@ const StyledHeaderTitle = styled.div`
}
`;
type SettingsRoleLabelContainerProps = {
roleId: string;
};
export const SettingsRoleLabelContainer = ({
roleId,
}: SettingsRoleLabelContainerProps) => {

View File

@ -16,5 +16,6 @@ export const settingsDraftRoleFamilyState = createFamilyState<Role, string>({
isEditable: false,
workspaceMembers: [],
settingPermissions: [],
objectPermissions: [],
},
});

View File

@ -1,8 +0,0 @@
import { IconComponent } from 'twenty-ui/display';
export type SettingsRolePermissionsObjectPermission = {
key: string;
label: string;
value: boolean;
Icon: IconComponent;
setValue: (value: boolean) => void;
};

View File

@ -44,4 +44,5 @@ export enum SettingsPath {
Roles = 'roles',
RoleCreate = 'roles/create',
RoleDetail = 'roles/:roleId',
RoleObjectLevel = 'roles/:roleId/object/:objectMetadataId',
}

View File

@ -0,0 +1,27 @@
import { Navigate, useParams } from 'react-router-dom';
import { SettingsRolesQueryEffect } from '@/settings/roles/components/SettingsRolesQueryEffect';
import { SettingsRolePermissionsObjectLevelObjectForm } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectForm';
import { isDefined } from 'twenty-shared/utils';
export const SettingsRoleObjectLevel = () => {
const { roleId, objectMetadataId } = useParams();
if (!isDefined(roleId)) {
return <Navigate to="/settings/roles" />;
}
if (!isDefined(objectMetadataId)) {
return <Navigate to={`/settings/roles/${roleId}`} />;
}
return (
<>
<SettingsRolesQueryEffect />
<SettingsRolePermissionsObjectLevelObjectForm
roleId={roleId}
objectMetadataId={objectMetadataId}
/>
</>
);
};

View File

@ -3,6 +3,7 @@ import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { Relation } from 'typeorm';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { SettingPermissionDTO } from 'src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto';
@ -46,4 +47,7 @@ export class RoleDTO {
@Field(() => [SettingPermissionDTO], { nullable: true })
settingPermissions?: SettingPermissionDTO[];
@Field(() => [ObjectPermissionDTO], { nullable: true })
objectPermissions?: ObjectPermissionDTO[];
}

View File

@ -39,7 +39,11 @@ export class RoleService {
where: {
workspaceId,
},
relations: ['userWorkspaceRoles', 'settingPermissions'],
relations: [
'userWorkspaceRoles',
'settingPermissions',
'objectPermissions',
],
});
}

View File

@ -4,18 +4,18 @@ export {
IconAlertCircle,
IconAlertTriangle,
IconApi,
IconAppWindow,
IconApps,
IconAppWindow,
IconArchive,
IconArchiveOff,
IconArrowBackUp,
IconArrowDown,
IconArrowLeft,
IconArrowRight,
IconArrowUp,
IconArrowUpRight,
IconArrowsDiagonal,
IconArrowsVertical,
IconArrowUp,
IconArrowUpRight,
IconAt,
IconBaselineDensitySmall,
IconBell,
@ -47,8 +47,8 @@ export {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronUp,
IconChevronsRight,
IconChevronUp,
IconCircleDot,
IconCircleOff,
IconCirclePlus,
@ -217,6 +217,7 @@ export {
IconPaperclip,
IconPassword,
IconPencil,
IconPencilOff,
IconPercentage,
IconPhone,
IconPhoto,
@ -283,6 +284,7 @@ export {
IconTimelineEvent,
IconTool,
IconTrash,
IconTrashOff,
IconTrashX,
IconTypography,
IconUnlink,

View File

@ -65,18 +65,18 @@ export {
IconAlertCircle,
IconAlertTriangle,
IconApi,
IconAppWindow,
IconApps,
IconAppWindow,
IconArchive,
IconArchiveOff,
IconArrowBackUp,
IconArrowDown,
IconArrowLeft,
IconArrowRight,
IconArrowUp,
IconArrowUpRight,
IconArrowsDiagonal,
IconArrowsVertical,
IconArrowUp,
IconArrowUpRight,
IconAt,
IconBaselineDensitySmall,
IconBell,
@ -108,8 +108,8 @@ export {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronUp,
IconChevronsRight,
IconChevronUp,
IconCircleDot,
IconCircleOff,
IconCirclePlus,
@ -278,6 +278,7 @@ export {
IconPaperclip,
IconPassword,
IconPencil,
IconPencilOff,
IconPercentage,
IconPhone,
IconPhoto,
@ -344,6 +345,7 @@ export {
IconTimelineEvent,
IconTool,
IconTrash,
IconTrashOff,
IconTrashX,
IconTypography,
IconUnlink,

View File

@ -19,6 +19,11 @@ export enum CheckboxSize {
Small = 'small',
}
export enum CheckboxAccent {
Blue = 'blue',
Orange = 'orange',
}
type CheckboxProps = {
checked: boolean;
indeterminate?: boolean;
@ -30,11 +35,13 @@ type CheckboxProps = {
shape?: CheckboxShape;
className?: string;
disabled?: boolean;
accent?: CheckboxAccent;
};
type InputProps = {
checkboxSize: CheckboxSize;
variant: CheckboxVariant;
accent?: CheckboxAccent;
indeterminate?: boolean;
hoverable?: boolean;
shape?: CheckboxShape;
@ -68,12 +75,14 @@ const StyledInputContainer = styled.div<InputProps>`
}
}};
position: relative;
${({ hoverable, isChecked, theme, indeterminate, disabled }) => {
${({ hoverable, isChecked, theme, indeterminate, disabled, accent }) => {
if (!hoverable || disabled === true) return '';
return `&:hover{
background-color: ${
indeterminate || isChecked
? theme.background.transparent.blue
? accent === CheckboxAccent.Blue
? theme.background.transparent.blue
: theme.background.transparent.orange
: theme.background.transparent.light
};
}}
@ -100,9 +109,15 @@ const StyledInput = styled.input<InputProps>`
& + label:before {
--size: ${({ checkboxSize }) =>
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
background: ${({ theme, indeterminate, isChecked, disabled }) => {
background: ${({ theme, indeterminate, isChecked, disabled, accent }) => {
if (!(indeterminate || isChecked)) return 'transparent';
return disabled ? theme.adaptiveColors.blue3 : theme.color.blue;
return disabled
? accent === CheckboxAccent.Blue
? theme.adaptiveColors.blue3
: theme.adaptiveColors.orange3
: accent === CheckboxAccent.Blue
? theme.color.blue
: theme.color.orange;
}};
border-color: ${({
theme,
@ -110,10 +125,17 @@ const StyledInput = styled.input<InputProps>`
isChecked,
variant,
disabled,
accent,
}) => {
switch (true) {
case indeterminate || isChecked:
return disabled ? theme.adaptiveColors.blue3 : theme.color.blue;
return disabled
? accent === CheckboxAccent.Blue
? theme.adaptiveColors.blue3
: theme.adaptiveColors.orange3
: accent === CheckboxAccent.Blue
? theme.color.blue
: theme.color.orange;
case disabled:
return theme.border.color.strong;
case variant === CheckboxVariant.Primary:
@ -165,6 +187,7 @@ export const Checkbox = ({
hoverable = true,
className,
disabled = false,
accent = CheckboxAccent.Blue,
}: CheckboxProps) => {
const [isInternalChecked, setIsInternalChecked] =
React.useState<boolean>(false);
@ -191,6 +214,7 @@ export const Checkbox = ({
indeterminate={indeterminate}
className={className}
disabled={disabled}
accent={accent}
>
<StyledInput
autoComplete="off"
@ -206,6 +230,7 @@ export const Checkbox = ({
isChecked={isInternalChecked}
onChange={handleChange}
disabled={disabled}
accent={accent}
/>
<label htmlFor={checkboxId}>
{indeterminate ? (

View File

@ -7,6 +7,7 @@ import {
import {
Checkbox,
CheckboxAccent,
CheckboxShape,
CheckboxSize,
CheckboxVariant,
@ -29,6 +30,7 @@ export const Default: Story = {
variant: CheckboxVariant.Primary,
size: CheckboxSize.Small,
shape: CheckboxShape.Squared,
accent: CheckboxAccent.Blue,
},
decorators: [ComponentDecorator],
};
@ -42,6 +44,7 @@ export const Catalog: CatalogStory<Story, typeof Checkbox> = {
checked: { control: false },
hoverable: { control: false },
shape: { control: false },
accent: { control: false },
},
parameters: {
catalog: {
@ -82,6 +85,11 @@ export const Catalog: CatalogStory<Story, typeof Checkbox> = {
values: Object.values(CheckboxSize),
props: (size: CheckboxSize) => ({ size }),
},
{
name: 'accent',
values: Object.values(CheckboxAccent),
props: (accent: CheckboxAccent) => ({ accent }),
},
],
},
},

View File

@ -82,6 +82,7 @@ export {
CheckboxVariant,
CheckboxShape,
CheckboxSize,
CheckboxAccent,
Checkbox,
} from './components/Checkbox';
export { IconListViewGrip } from './components/IconListViewGrip';

View File

@ -23,6 +23,7 @@ export const BACKGROUND_DARK = {
lighter: RGBA(GRAY_SCALE.gray0, 0.03),
danger: RGBA(COLOR.red, 0.08),
blue: RGBA(COLOR.blue, 0.2),
orange: RGBA(COLOR.orange, 0.2),
},
overlayPrimary: RGBA(GRAY_SCALE.gray100, 0.8),
overlaySecondary: RGBA(GRAY_SCALE.gray100, 0.6),

View File

@ -23,6 +23,7 @@ export const BACKGROUND_LIGHT = {
lighter: RGBA(GRAY_SCALE.gray100, 0.02),
danger: RGBA(COLOR.red, 0.08),
blue: RGBA(COLOR.blue, 0.08),
orange: RGBA(COLOR.orange, 0.08),
},
overlayPrimary: RGBA(GRAY_SCALE.gray80, 0.8),
overlaySecondary: RGBA(GRAY_SCALE.gray80, 0.4),