[permissions] QA fixes (#13698)

In this PR

- Introduction of readableFields and updatableFields in
objectMetadataItem selector to ease filtering from a developer
experience perspective ( + to help developers think to do it). In
discussion @lucasbordeau @charlesBochet
- Remove non-updatable field from CSV import process (@etiennejouan)
- QA fix / Non-readable fields should not show on show page
- QA fix / It should not be offered to create a kanban view on a
non-readable field
- QA fix / It should not be offered to create view groups on a
non-readable field
- QA fix / Rating field should have a readonly mode

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2025-08-07 18:21:59 +02:00
committed by GitHub
parent 258f21a4be
commit ee19ee5119
120 changed files with 1211 additions and 943 deletions
@@ -37,6 +37,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
objectMetadataId: personObjectMetadataItem.id,
restrictedFields: {},
},
},
});
@@ -140,6 +141,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
objectMetadataId: personObjectMetadataItem.id,
restrictedFields: {},
},
},
});
@@ -0,0 +1,57 @@
import { getNonReadableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions';
import { getNonUpdatableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
type enrichObjectMetadataItemsWithPermissionsArgs = {
objectMetadataItems: Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
};
export const enrichObjectMetadataItemsWithPermissions = ({
objectMetadataItems,
objectPermissionsByObjectMetadataId,
}: enrichObjectMetadataItemsWithPermissionsArgs) => {
const formattedObjects: ObjectMetadataItem[] =
objectMetadataItems.map((object) => {
const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: object.id,
});
const nonReadableFieldMetadataIds = !isDefined(objectPermissions)
? []
: getNonReadableFieldMetadataIdsFromObjectPermissions({
objectPermissions: objectPermissions,
});
const nonUpdatableFieldMetadataIds = !isDefined(objectPermissions)
? []
: getNonUpdatableFieldMetadataIdsFromObjectPermissions({
objectPermissions: objectPermissions,
});
const { fields, ...objectWithoutFields } = object;
return {
...objectWithoutFields,
fields: fields,
readableFields: fields.filter(
(field) => !nonReadableFieldMetadataIds.includes(field.id),
),
updatableFields: fields.filter(
(field) => !nonUpdatableFieldMetadataIds.includes(field.id),
),
} satisfies ObjectMetadataItem;
}) ?? [];
return formattedObjects;
};
@@ -1,9 +1,9 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getActiveFieldMetadataItems = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
objectMetadataItem: Pick<ObjectMetadataItem, 'readableFields'>,
) =>
objectMetadataItem.fields.filter(
objectMetadataItem.readableFields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
);
@@ -0,0 +1,15 @@
import { ObjectPermissions } from 'twenty-shared/types';
type GetNonReadableFieldMetadataIdsFromObjectPermissionsArgs = {
objectPermissions: ObjectPermissions;
};
export const getNonReadableFieldMetadataIdsFromObjectPermissions = ({
objectPermissions,
}: GetNonReadableFieldMetadataIdsFromObjectPermissionsArgs) => {
const restrictedFields = objectPermissions.restrictedFields;
return Object.entries(restrictedFields)
.filter(([_, restrictedField]) => restrictedField.canRead === false)
.map(([fieldMetadataId]) => fieldMetadataId);
};
@@ -0,0 +1,15 @@
import { ObjectPermissions } from 'twenty-shared/types';
type GetNonUpdatableFieldMetadataIdsFromObjectPermissionsArgs = {
objectPermissions: ObjectPermissions;
};
export const getNonUpdatableFieldMetadataIdsFromObjectPermissions = ({
objectPermissions,
}: GetNonUpdatableFieldMetadataIdsFromObjectPermissionsArgs): string[] => {
const restrictedFields = objectPermissions.restrictedFields;
return Object.entries(restrictedFields)
.filter(([_, restrictedField]) => restrictedField.canUpdate === false)
.map(([fieldMetadataId]) => fieldMetadataId);
};
@@ -1,12 +1,18 @@
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated/graphql';
export const getObjectPermissionsForObject = (
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>,
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>,
objectMetadataId: string,
): ObjectPermission => {
const objectPermissions =
objectPermissionsByObjectMetadataId[objectMetadataId];
): ObjectPermissions & { objectMetadataId: string } => {
const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId,
});
if (!isDefined(objectPermissions)) {
return {
@@ -1,21 +0,0 @@
import { RestrictedField } from 'twenty-shared/types';
import { ObjectPermission } from '~/generated/graphql';
export const getReadRestrictedFieldMetadataIdsFromObjectPermissions = ({
objectPermissions,
objectMetadataId,
}: {
objectPermissions?: ObjectPermission[] | null;
objectMetadataId: string;
}) => {
const restrictedFields = objectPermissions?.find(
(permission) => permission.objectMetadataId === objectMetadataId,
)?.restrictedFields;
return Object.entries(restrictedFields ?? {})
.filter(
([_fieldMetadataId, restrictedField]) =>
(restrictedField as RestrictedField).canRead === false,
)
.map(([fieldMetadataId]) => fieldMetadataId);
};
@@ -1,10 +1,6 @@
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { isUndefined } from '@sniptt/guards';
import {
FieldMetadataType,
ObjectPermission,
RelationType,
} from '~/generated-metadata/graphql';
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
@@ -12,6 +8,7 @@ import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isNonCompositeField } from '@/object-record/object-filter-dropdown/utils/isNonCompositeField';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { ObjectPermissions } from 'twenty-shared/types';
type MapFieldMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[];
@@ -22,7 +19,10 @@ type MapFieldMetadataToGraphQLQueryArgs = {
>;
relationRecordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
// TODO: change ObjectMetadataItems mock before refactoring with relation computed field
@@ -1,24 +1,25 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery';
import { shouldFieldBeQueried } from '@/object-metadata/utils/shouldFieldBeQueried';
import { RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields';
import { isRecordGqlFieldsNode } from '@/object-record/graphql/utils/isRecordGraphlFieldsNode';
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataType, ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated/graphql';
type MapObjectMetadataToGraphQLQueryArgs = {
objectMetadataItems: ObjectMetadataItem[];
objectMetadataItem: Pick<
ObjectMetadataItem,
'nameSingular' | 'fields' | 'id'
'nameSingular' | 'fields' | 'id' | 'readableFields'
>;
recordGqlFields?: RecordGqlFields;
computeReferences?: boolean;
isRootLevel?: boolean;
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isFieldsPermissionsEnabled?: boolean;
};
@@ -45,22 +46,10 @@ export const mapObjectMetadataToGraphQLQuery = ({
}
}
const filterReadableFields = (field: FieldMetadataItem) => {
if (isFieldsPermissionsEnabled !== true) {
return true;
}
const objectPermission = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
return objectPermission.restrictedFields?.[field.id]?.canRead !== false;
};
const manyToOneRelationFields = objectMetadataItem?.fields
const manyToOneRelationFields = objectMetadataItem?.readableFields
.filter((field) => field.isActive)
.filter((field) => field.type === FieldMetadataType.RELATION)
.filter((field) => isDefined(field.settings?.joinColumnName))
.filter(filterReadableFields);
.filter((field) => isDefined(field.settings?.joinColumnName));
const manyToOneRelationGqlFieldWithFieldMetadata =
manyToOneRelationFields.map((field) => ({
@@ -69,9 +58,8 @@ export const mapObjectMetadataToGraphQLQuery = ({
}));
const gqlFieldWithFieldMetadataThatCouldBeQueried = [
...objectMetadataItem.fields
...objectMetadataItem.readableFields
.filter((fieldMetadata) => fieldMetadata.isActive)
.filter(filterReadableFields)
.map((fieldMetadata) => ({
gqlField: fieldMetadata.name,
fieldMetadata,
@@ -4,12 +4,17 @@ import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/o
import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
type mapPaginatedObjectMetadataItemsToObjectMetadataItemsArgs = {
pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined;
};
export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
pagedObjectMetadataItems,
}: {
pagedObjectMetadataItems: ObjectMetadataItemsQuery | undefined;
}) => {
const formattedObjects: ObjectMetadataItem[] =
}: mapPaginatedObjectMetadataItemsToObjectMetadataItemsArgs) => {
const formattedObjects: Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>[] =
pagedObjectMetadataItems?.objects.edges.map((object) => {
const labelIdentifierFieldMetadataId =
objectMetadataItemSchema.shape.labelIdentifierFieldMetadataId.parse(
@@ -35,7 +40,10 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
),
}) satisfies IndexMetadataItem,
),
} satisfies ObjectMetadataItem;
} satisfies Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>;
}) ?? [];
return formattedObjects;
@@ -1,11 +1,11 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const mapSoftDeleteFieldsToGraphQLQuery = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
objectMetadataItem: Pick<ObjectMetadataItem, 'readableFields'>,
): string => {
const softDeleteFields = ['deletedAt', 'id'];
const fieldsThatShouldBeQueried = objectMetadataItem.fields
const fieldsThatShouldBeQueried = objectMetadataItem.readableFields
.filter((field) => field.isActive && softDeleteFields.includes(field.name))
.sort(
(a, b) =>