From e55ecb4dcdae40d095c7e9a9f8c67ee3e801854f Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 24 Apr 2025 18:15:43 +0200 Subject: [PATCH] object level override form (#11672) --- .../src/generated-metadata/graphql.ts | 56 +++++-- .../twenty-front/src/generated/graphql.tsx | 64 ++++++-- .../components/SettingsRolesQueryEffect.tsx | 11 +- .../upsertObjectPermissionsMutation.ts | 15 ++ .../components/SettingsRolePermissions.tsx | 4 +- .../SettingsRolePermissions.stories.tsx | 7 +- ...ObjectLevelObjectPickerDropdownContent.tsx | 3 +- ...RolePermissionsObjectLevelOverrideCell.tsx | 9 +- ...tingsRolePermissionsObjectLevelSection.tsx | 90 +++++++---- ...sRolePermissionsObjectLevelTableHeader.tsx | 4 +- ...ingsRolePermissionsObjectLevelTableRow.tsx | 9 +- .../components/OverridableCheckbox.tsx | 95 ++++++++++++ ...ssionsObjectLevelObjectFormObjectLevel.tsx | 95 +++++++----- ...LevelObjectFormObjectLevelTableHeader.tsx} | 4 +- ...jectLevelObjectFormObjectLevelTableRow.tsx | 141 ++++++++++++++++++ .../SettingsRolePermissionsObjectsSection.tsx | 31 ++-- ...SettingsRolePermissionsObjectsTableRow.tsx | 12 +- ...PermissionToRoleObjectPermissionMapping.ts | 7 + ...SettingsRolePermissionsObjectPermission.ts | 11 +- .../roles/role/components/SettingsRole.tsx | 90 ++++++++--- .../components/SettingsRoleEditEffect.tsx | 32 ++-- ....ts => upsert-object-permissions.input.ts} | 20 ++- .../object-permission.service.ts | 46 +++--- .../metadata-modules/role/role.resolver.ts | 25 ++-- .../roles.integration-spec.ts | 23 ++- 25 files changed, 708 insertions(+), 196 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertObjectPermissionsMutation.ts create mode 100644 packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox.tsx rename packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/{SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader.tsx => SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader.tsx} (86%) create mode 100644 packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow.tsx create mode 100644 packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping.ts rename packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/{upsert-object-permission-input.ts => upsert-object-permissions.input.ts} (62%) diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 3be34163c..6da73e01f 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -517,6 +517,15 @@ export type CustomDomainValidRecords = { records: Array; }; +/** Database Event Action */ +export enum DatabaseEventAction { + CREATED = 'CREATED', + DELETED = 'DELETED', + DESTROYED = 'DESTROYED', + RESTORED = 'RESTORED', + UPDATED = 'UPDATED' +} + export type DateFilter = { eq?: InputMaybe; gt?: InputMaybe; @@ -1004,7 +1013,7 @@ export type Mutation = { uploadImage: Scalars['String']['output']; uploadProfilePicture: Scalars['String']['output']; uploadWorkspaceLogo: Scalars['String']['output']; - upsertOneObjectPermission: ObjectPermission; + upsertObjectPermissions: Array; upsertSettingPermissions: Array; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; @@ -1368,8 +1377,8 @@ export type MutationUploadWorkspaceLogoArgs = { }; -export type MutationUpsertOneObjectPermissionArgs = { - upsertObjectPermissionInput: UpsertObjectPermissionInput; +export type MutationUpsertObjectPermissionsArgs = { + upsertObjectPermissionsInput: UpsertObjectPermissionsInput; }; @@ -1481,6 +1490,14 @@ export type ObjectPermission = { roleId: Scalars['String']['output']; }; +export type ObjectPermissionInput = { + canDestroyObjectRecords?: InputMaybe; + canReadObjectRecords?: InputMaybe; + canSoftDeleteObjectRecords?: InputMaybe; + canUpdateObjectRecords?: InputMaybe; + objectMetadataId: Scalars['String']['input']; +}; + export type ObjectRecordFilterInput = { and?: InputMaybe>; createdAt?: InputMaybe; @@ -1500,6 +1517,21 @@ export type ObjectStandardOverrides = { translations?: Maybe; }; +export type OnDbEventDto = { + __typename?: 'OnDbEventDTO'; + action: DatabaseEventAction; + eventDate: Scalars['DateTime']['output']; + objectNameSingular: Scalars['String']['output']; + record: Scalars['JSON']['output']; + updatedFields?: Maybe>; +}; + +export type OnDbEventInput = { + action?: InputMaybe; + objectNameSingular?: InputMaybe; + recordId?: InputMaybe; +}; + /** Onboarding status */ export enum OnboardingStatus { COMPLETED = 'COMPLETED', @@ -2090,6 +2122,16 @@ export type SubmitFormStepInput = { workflowRunId: Scalars['String']['input']; }; +export type Subscription = { + __typename?: 'Subscription'; + onDbEvent: OnDbEventDto; +}; + + +export type SubscriptionOnDbEventArgs = { + input: OnDbEventInput; +}; + export enum SubscriptionInterval { Day = 'Day', Month = 'Month', @@ -2321,12 +2363,8 @@ export type UpdateWorkspaceInput = { subdomain?: InputMaybe; }; -export type UpsertObjectPermissionInput = { - canDestroyObjectRecords?: InputMaybe; - canReadObjectRecords?: InputMaybe; - canSoftDeleteObjectRecords?: InputMaybe; - canUpdateObjectRecords?: InputMaybe; - objectMetadataId: Scalars['String']['input']; +export type UpsertObjectPermissionsInput = { + objectPermissions: Array; roleId: Scalars['String']['input']; }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 59299c719..bff4b9354 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -929,7 +929,7 @@ export type Mutation = { uploadImage: Scalars['String']; uploadProfilePicture: Scalars['String']; uploadWorkspaceLogo: Scalars['String']; - upsertOneObjectPermission: ObjectPermission; + upsertObjectPermissions: Array; upsertSettingPermissions: Array; userLookupAdminPanel: UserLookup; validateApprovedAccessDomain: ApprovedAccessDomain; @@ -1243,8 +1243,8 @@ export type MutationUploadWorkspaceLogoArgs = { }; -export type MutationUpsertOneObjectPermissionArgs = { - upsertObjectPermissionInput: UpsertObjectPermissionInput; +export type MutationUpsertObjectPermissionsArgs = { + upsertObjectPermissionsInput: UpsertObjectPermissionsInput; }; @@ -1356,6 +1356,14 @@ export type ObjectPermission = { roleId: Scalars['String']; }; +export type ObjectPermissionInput = { + canDestroyObjectRecords?: InputMaybe; + canReadObjectRecords?: InputMaybe; + canSoftDeleteObjectRecords?: InputMaybe; + canUpdateObjectRecords?: InputMaybe; + objectMetadataId: Scalars['String']; +}; + export type ObjectRecordFilterInput = { and?: InputMaybe>; createdAt?: InputMaybe; @@ -2142,12 +2150,8 @@ export type UpdateWorkspaceInput = { subdomain?: InputMaybe; }; -export type UpsertObjectPermissionInput = { - canDestroyObjectRecords?: InputMaybe; - canReadObjectRecords?: InputMaybe; - canSoftDeleteObjectRecords?: InputMaybe; - canUpdateObjectRecords?: InputMaybe; - objectMetadataId: Scalars['String']; +export type UpsertObjectPermissionsInput = { + objectPermissions: Array; roleId: Scalars['String']; }; @@ -2759,6 +2763,13 @@ export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{ export type UpdateWorkspaceMemberRoleMutation = { __typename?: 'Mutation', updateWorkspaceMemberRole: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, roles?: 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 }> | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } }; +export type UpsertObjectPermissionsMutationVariables = Exact<{ + upsertObjectPermissionsInput: UpsertObjectPermissionsInput; +}>; + + +export type UpsertObjectPermissionsMutation = { __typename?: 'Mutation', upsertObjectPermissions: Array<{ __typename?: 'ObjectPermission', id: string, objectMetadataId: string, roleId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> }; + export type UpsertSettingPermissionsMutationVariables = Exact<{ upsertSettingPermissionsInput: UpsertSettingPermissionsInput; }>; @@ -5058,6 +5069,41 @@ export function useUpdateWorkspaceMemberRoleMutation(baseOptions?: Apollo.Mutati export type UpdateWorkspaceMemberRoleMutationHookResult = ReturnType; export type UpdateWorkspaceMemberRoleMutationResult = Apollo.MutationResult; export type UpdateWorkspaceMemberRoleMutationOptions = Apollo.BaseMutationOptions; +export const UpsertObjectPermissionsDocument = gql` + mutation UpsertObjectPermissions($upsertObjectPermissionsInput: UpsertObjectPermissionsInput!) { + upsertObjectPermissions( + upsertObjectPermissionsInput: $upsertObjectPermissionsInput + ) { + ...ObjectPermissionFragment + } +} + ${ObjectPermissionFragmentFragmentDoc}`; +export type UpsertObjectPermissionsMutationFn = Apollo.MutationFunction; + +/** + * __useUpsertObjectPermissionsMutation__ + * + * To run a mutation, you first call `useUpsertObjectPermissionsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpsertObjectPermissionsMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [upsertObjectPermissionsMutation, { data, loading, error }] = useUpsertObjectPermissionsMutation({ + * variables: { + * upsertObjectPermissionsInput: // value for 'upsertObjectPermissionsInput' + * }, + * }); + */ +export function useUpsertObjectPermissionsMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpsertObjectPermissionsDocument, options); + } +export type UpsertObjectPermissionsMutationHookResult = ReturnType; +export type UpsertObjectPermissionsMutationResult = Apollo.MutationResult; +export type UpsertObjectPermissionsMutationOptions = Apollo.BaseMutationOptions; export const UpsertSettingPermissionsDocument = gql` mutation UpsertSettingPermissions($upsertSettingPermissionsInput: UpsertSettingPermissionsInput!) { upsertSettingPermissions( diff --git a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesQueryEffect.tsx b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesQueryEffect.tsx index fea085883..133d7f63e 100644 --- a/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesQueryEffect.tsx +++ b/packages/twenty-front/src/modules/settings/roles/components/SettingsRolesQueryEffect.tsx @@ -2,10 +2,12 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState'; import { settingsRoleIdsState } from '@/settings/roles/states/settingsRoleIdsState'; import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useEffect } from 'react'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { Role, useGetRolesQuery } from '~/generated/graphql'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const SettingsRolesQueryEffect = () => { const { data, loading } = useGetRolesQuery({ @@ -17,11 +19,18 @@ export const SettingsRolesQueryEffect = () => { ); const populateRoles = useRecoilCallback( - ({ set }) => + ({ set, snapshot }) => (roles: Role[]) => { const roleIds = roles.map((role) => role.id); set(settingsRoleIdsState, roleIds); roles.forEach((role) => { + const persistedRole = getSnapshotValue( + snapshot, + settingsPersistedRoleFamilyState(role.id), + ); + if (isDeeplyEqual(role, persistedRole)) { + return; + } set(settingsDraftRoleFamilyState(role.id), role); set(settingsPersistedRoleFamilyState(role.id), role); }); diff --git a/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertObjectPermissionsMutation.ts b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertObjectPermissionsMutation.ts new file mode 100644 index 000000000..3d5ba50b0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/graphql/mutations/upsertObjectPermissionsMutation.ts @@ -0,0 +1,15 @@ +import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment'; +import { gql } from '@apollo/client'; + +export const UPSERT_OBJECT_PERMISSIONS = gql` + ${OBJECT_PERMISSION_FRAGMENT} + mutation UpsertObjectPermissions( + $upsertObjectPermissionsInput: UpsertObjectPermissionsInput! + ) { + upsertObjectPermissions( + upsertObjectPermissionsInput: $upsertObjectPermissionsInput + ) { + ...ObjectPermissionFragment + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx index cbe9a32ad..2b917bf95 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/components/SettingsRolePermissions.tsx @@ -14,11 +14,13 @@ const StyledRolePermissionsContainer = styled.div` type SettingsRolePermissionsProps = { roleId: string; isEditable: boolean; + isCreateMode: boolean; }; export const SettingsRolePermissions = ({ roleId, isEditable, + isCreateMode, }: SettingsRolePermissionsProps) => { const isPermissionsV2Enabled = useIsFeatureEnabled( FeatureFlagKey.IsPermissionsV2Enabled, @@ -30,7 +32,7 @@ export const SettingsRolePermissions = ({ roleId={roleId} isEditable={isEditable} /> - {isPermissionsV2Enabled && ( + {isPermissionsV2Enabled && !isCreateMode && ( , @@ -25,6 +25,7 @@ const SettingsRolePermissionsWrapper = ( ); }; @@ -42,6 +43,7 @@ export const Default: Story = { args: { roleId: '1', isEditable: true, + isCreateMode: false, }, }; @@ -49,11 +51,14 @@ export const ReadOnly: Story = { args: { roleId: '1', isEditable: false, + isCreateMode: false, }, }; export const PendingRole: Story = { args: { roleId: PENDING_ROLE_ID, + isEditable: true, + isCreateMode: true, }, }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent.tsx index 662a84133..6680f11a4 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent.tsx @@ -19,7 +19,8 @@ export const SettingsRolePermissionsObjectLevelObjectPickerDropdownContent = ({ }: SettingsRolePermissionsObjectLevelObjectPickerDropdownContentProps) => { const [searchFilter, setSearchFilter] = useState(''); - const { objectMetadataItems } = useFilteredObjectMetadataItems(); + const { alphaSortedActiveObjectMetadataItems: objectMetadataItems } = + useFilteredObjectMetadataItems(); const { getIcon } = useIcons(); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCell.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCell.tsx index 02159ab44..c1b3d8688 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCell.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelOverrideCell.tsx @@ -1,3 +1,4 @@ +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 { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { useTheme } from '@emotion/react'; @@ -46,12 +47,8 @@ export const SettingsRolePermissionsObjectLevelOverrideCell = ({ settingsDraftRoleFamilyState(objectPermission.roleId), ); - const permissionMappings = { - canReadObjectRecords: 'canReadAllObjectRecords', - canUpdateObjectRecords: 'canUpdateAllObjectRecords', - canSoftDeleteObjectRecords: 'canSoftDeleteAllObjectRecords', - canDestroyObjectRecords: 'canDestroyAllObjectRecords', - } as const; + const permissionMappings = + SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING; type ObjectPermissionKey = keyof typeof permissionMappings; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx index f60d7cfc4..2a4c6c3ed 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelSection.tsx @@ -1,24 +1,30 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { SettingsRolePermissionsObjectLevelObjectPickerDropdownContent } from '@/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelObjectPickerDropdownContent'; 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 { SettingsPath } from '@/types/SettingsPath'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; 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 { useRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { H2Title } from 'twenty-ui/display'; +import { H2Title, IconPlus } from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; +import { v4 } from 'uuid'; +import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -// 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 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)}; @@ -36,11 +42,14 @@ const StyledNoOverride = styled(TableCell)` export const SettingsRolePermissionsObjectLevelSection = ({ roleId, + isEditable, }: SettingsRolePermissionsObjectLevelSectionProps) => { - const settingsDraftRole = useRecoilValue( + const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( settingsDraftRoleFamilyState(roleId), ); + const navigate = useNavigateSettings(); + const objectMetadataItems = useObjectMetadataItems(); const objectMetadataMap = objectMetadataItems.objectMetadataItems.reduce( @@ -51,29 +60,52 @@ export const SettingsRolePermissionsObjectLevelSection = ({ {} as Record, ); - const objectPermissions = settingsDraftRole.objectPermissions; + const filteredObjectPermissions = settingsDraftRole.objectPermissions?.filter( + (objectPermission) => + (isDefined(objectPermission.canReadObjectRecords) && + objectPermission.canReadObjectRecords !== + settingsDraftRole.canReadAllObjectRecords) || + (isDefined(objectPermission.canUpdateObjectRecords) && + objectPermission.canUpdateObjectRecords !== + settingsDraftRole.canUpdateAllObjectRecords) || + (isDefined(objectPermission.canSoftDeleteObjectRecords) && + objectPermission.canSoftDeleteObjectRecords !== + settingsDraftRole.canSoftDeleteAllObjectRecords) || + (isDefined(objectPermission.canDestroyObjectRecords) && + objectPermission.canDestroyObjectRecords !== + settingsDraftRole.canDestroyAllObjectRecords), + ); - // const handleSelectObjectMetadata = (objectMetadataId: string) => { - // setSettingsDraftRole((draftRole) => ({ - // ...draftRole, - // objectPermissions: [ - // ...(draftRole.objectPermissions ?? []), - // { objectMetadataId, roleId, id: v4() }, - // ], - // })); - // }; + const handleSelectObjectMetadata = (objectMetadataId: string) => { + setSettingsDraftRole((draftRole) => ({ + ...draftRole, + objectPermissions: [ + ...(draftRole.objectPermissions ?? []).filter( + (permission) => + permission.objectMetadataId !== objectMetadataId || + permission.roleId !== roleId, + ), + { objectMetadataId, roleId, id: v4() }, + ], + })); + navigate(SettingsPath.RoleObjectLevel, { + roleId, + objectMetadataId, + }); + }; return (
- {isDefined(objectPermissions) && objectPermissions?.length > 0 ? ( - objectPermissions?.map((objectPermission) => ( + {isDefined(filteredObjectPermissions) && + filteredObjectPermissions?.length > 0 ? ( + filteredObjectPermissions?.map((objectPermission) => (
- {/* + objectPermission.objectMetadataId, + ) ?? [] + } onSelect={handleSelectObjectMetadata} /> } /> - */} +
); }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader.tsx index 09bcba362..b58db2c2c 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableHeader.tsx @@ -3,9 +3,9 @@ import { TableRow } from '@/ui/layout/table/components/TableRow'; import { t } from '@lingui/core/macro'; export const SettingsRolePermissionsObjectLevelTableHeader = () => ( - + {t`Object`} - {t`Permission overrides`} + {t`Permissions`} ); diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow.tsx index 5575a425a..42d6c57bb 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/components/SettingsRolePermissionsObjectLevelTableRow.tsx @@ -5,7 +5,11 @@ 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 { + IconChevronRight, + OverflowingTextWithTooltip, + useIcons, +} from 'twenty-ui/display'; import { ObjectPermission } from '~/generated/graphql'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; @@ -44,6 +48,7 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({ roleId: objectPermission.roleId, objectMetadataId: objectPermission.objectMetadataId, })} + gridAutoColumns="180px 1fr 1fr" > {!!Icon && ( @@ -54,7 +59,7 @@ export const SettingsRolePermissionsObjectLevelTableRow = ({ /> )} - {objectMetadataItem.labelPlural} + diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox.tsx new file mode 100644 index 000000000..69522bfdb --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox.tsx @@ -0,0 +1,95 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { IconReload, IconX } from 'twenty-ui/display'; +import { Checkbox } from 'twenty-ui/input'; + +export type OverridableCheckboxType = 'default' | 'override' | 'no_cta'; + +const StyledOverridableCheckboxContainer = styled.div` + align-items: center; + display: inline-flex; + justify-content: flex-start; + width: 48px; +`; + +const StyledOverridableCheckboxContainerItem = styled.div` + align-items: center; + display: flex; + height: 24px; + justify-content: center; + width: 24px; +`; + +const StyledIconWrapper = styled.div<{ + isDisabled?: boolean; +}>` + align-items: center; + cursor: ${({ isDisabled }) => (isDisabled ? 'not-allowed' : 'pointer')}; + display: flex; + height: 100%; + justify-content: center; + opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)}; + width: 100%; +`; + +export type OverridableCheckboxProps = { + type?: OverridableCheckboxType; + onChange: () => void; + checked: boolean; + disabled: boolean; +}; + +export const OverridableCheckbox = ({ + type = 'default', + onChange, + checked, + disabled, +}: OverridableCheckboxProps) => { + const theme = useTheme(); + + return ( + + {type === 'default' && ( + <> + + + + + + + + + + )} + {type === 'override' && ( + <> + + + + + + + + + + )} + {type === 'no_cta' && ( + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel.tsx index 7289b125c..9c7e15ae6 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevel.tsx @@ -1,13 +1,14 @@ 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 { SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader'; +import { SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow'; +import { SettingsRolePermissionsObjectLevelPermission } 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 { useRecoilState } from 'recoil'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; +import { ObjectPermission } from '~/generated-metadata/graphql'; const StyledTable = styled.div` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; @@ -27,7 +28,7 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({ roleId, objectMetadataItem, }: SettingsRolePermissionsObjectLevelObjectFormObjectLevelProps) => { - const settingsDraftRole = useRecoilValue( + const [settingsDraftRole, setSettingsDraftRole] = useRecoilState( settingsDraftRoleFamilyState(roleId), ); @@ -42,40 +43,56 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({ const objectLabel = objectMetadataItem.labelPlural; - const objectPermissionsConfig: SettingsRolePermissionsObjectPermission[] = [ - { - key: 'canReadObjectRecords', - label: t`See Records on ${objectLabel}`, - value: settingsDraftRoleObjectPermissions.canReadObjectRecords, - setValue: (_value: boolean) => { - // TODO: Implement + const updateObjectPermission = ( + permissionKey: keyof ObjectPermission, + value: boolean | null, + ) => { + setSettingsDraftRole((currentRole) => { + const updatedPermissions = currentRole.objectPermissions?.map((perm) => { + if (perm.objectMetadataId === objectMetadataItem.id) { + return { ...perm, [permissionKey]: value }; + } + return perm; + }); + return { ...currentRole, objectPermissions: updatedPermissions }; + }); + }; + + const objectPermissionsConfig: SettingsRolePermissionsObjectLevelPermission[] = + [ + { + key: 'canReadObjectRecords', + label: t`See Records on ${objectLabel}`, + value: settingsDraftRoleObjectPermissions.canReadObjectRecords, + setValue: (value: boolean | null) => { + updateObjectPermission('canReadObjectRecords', value); + }, }, - }, - { - key: 'canUpdateObjectRecords', - label: t`Edit Records on ${objectLabel}`, - value: settingsDraftRoleObjectPermissions.canUpdateObjectRecords, - setValue: (_value: boolean) => { - // TODO: Implement + { + key: 'canUpdateObjectRecords', + label: t`Edit Records on ${objectLabel}`, + value: settingsDraftRoleObjectPermissions.canUpdateObjectRecords, + setValue: (value: boolean | null) => { + updateObjectPermission('canUpdateObjectRecords', value); + }, }, - }, - { - key: 'canSoftDeleteObjectRecords', - label: t`Delete Records on ${objectLabel}`, - value: settingsDraftRoleObjectPermissions.canSoftDeleteObjectRecords, - setValue: (_value: boolean) => { - // TODO: Implement + { + key: 'canSoftDeleteObjectRecords', + label: t`Delete Records on ${objectLabel}`, + value: settingsDraftRoleObjectPermissions.canSoftDeleteObjectRecords, + setValue: (value: boolean | null) => { + updateObjectPermission('canSoftDeleteObjectRecords', value); + }, }, - }, - { - key: 'canDestroyObjectRecords', - label: t`Destroy Records on ${objectLabel}`, - value: settingsDraftRoleObjectPermissions.canDestroyObjectRecords, - setValue: (_value: boolean) => { - // TODO: Implement + { + key: 'canDestroyObjectRecords', + label: t`Destroy Records on ${objectLabel}`, + value: settingsDraftRoleObjectPermissions.canDestroyObjectRecords, + setValue: (value: boolean | null) => { + updateObjectPermission('canDestroyObjectRecords', value); + }, }, - }, - ]; + ]; return (
@@ -84,13 +101,17 @@ export const SettingsRolePermissionsObjectLevelObjectFormObjectLevel = ({ description={t`Ability to interact with this specific object`} /> - + {objectPermissionsConfig.map((permission) => ( - ))} diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader.tsx similarity index 86% rename from packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader.tsx rename to packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader.tsx index e39c02864..43ff79a2c 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelHeader.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader.tsx @@ -2,9 +2,9 @@ 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 = +export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableHeader = () => ( - + {t`Name`} diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow.tsx new file mode 100644 index 000000000..b86826da5 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/object-level-permissions/object-form/components/SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow.tsx @@ -0,0 +1,141 @@ +import { OverridableCheckbox } from '@/settings/roles/role-permissions/object-level-permissions/object-form/components/OverridableCheckbox'; +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 { SettingsRolePermissionsObjectLevelPermission } from '@/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission'; +import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { ObjectPermission } from '~/generated-metadata/graphql'; +import type { Role } from '~/generated/graphql'; + +const StyledLabel = styled.span` + color: ${({ theme }) => theme.font.color.primary}; +`; + +const StyledOverrideInfo = styled.span` + background: ${({ theme }) => theme.adaptiveColors.orange1}; + border-radius: ${({ theme }) => theme.spacing(1)}; + color: ${({ theme }) => theme.color.orange}; + padding: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledPermissionCell = styled(TableCell)` + align-items: center; + display: flex; + flex: 1; + gap: ${({ theme }) => theme.spacing(2)}; + padding-left: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledCheckboxCell = styled(TableCell)` + align-items: center; + display: flex; + justify-content: flex-end; + padding-right: ${({ theme }) => theme.spacing(4)}; +`; + +const StyledTableRow = styled(TableRow)` + align-items: center; + display: flex; +`; + +type OverridableCheckboxType = 'no_cta' | 'default' | 'override'; + +type SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps = { + permission: SettingsRolePermissionsObjectLevelPermission; + isEditable: boolean; + settingsDraftRoleObjectPermissions: ObjectPermission; + roleId: string; +}; + +export const SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRow = + ({ + permission, + isEditable, + settingsDraftRoleObjectPermissions, + roleId, + }: SettingsRolePermissionsObjectLevelObjectFormObjectLevelTableRowProps) => { + const theme = useTheme(); + + const settingsDraftRole = useRecoilValue( + settingsDraftRoleFamilyState(roleId), + ); + + const label = permission.label; + + const { Icon } = + SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission.key]; + + const permissionMappings = + SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING; + + const settingsDraftRoleObjectPermissionValue = + settingsDraftRoleObjectPermissions[ + permission.key as keyof ObjectPermission + ]; + + const rolePermission = + permissionMappings[permission.key as keyof typeof permissionMappings]; + + const settingsDraftRoleGlobalPermissionValue = + settingsDraftRole[rolePermission as keyof Role]; + + const isChecked = !!settingsDraftRoleObjectPermissionValue; + + const isRevoked = + isDefined(settingsDraftRoleObjectPermissionValue) && + settingsDraftRoleGlobalPermissionValue === true && + isChecked === false; + + let checkboxType: OverridableCheckboxType; + + if ( + settingsDraftRoleGlobalPermissionValue === true && + settingsDraftRoleObjectPermissionValue === false + ) { + checkboxType = 'override'; + } else if (settingsDraftRoleGlobalPermissionValue === false) { + checkboxType = 'no_cta'; + } else { + checkboxType = 'default'; + } + + const handleCheckboxChange = () => { + if (!isEditable) return; + + if (checkboxType === 'default') { + permission.setValue(false); + } else if (checkboxType === 'override') { + permission.setValue(null); + } else if (checkboxType === 'no_cta') { + permission.setValue(!isChecked); + } + }; + + return ( + + + + {label} + {isRevoked ? ( + + {t`Revoked for this object`} + + ) : null} + + + + + + ); + }; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsSection.tsx b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsSection.tsx index 5db21d263..b7edb366d 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsSection.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/components/SettingsRolePermissionsObjectsSection.tsx @@ -5,7 +5,6 @@ import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDr 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'; @@ -37,12 +36,11 @@ export const SettingsRolePermissionsObjectsSection = ({ { key: 'canReadObjectRecords', label: t`See Records on All Objects`, - overriddenBy: + revokedBy: objectPermissions?.filter( (permission) => - isDefined(permission.canReadObjectRecords) && - permission.canReadObjectRecords !== - settingsDraftRole.canReadAllObjectRecords, + permission.canReadObjectRecords === false && + settingsDraftRole.canReadAllObjectRecords === true, )?.length ?? 0, value: settingsDraftRole.canReadAllObjectRecords, setValue: (value: boolean) => { @@ -55,12 +53,11 @@ export const SettingsRolePermissionsObjectsSection = ({ { key: 'canUpdateObjectRecords', label: t`Edit Records on All Objects`, - overriddenBy: + revokedBy: objectPermissions?.filter( (permission) => - isDefined(permission.canUpdateObjectRecords) && - permission.canUpdateObjectRecords !== - settingsDraftRole.canUpdateAllObjectRecords, + permission.canUpdateObjectRecords === false && + settingsDraftRole.canUpdateAllObjectRecords === true, )?.length ?? 0, value: settingsDraftRole.canUpdateAllObjectRecords, setValue: (value: boolean) => { @@ -73,12 +70,11 @@ export const SettingsRolePermissionsObjectsSection = ({ { key: 'canSoftDeleteObjectRecords', label: t`Delete Records on All Objects`, - overriddenBy: + revokedBy: objectPermissions?.filter( (permission) => - isDefined(permission.canSoftDeleteObjectRecords) && - permission.canSoftDeleteObjectRecords !== - settingsDraftRole.canSoftDeleteAllObjectRecords, + permission.canSoftDeleteObjectRecords === false && + settingsDraftRole.canSoftDeleteAllObjectRecords === true, )?.length ?? 0, value: settingsDraftRole.canSoftDeleteAllObjectRecords, setValue: (value: boolean) => { @@ -91,12 +87,11 @@ export const SettingsRolePermissionsObjectsSection = ({ { key: 'canDestroyObjectRecords', label: t`Destroy Records on All Objects`, - overriddenBy: + revokedBy: objectPermissions?.filter( (permission) => - isDefined(permission.canDestroyObjectRecords) && - permission.canDestroyObjectRecords !== - settingsDraftRole.canDestroyAllObjectRecords, + permission.canDestroyObjectRecords === false && + settingsDraftRole.canDestroyAllObjectRecords === true, )?.length ?? 0, value: settingsDraftRole.canDestroyAllObjectRecords, setValue: (value: boolean) => { @@ -112,7 +107,7 @@ export const SettingsRolePermissionsObjectsSection = ({
{ const theme = useTheme(); - const isOverriddenBy = permission.overriddenBy; - const isOverridden = isOverriddenBy && isOverriddenBy > 0; + const revokedBy = permission.revokedBy; + const isRevoked = revokedBy && revokedBy > 0; const label = permission.label; - const pluralizedObject = pluralize('object', isOverriddenBy); + const pluralizedObject = pluralize('object', revokedBy); const { Icon } = SETTINGS_ROLE_OBJECT_PERMISSION_ICON_CONFIG[permission.key]; @@ -62,9 +62,9 @@ export const SettingsRolePermissionsObjectsTableRow = ({ {label} - {isOverridden ? ( + {isRevoked ? ( - {t`Overridden on ${isOverriddenBy} ${pluralizedObject}`} + {t`Revoked on ${revokedBy} ${pluralizedObject}`} ) : null} @@ -73,7 +73,7 @@ export const SettingsRolePermissionsObjectsTableRow = ({ checked={permission.value ?? false} onChange={() => permission.setValue(!permission.value)} disabled={!isEditable} - accent={isOverridden ? CheckboxAccent.Orange : CheckboxAccent.Blue} + accent={isRevoked ? CheckboxAccent.Orange : CheckboxAccent.Blue} /> diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping.ts b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping.ts new file mode 100644 index 000000000..f761001db --- /dev/null +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/constants/settingsRoleObjectLevelPermissionToRoleObjectPermissionMapping.ts @@ -0,0 +1,7 @@ +export const SETTINGS_ROLE_OBJECT_LEVEL_PERMISSION_TO_ROLE_OBJECT_PERMISSION_MAPPING = + { + canReadObjectRecords: 'canReadAllObjectRecords', + canUpdateObjectRecords: 'canUpdateAllObjectRecords', + canSoftDeleteObjectRecords: 'canSoftDeleteAllObjectRecords', + canDestroyObjectRecords: 'canDestroyAllObjectRecords', + } as const; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission.ts b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission.ts index 8034156dc..c5312ae7e 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission.ts +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/objects-permissions/types/SettingsRolePermissionsObjectPermission.ts @@ -2,7 +2,14 @@ import { ReactNode } from 'react'; export type SettingsRolePermissionsObjectPermission = { key: string; label: string | ReactNode; - value?: boolean | null; + value?: boolean; setValue: (value: boolean) => void; - overriddenBy?: number; + revokedBy?: number; +}; + +export type SettingsRolePermissionsObjectLevelPermission = { + key: string; + label: string | ReactNode; + value?: boolean | null; + setValue: (value: boolean | null) => void; }; diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx index 6c7157b6a..ce7b79f42 100644 --- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRole.tsx @@ -29,6 +29,7 @@ import { Role, useCreateOneRoleMutation, useUpdateOneRoleMutation, + useUpsertObjectPermissionsMutation, useUpsertSettingPermissionsMutation, } from '~/generated/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; @@ -67,6 +68,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { const [createRole] = useCreateOneRoleMutation(); const [updateRole] = useUpdateOneRoleMutation(); const [upsertSettingPermissions] = useUpsertSettingPermissionsMutation(); + const [upsertObjectPermissions] = useUpsertObjectPermissionsMutation(); const { addWorkspaceMembersToRole } = useUpdateWorkspaceMemberRole(roleId); @@ -142,25 +144,54 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { }, }, onCompleted: async (data) => { - await addWorkspaceMembersToRole({ - roleId: data.createOneRole.id, - workspaceMemberIds: settingsDraftRole.workspaceMembers.map( - (member) => member.id, - ), - }); + if (isDefined(dirtyFields.workspaceMembers)) { + await addWorkspaceMembersToRole({ + roleId: data.createOneRole.id, + workspaceMemberIds: settingsDraftRole.workspaceMembers.map( + (member) => member.id, + ), + }); + } - await upsertSettingPermissions({ - variables: { - upsertSettingPermissionsInput: { - roleId: data.createOneRole.id, - settingPermissionKeys: - settingsDraftRole.settingPermissions?.map( - (settingPermission) => settingPermission.setting, - ) ?? [], + if (isDefined(dirtyFields.settingPermissions)) { + await upsertSettingPermissions({ + variables: { + upsertSettingPermissionsInput: { + roleId: data.createOneRole.id, + settingPermissionKeys: + settingsDraftRole.settingPermissions?.map( + (settingPermission) => settingPermission.setting, + ) ?? [], + }, }, - }, - refetchQueries: [getOperationName(GET_ROLES) ?? ''], - }); + refetchQueries: [getOperationName(GET_ROLES) ?? ''], + }); + } + + if (isDefined(dirtyFields.objectPermissions)) { + await upsertObjectPermissions({ + 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, @@ -206,6 +237,30 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { refetchQueries: [getOperationName(GET_ROLES) ?? ''], }); } + + if (isDefined(dirtyFields.objectPermissions)) { + await upsertObjectPermissions({ + variables: { + upsertObjectPermissionsInput: { + roleId: roleId, + objectPermissions: + settingsDraftRole.objectPermissions?.map( + (objectPermission) => ({ + objectMetadataId: objectPermission.objectMetadataId, + canReadObjectRecords: objectPermission.canReadObjectRecords, + canUpdateObjectRecords: + objectPermission.canUpdateObjectRecords, + canSoftDeleteObjectRecords: + objectPermission.canSoftDeleteObjectRecords, + canDestroyObjectRecords: + objectPermission.canDestroyObjectRecords, + }), + ) ?? [], + }, + }, + refetchQueries: [getOperationName(GET_ROLES) ?? ''], + }); + } } }; @@ -251,6 +306,7 @@ export const SettingsRole = ({ roleId, isCreateMode }: SettingsRoleProps) => { )} {activeTabId === SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.SETTINGS && ( diff --git a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleEditEffect.tsx b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleEditEffect.tsx index a95752eee..bf7e0d693 100644 --- a/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleEditEffect.tsx +++ b/packages/twenty-front/src/modules/settings/roles/role/components/SettingsRoleEditEffect.tsx @@ -2,10 +2,13 @@ import { SETTINGS_ROLE_DETAIL_TABS } from '@/settings/roles/role/constants/Setti import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState'; import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState'; import { activeTabIdComponentState } from '@/ui/layout/tab/states/activeTabIdComponentState'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useEffect, useState } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilCallback, useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { Role } from '~/generated/graphql'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type SettingsRoleEditEffectProps = { roleId: string; @@ -17,24 +20,35 @@ export const SettingsRoleEditEffect = ({ const [isInitialized, setIsInitialized] = useState(false); const role = useRecoilValue(settingsPersistedRoleFamilyState(roleId)); - const setDraftRole = useSetRecoilState(settingsDraftRoleFamilyState(roleId)); const setActiveTabId = useSetRecoilComponentStateV2( activeTabIdComponentState, SETTINGS_ROLE_DETAIL_TABS.COMPONENT_INSTANCE_ID, ); + const updateDraftRoleIfNeeded = useRecoilCallback( + ({ set, snapshot }) => + (newRole: Role) => { + const currentPersistedRole = getSnapshotValue( + snapshot, + settingsPersistedRoleFamilyState(newRole.id), + ); + + if (!isDeeplyEqual(newRole, currentPersistedRole)) { + set(settingsDraftRoleFamilyState(newRole.id), newRole); + } + }, + [], + ); + useEffect(() => { - if (isInitialized) { + if (isInitialized || !isDefined(role)) { return; } setActiveTabId(SETTINGS_ROLE_DETAIL_TABS.TABS_IDS.ASSIGNMENT); - - if (isDefined(role)) { - setDraftRole(role); - setIsInitialized(true); - } - }, [isInitialized, role, setActiveTabId, setDraftRole]); + updateDraftRoleIfNeeded(role); + setIsInitialized(true); + }, [isInitialized, role, setActiveTabId, updateDraftRoleIfNeeded]); return <>; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input.ts similarity index 62% rename from packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input.ts rename to packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input.ts index 203fc3691..74748df32 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input.ts @@ -1,14 +1,30 @@ import { Field, InputType } from '@nestjs/graphql'; -import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator'; +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsNotEmpty, + IsOptional, + IsUUID, +} from 'class-validator'; @InputType() -export class UpsertObjectPermissionInput { +export class UpsertObjectPermissionsInput { @IsUUID() @IsNotEmpty() @Field() roleId: string; + @IsArray() + @ArrayMinSize(1) + @IsNotEmpty() + @Field(() => [ObjectPermissionInput]) + objectPermissions: ObjectPermissionInput[]; +} + +@InputType() +export class ObjectPermissionInput { @IsUUID() @IsNotEmpty() @Field() diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts index a023b2fa0..66f58a1af 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts @@ -1,10 +1,10 @@ import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { UpsertObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input'; +import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input'; import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { PermissionsException, @@ -25,24 +25,29 @@ export class ObjectPermissionService { private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, ) {} - public async upsertObjectPermission({ + public async upsertObjectPermissions({ workspaceId, input, }: { workspaceId: string; - input: UpsertObjectPermissionInput; - }): Promise { + input: UpsertObjectPermissionsInput; + }): Promise { try { await this.validateRoleIsEditableOrThrow({ roleId: input.roleId, workspaceId, }); - const result = await this.objectPermissionRepository.upsert( - { + const objectPermissions = input.objectPermissions.map( + (objectPermission) => ({ + ...objectPermission, + roleId: input.roleId, workspaceId, - ...input, - }, + }), + ); + + const result = await this.objectPermissionRepository.upsert( + objectPermissions, { conflictPaths: ['objectMetadataId', 'roleId'], }, @@ -61,9 +66,14 @@ export class ObjectPermissionService { }, ); - return this.objectPermissionRepository.findOne({ + return this.objectPermissionRepository.find({ where: { - id: objectPermissionId, + roleId: input.roleId, + objectMetadataId: In( + input.objectPermissions.map( + (objectPermission) => objectPermission.objectMetadataId, + ), + ), }, }); } catch (error) { @@ -71,7 +81,9 @@ export class ObjectPermissionService { error, roleId: input.roleId, workspaceId, - objectMetadataId: input.objectMetadataId, + objectMetadataIds: input.objectPermissions.map( + (objectPermission) => objectPermission.objectMetadataId, + ), }); throw error; @@ -82,12 +94,12 @@ export class ObjectPermissionService { error, roleId, workspaceId, - objectMetadataId, + objectMetadataIds, }: { error: Error; roleId: string; workspaceId: string; - objectMetadataId: string; + objectMetadataIds: string[]; }) { if (error.message.includes('violates foreign key constraint')) { const role = await this.roleRepository.findOne({ @@ -104,14 +116,14 @@ export class ObjectPermissionService { ); } - const objectMetadata = await this.objectMetadataRepository.findOne({ + const objectMetadata = await this.objectMetadataRepository.find({ where: { workspaceId, - id: objectMetadataId, + id: In(objectMetadataIds), }, }); - if (!isDefined(objectMetadata)) { + if (objectMetadata.length !== objectMetadataIds.length) { throw new PermissionsException( PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND, PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts index 59bdf5ac8..84f98d227 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.resolver.ts @@ -17,7 +17,7 @@ import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto'; -import { UpsertObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input'; +import { UpsertObjectPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permissions.input'; import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { @@ -147,21 +147,18 @@ export class RoleResolver { return deletedRoleId; } - @Mutation(() => ObjectPermissionDTO) - async upsertOneObjectPermission( + @Mutation(() => [ObjectPermissionDTO]) + async upsertObjectPermissions( @AuthWorkspace() workspace: Workspace, - @Args('upsertObjectPermissionInput') - upsertObjectPermissionInput: UpsertObjectPermissionInput, - ) { + @Args('upsertObjectPermissionsInput') + upsertObjectPermissionsInput: UpsertObjectPermissionsInput, + ): Promise { await this.validatePermissionsV2EnabledOrThrow(workspace); - const objectPermission = - await this.objectPermissionService.upsertObjectPermission({ - workspaceId: workspace.id, - input: upsertObjectPermissionInput, - }); - - return objectPermission; + return this.objectPermissionService.upsertObjectPermissions({ + workspaceId: workspace.id, + input: upsertObjectPermissionsInput, + }); } @Mutation(() => [SettingPermissionDTO]) @@ -169,7 +166,7 @@ export class RoleResolver { @AuthWorkspace() workspace: Workspace, @Args('upsertSettingPermissionsInput') upsertSettingPermissionsInput: UpsertSettingPermissionsInput, - ) { + ): Promise { await this.validatePermissionsV2EnabledOrThrow(workspace); return this.settingPermissionService.upsertSettingPermissions({ diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts index c3f09c1dd..2c0e371a7 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts @@ -72,19 +72,12 @@ describe('roles permissions', () => { }); afterAll(async () => { - const disablePermissionsQuery = updateFeatureFlagFactory( - SEED_APPLE_WORKSPACE_ID, - 'IsPermissionsEnabled', - false, - ); - const disablePermissionsV2Query = updateFeatureFlagFactory( SEED_APPLE_WORKSPACE_ID, 'IsPermissionsV2Enabled', false, ); - await makeGraphqlAPIRequest(disablePermissionsQuery); await makeGraphqlAPIRequest(disablePermissionsV2Query); }); @@ -480,9 +473,10 @@ describe('roles permissions', () => { roleId: string; }) => ` mutation UpsertObjectPermissions { - upsertOneObjectPermission(upsertObjectPermissionInput: {objectMetadataId: "${objectMetadataId}", roleId: "${roleId}", canUpdateObjectRecords: true}) { + upsertObjectPermissions(upsertObjectPermissionsInput: { roleId: "${roleId}", objectPermissions: [{objectMetadataId: "${objectMetadataId}", canUpdateObjectRecords: true}]}) { id roleId + objectMetadataId canUpdateObjectRecords } } @@ -540,12 +534,15 @@ describe('roles permissions', () => { .expect((res) => { expect(res.body.data).toBeDefined(); expect(res.body.errors).toBeUndefined(); - expect(res.body.data.upsertOneObjectPermission.roleId).toBe( - createdEditableRoleId, + expect(res.body.data.upsertObjectPermissions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + roleId: createdEditableRoleId, + objectMetadataId: listingObjectId, + canUpdateObjectRecords: true, + }), + ]), ); - expect( - res.body.data.upsertOneObjectPermission.canUpdateObjectRecords, - ).toBe(true); }); }); });