[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
@@ -11,6 +11,29 @@ import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMeta
jest.mock('@/object-metadata/hooks/useObjectMetadataItem');
jest.mock('@/object-record/utils/generateAggregateQuery');
const fields = [
{
id: '20202020-fed9-4ce5-9502-02a8efaf46e1',
name: 'amount',
label: 'Amount',
type: FieldMetadataType.NUMBER,
isCustom: false,
isActive: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as FieldMetadataItem,
{
id: '20202020-dd4a-4ea4-bb7b-1c7300491b65',
name: 'name',
label: 'Name',
type: FieldMetadataType.TEXT,
isCustom: false,
isActive: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as FieldMetadataItem,
];
const mockObjectMetadataItem: ObjectMetadataItem = {
nameSingular: 'company',
namePlural: 'companies',
@@ -23,28 +46,9 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
isActive: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
fields: [
{
id: '20202020-fed9-4ce5-9502-02a8efaf46e1',
name: 'amount',
label: 'Amount',
type: FieldMetadataType.NUMBER,
isCustom: false,
isActive: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as FieldMetadataItem,
{
id: '20202020-dd4a-4ea4-bb7b-1c7300491b65',
name: 'name',
label: 'Name',
type: FieldMetadataType.TEXT,
isCustom: false,
isActive: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as FieldMetadataItem,
],
fields,
readableFields: fields,
updatableFields: fields,
indexMetadatas: [],
isLabelSyncedWithName: true,
isRemote: false,
@@ -118,19 +122,19 @@ describe('useAggregateRecordsQuery', () => {
);
});
it('should throw error for invalid aggregation operation', () => {
expect(() =>
renderHook(
() =>
useAggregateRecordsQuery({
objectNameSingular: 'company',
recordGqlFieldsAggregate: {
name: [AggregateOperations.SUM],
},
}),
{ wrapper: Wrapper },
),
).toThrow();
it('should early return for invalid aggregation operation', () => {
const { result } = renderHook(
() =>
useAggregateRecordsQuery({
objectNameSingular: 'company',
recordGqlFieldsAggregate: {
name: [AggregateOperations.SUM],
},
}),
{ wrapper: Wrapper },
);
expect(result.current.gqlFieldToFieldMap).toEqual({});
});
it('should handle multiple aggregations', () => {
@@ -28,8 +28,11 @@ export const useAggregateRecordsQuery = ({
const apolloCoreClient = useApolloCoreClient();
const availableAggregations = useMemo(
() => getAvailableAggregationsFromObjectFields(objectMetadataItem.fields),
[objectMetadataItem.fields],
() =>
getAvailableAggregationsFromObjectFields(
objectMetadataItem.readableFields,
),
[objectMetadataItem.readableFields],
);
const recordGqlFields: RecordGqlFields = {};
@@ -42,9 +45,7 @@ export const useAggregateRecordsQuery = ({
availableAggregations[fieldName]?.[aggregateOperation];
if (!isDefined(fieldToQuery)) {
throw new Error(
`Cannot query operation ${aggregateOperation} on field ${fieldName}`,
);
return;
}
gqlFieldToFieldMap[fieldToQuery] = [fieldName, aggregateOperation];
@@ -25,7 +25,7 @@ export const useAttachRelatedRecordFromRecord = ({
objectNameSingular: recordObjectNameSingular,
});
const fieldOnObject = objectMetadataItem.fields.find((field) => {
const fieldOnObject = objectMetadataItem.readableFields.find((field) => {
return field.name === fieldNameOnRecordObject;
});
@@ -21,7 +21,7 @@ export const useDetachRelatedRecordFromRecord = ({
objectNameSingular: recordObjectNameSingular,
});
const fieldOnObject = objectMetadataItem.fields.find((field) => {
const fieldOnObject = objectMetadataItem.readableFields.find((field) => {
return field.name === fieldNameOnRecordObject;
});
@@ -1,11 +1,14 @@
import { useRecoilValue } from 'recoil';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ObjectPermission } from '~/generated/graphql';
type useObjectPermissionsReturnType = {
objectPermissionsByObjectMetadataId: Record<string, ObjectPermission>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
};
export const useObjectPermissions = (): useObjectPermissionsReturnType => {
@@ -19,7 +22,10 @@ export const useObjectPermissions = (): useObjectPermissionsReturnType => {
}
const objectPermissionsByObjectMetadataId = objectPermissions?.reduce(
(acc: Record<string, ObjectPermission>, objectPermission) => {
(
acc: Record<string, ObjectPermissions & { objectMetadataId: string }>,
objectPermission,
) => {
acc[objectPermission.objectMetadataId] = objectPermission;
return acc;
},
@@ -1,9 +1,12 @@
import { getObjectPermissionsForObject } from '~/modules/object-metadata/utils/getObjectPermissionsForObject';
import { useMemo } from 'react';
import { ObjectPermissions } from 'twenty-shared/types';
import { useObjectPermissions } from './useObjectPermissions';
export const useObjectPermissionsForObject = (objectMetadataId: string) => {
export const useObjectPermissionsForObject = (
objectMetadataId: string,
): ObjectPermissions & { objectMetadataId: string } => {
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
return useMemo(() => {