Field permission CRUD front-end implementation (#13524)

This PR implements what's needed to edit field permissions on a role.

Field permissions that aren't useful are kept in the database to avoid
the overhead of adding cleaning logics both in front end and back end.
They just won't be taken into account if object permission doesn't allow
it.

In this PR we also handle the case were there's no object permission
override but where there are only field permission overrides, which can
happen if an object inherits from a "all object can read" but restricts
read only on some fields.

<img width="547" height="642" alt="image"
src="https://github.com/user-attachments/assets/77d81f89-4af9-42b6-97f3-fae3a6ba1eeb"
/>


<img width="590" height="912" alt="image"
src="https://github.com/user-attachments/assets/69fab8ee-7252-401a-bc6f-8a8b7c7f6bc4"
/>

Fixes https://github.com/twentyhq/core-team-issues/issues/1152

This PR lowers unit test coverage because the essential unit tests for
this PR have been written and writing more tests would not be a great
tradeoff for this feature which has already taken a lot of efforts.
This commit is contained in:
Lucas Bordeau
2025-08-05 11:50:23 +02:00
committed by GitHub
parent 497fc2c0b2
commit 04ed843aaf
59 changed files with 2358 additions and 527 deletions
@@ -0,0 +1,112 @@
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
import { FieldPermission, ObjectPermission, Role } from '~/generated/graphql';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
export const MOCK_ROLE_ID_GRANTS_ALL = 'role-id-1';
export const MOCK_ROLE_ID_REVOKES_ALL = 'role-id-2';
export const MOCK_OBJECT_PERMISSION_1_REVOKES_ALL: ObjectPermission = {
objectMetadataId: 'object-metadata-1',
canReadObjectRecords: false,
canUpdateObjectRecords: false,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
};
export const MOCK_OBJECT_PERMISSION_2_GRANTS_ALL: ObjectPermission = {
objectMetadataId: 'object-metadata-2',
canReadObjectRecords: true,
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
};
export const MOCK_OBJECT_PERMISSION_3_NULL_ALL: ObjectPermission = {
objectMetadataId: 'object-metadata-3',
canReadObjectRecords: null,
canUpdateObjectRecords: null,
canSoftDeleteObjectRecords: null,
canDestroyObjectRecords: null,
};
export const BASE_FIELD_PERMISSION: Omit<FieldPermission, 'roleId'> = {
id: 'field-permission-1',
fieldMetadataId: 'field-metadata-id-1',
objectMetadataId: MOCK_OBJECT_PERMISSION_1_REVOKES_ALL.objectMetadataId,
canReadFieldValue: false,
canUpdateFieldValue: false,
};
export const BASE_FIELD_PERMISSION_OBJECT_METADATA_3: Omit<
FieldPermission,
'roleId'
> = {
id: 'field-permission-1',
fieldMetadataId: 'field-metadata-id-1',
objectMetadataId: 'object-metadata-3',
canReadFieldValue: false,
canUpdateFieldValue: false,
};
export const BASE_ROLE_MOCK_GRANTS_ALL: Role = {
canAccessAllTools: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
canUpdateAllSettings: true,
id: MOCK_ROLE_ID_GRANTS_ALL,
isEditable: true,
label: 'Role grants all',
workspaceMembers: [],
fieldPermissions: [
{ ...BASE_FIELD_PERMISSION, roleId: MOCK_ROLE_ID_GRANTS_ALL },
{
...BASE_FIELD_PERMISSION_OBJECT_METADATA_3,
roleId: MOCK_ROLE_ID_GRANTS_ALL,
},
],
objectPermissions: [
MOCK_OBJECT_PERMISSION_1_REVOKES_ALL,
MOCK_OBJECT_PERMISSION_2_GRANTS_ALL,
MOCK_OBJECT_PERMISSION_3_NULL_ALL,
],
};
export const BASE_ROLE_MOCK_REVOKES_ALL: Role = {
canAccessAllTools: false,
canReadAllObjectRecords: false,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
canUpdateAllSettings: true,
id: MOCK_ROLE_ID_REVOKES_ALL,
isEditable: true,
label: 'Role revokes all',
workspaceMembers: [],
fieldPermissions: [
{ ...BASE_FIELD_PERMISSION, roleId: MOCK_ROLE_ID_REVOKES_ALL },
{
...BASE_FIELD_PERMISSION_OBJECT_METADATA_3,
roleId: MOCK_ROLE_ID_REVOKES_ALL,
},
],
objectPermissions: [
MOCK_OBJECT_PERMISSION_1_REVOKES_ALL,
MOCK_OBJECT_PERMISSION_2_GRANTS_ALL,
MOCK_OBJECT_PERMISSION_3_NULL_ALL,
],
};
export const rolesMockHookWrapper = getJestMetadataAndApolloMocksWrapper({
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(
settingsDraftRoleFamilyState(MOCK_ROLE_ID_GRANTS_ALL),
BASE_ROLE_MOCK_GRANTS_ALL,
);
snapshot.set(
settingsDraftRoleFamilyState(MOCK_ROLE_ID_REVOKES_ALL),
BASE_ROLE_MOCK_REVOKES_ALL,
);
},
});