Refactor read only object and fields (#13936)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
neo773
2025-08-19 21:40:35 +05:30
committed by GitHub
parent bb7cd1baa1
commit 3ef94f6e8c
114 changed files with 1274 additions and 483 deletions
@@ -53,6 +53,7 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
isUIReadOnly: false,
};
const Wrapper = getJestMetadataAndApolloMocksWrapper({
@@ -31,6 +31,7 @@ const objectMetadataItemWithPositionField: ObjectMetadataItem = {
icon: 'icon',
isActive: true,
isSystem: false,
isUIReadOnly: false,
isCustom: false,
isRemote: false,
isSearchable: false,
@@ -1,8 +1,8 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
export type UseFieldIsReadOnlyParams = {
fieldMetadataId: string;
@@ -42,10 +42,6 @@ export const useIsRecordFieldReadOnly = ({
return isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions,
fieldMetadataId,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
fieldMetadataItem,
});
};
@@ -1,6 +1,7 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { isRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordReadOnly';
import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly';
import { useIsRecordDeleted } from '@/object-record/record-field/ui/hooks/useIsRecordDeleted';
type UseIsRecordReadOnlyParams = {
@@ -12,6 +13,10 @@ export const useIsRecordReadOnly = ({
recordId,
objectMetadataId,
}: UseIsRecordReadOnlyParams) => {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectPermissions = getObjectPermissionsForObject(
@@ -24,5 +29,6 @@ export const useIsRecordReadOnly = ({
return isRecordReadOnly({
objectPermissions,
isRecordDeleted,
objectMetadataItem,
});
};
@@ -0,0 +1,67 @@
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
describe('isObjectMetadataReadOnly', () => {
it('should return false if object can be updated and is not UI read only and is not remote', () => {
const result = isObjectMetadataReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
restrictedFields: {},
},
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(false);
});
it('should return true if object cannot be updated and is not UI read only and is not remote', () => {
const result = isObjectMetadataReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
restrictedFields: {},
},
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if object metadata is UI read only', () => {
const result = isObjectMetadataReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
restrictedFields: {},
},
objectMetadataItem: {
isUIReadOnly: true,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if object metadata is remote', () => {
const result = isObjectMetadataReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
restrictedFields: {},
},
objectMetadataItem: {
isUIReadOnly: false,
isRemote: true,
},
});
expect(result).toBe(true);
});
});
@@ -1,4 +1,4 @@
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { FieldMetadataType } from '~/generated-metadata/graphql';
describe('isRecordFieldReadOnly', () => {
@@ -12,16 +12,18 @@ describe('isRecordFieldReadOnly', () => {
isRecordReadOnly: false,
objectPermissions: mockObjectPermissions,
fieldMetadataId: 'field-123',
objectNameSingular: 'person',
fieldName: 'firstName',
fieldType: FieldMetadataType.TEXT,
isCustom: false,
fieldMetadataType: FieldMetadataType.TEXT,
isUIReadOnly: false,
};
it('should return true when record is read-only', () => {
const result = isRecordFieldReadOnly({
...mockParams,
isRecordReadOnly: true,
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
},
});
expect(result).toBe(true);
@@ -34,6 +36,10 @@ describe('isRecordFieldReadOnly', () => {
...mockObjectPermissions,
canUpdateObjectRecords: false,
},
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
},
});
expect(result).toBe(true);
@@ -48,36 +54,22 @@ describe('isRecordFieldReadOnly', () => {
'field-123': { canUpdate: false },
},
},
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
},
});
expect(result).toBe(true);
});
it('should return true for system read-only fields like createdAt', () => {
it('should return true when field is marked as UI read-only', () => {
const result = isRecordFieldReadOnly({
...mockParams,
fieldName: 'createdAt',
fieldType: FieldMetadataType.DATE_TIME,
});
expect(result).toBe(true);
});
it('should return true for calendar event objects (system read-only)', () => {
const result = isRecordFieldReadOnly({
...mockParams,
objectNameSingular: 'calendarEvent',
});
expect(result).toBe(true);
});
it('should return true for workflow non-name fields (system read-only)', () => {
const result = isRecordFieldReadOnly({
...mockParams,
objectNameSingular: 'workflow',
fieldName: 'status',
isCustom: false,
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: true,
},
});
expect(result).toBe(true);
@@ -86,6 +78,10 @@ describe('isRecordFieldReadOnly', () => {
it('should return false when all conditions allow editing', () => {
const result = isRecordFieldReadOnly({
...mockParams,
fieldMetadataItem: {
id: 'field-123',
isUIReadOnly: false,
},
});
expect(result).toBe(false);
@@ -0,0 +1,83 @@
import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly';
describe('isRecordReadOnly', () => {
it('should return false if record is not deleted, has update permissions and object metadata is not read only', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: false,
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(false);
});
it('should return true if record is not deleted but lacks update permissions and object metadata is not read only', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
isRecordDeleted: false,
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if record is deleted even with update permissions and object metadata is not read only', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: true,
objectMetadataItem: {
isUIReadOnly: false,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if record is not deleted and has update permissions but object metadata is UI read only', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
isRecordDeleted: true,
objectMetadataItem: {
isUIReadOnly: true,
isRemote: false,
},
});
expect(result).toBe(true);
});
it('should return true if record is not deleted and has update permissions but object metadata is remote', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: false,
objectMetadataItem: {
isUIReadOnly: false,
isRemote: true,
},
});
expect(result).toBe(true);
});
});
@@ -0,0 +1,20 @@
import { type ObjectPermission } from '~/generated/graphql';
type IsFieldMetadataReadOnlyByPermissionParams = {
objectPermissions: ObjectPermission;
fieldMetadataId: string;
};
export const isFieldMetadataReadOnlyByPermissions = ({
objectPermissions,
fieldMetadataId,
}: IsFieldMetadataReadOnlyByPermissionParams) => {
if (objectPermissions.canUpdateObjectRecords === false) {
return true;
}
const fieldMetadataIsRestrictedForUpdate =
objectPermissions.restrictedFields?.[fieldMetadataId]?.canUpdate === false;
return fieldMetadataIsRestrictedForUpdate;
};
@@ -0,0 +1,33 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isFieldMetadataReadOnlyByPermissions } from '@/object-record/read-only/utils/internal/isFieldMetadataReadOnlyByPermissions';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { type ObjectPermission } from '~/generated/graphql';
type IsFieldMetadataReadOnlyParams = {
objectPermissions: ObjectPermission;
objectMetadataItem: Pick<ObjectMetadataItem, 'isUIReadOnly' | 'isRemote'>;
fieldMetadataItem: Pick<FieldMetadataItem, 'id' | 'isUIReadOnly'>;
};
export const isFieldMetadataReadOnly = ({
objectPermissions,
objectMetadataItem,
fieldMetadataItem,
}: IsFieldMetadataReadOnlyParams) => {
const objectMetadataReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
const fieldReadOnlyByPermissions = isFieldMetadataReadOnlyByPermissions({
objectPermissions,
fieldMetadataId: fieldMetadataItem.id,
});
return (
objectMetadataReadOnly ||
fieldMetadataItem.isUIReadOnly ||
fieldReadOnlyByPermissions
);
};
@@ -0,0 +1,18 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectPermission } from '~/generated/graphql';
type IsObjectMetadataReadOnlyParams = {
objectPermissions: ObjectPermission;
objectMetadataItem: Pick<ObjectMetadataItem, 'isUIReadOnly' | 'isRemote'>;
};
export const isObjectMetadataReadOnly = ({
objectPermissions,
objectMetadataItem,
}: IsObjectMetadataReadOnlyParams) => {
return (
!objectPermissions.canUpdateObjectRecords ||
objectMetadataItem.isUIReadOnly ||
objectMetadataItem.isRemote
);
};
@@ -0,0 +1,26 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { isFieldMetadataReadOnlyByPermissions } from '@/object-record/read-only/utils/internal/isFieldMetadataReadOnlyByPermissions';
import { type ObjectPermission } from '~/generated/graphql';
type IsRecordFieldReadOnlyParams = {
isRecordReadOnly: boolean;
fieldMetadataItem: Pick<FieldMetadataItem, 'id' | 'isUIReadOnly'>;
objectPermissions: ObjectPermission;
};
export const isRecordFieldReadOnly = ({
objectPermissions,
isRecordReadOnly,
fieldMetadataItem,
}: IsRecordFieldReadOnlyParams) => {
const fieldReadOnlyByPermissions = isFieldMetadataReadOnlyByPermissions({
objectPermissions,
fieldMetadataId: fieldMetadataItem.id,
});
return (
isRecordReadOnly ||
fieldMetadataItem.isUIReadOnly ||
fieldReadOnlyByPermissions
);
};
@@ -0,0 +1,23 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { type ObjectPermission } from '~/generated/graphql';
export type IsObjectReadOnlyParams = {
objectPermissions: ObjectPermission;
objectMetadataItem: Pick<ObjectMetadataItem, 'isUIReadOnly' | 'isRemote'>;
isRecordDeleted: boolean;
};
export const isRecordReadOnly = ({
objectPermissions,
isRecordDeleted,
objectMetadataItem,
}: IsObjectReadOnlyParams) => {
return (
isRecordDeleted ||
isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
})
);
};
@@ -1,3 +1,4 @@
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCardBodyContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBodyContainer';
import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
@@ -10,7 +11,6 @@ import {
type RecordUpdateHook,
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/ui/utils/getFieldButtonIcon';
@@ -45,12 +45,10 @@ export const RecordBoardCardBody = ({
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions,
fieldMetadataId: fieldDefinition.fieldMetadataId,
fieldName: fieldDefinition.metadata.fieldName,
fieldType: fieldDefinition.type,
isCustom: fieldDefinition.metadata.isCustom,
objectNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular ?? '',
fieldMetadataItem: {
id: fieldDefinition.fieldMetadataId,
isUIReadOnly: fieldDefinition.metadata.isUIReadOnly ?? false,
},
}),
}),
);
@@ -2,6 +2,7 @@ import styled from '@emotion/styled';
import { Draggable } from '@hello-pangea/dnd';
import { useContext } from 'react';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
import { RecordBoardCardHotkeysEffect } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHotkeysEffect';
@@ -9,7 +10,6 @@ import { RecordBoardCardMultiDragPreview } from '@/object-record/record-board/re
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
const StyledDraggableContainer = styled.div`
@@ -35,6 +35,7 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
isCustom: false,
isActive: true,
isSystem: false,
isUIReadOnly: false,
isRemote: false,
isSearchable: false,
labelIdentifierFieldMetadataId: '06b33746-5293-4d07-9f7f-ebf5ad396064',
@@ -3,6 +3,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldListCellEditModePortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellEditModePortal';
import { RecordFieldListCellHoveredPortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortal';
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
@@ -11,8 +13,6 @@ import { RecordDetailRelationSection } from '@/object-record/record-field-list/r
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
@@ -123,11 +123,10 @@ export const RecordFieldList = ({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
},
}),
}}
>
@@ -172,11 +171,10 @@ export const RecordFieldList = ({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
},
}),
onMouseEnter: () =>
handleMouseEnter(
@@ -233,11 +231,10 @@ export const RecordFieldList = ({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
},
}),
}}
>
@@ -1,10 +1,10 @@
import { useContext } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { RecordDetailRelationSectionDropdownToMany } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany';
import { RecordDetailRelationSectionDropdownToOne } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { RelationType } from '~/generated-metadata/graphql';
@@ -1,12 +1,12 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import {
FieldContext,
type RecordUpdateHook,
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { type ReactNode } from 'react';
export const FieldContextProvider = ({
@@ -1,25 +0,0 @@
import { isObjectReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isObjectReadOnly';
describe('isObjectReadOnly', () => {
it('should return true if object is not read only', () => {
const result = isObjectReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
});
expect(result).toBe(false);
});
it('should return false if object is read only', () => {
const result = isObjectReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
});
expect(result).toBe(true);
});
});
@@ -1,51 +0,0 @@
import { isRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordReadOnly';
describe('isRecordReadOnly', () => {
it('should return false if record is not deleted and has update permissions', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: false,
});
expect(result).toBe(false);
});
it('should return true if record is not deleted but lacks update permissions', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
isRecordDeleted: false,
});
expect(result).toBe(true);
});
it('should return true if record is deleted even with update permissions', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: true,
objectMetadataId: '123',
},
isRecordDeleted: true,
});
expect(result).toBe(true);
});
it('should return true if record is deleted and lacks update permissions', () => {
const result = isRecordReadOnly({
objectPermissions: {
canUpdateObjectRecords: false,
objectMetadataId: '123',
},
isRecordDeleted: true,
});
expect(result).toBe(true);
});
});
@@ -1,21 +0,0 @@
import { isObjectReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isObjectReadOnly';
import { type ObjectPermission } from '~/generated/graphql';
export type IsFieldReadOnlyByPermissionParams = {
objectPermissions: ObjectPermission;
fieldMetadataId: string;
};
export const isFieldReadOnlyByPermissions = ({
objectPermissions,
fieldMetadataId,
}: IsFieldReadOnlyByPermissionParams) => {
if (isObjectReadOnly({ objectPermissions }) === true) {
return true;
}
const fieldMetadataIsRestrictedForUpdate =
objectPermissions.restrictedFields[fieldMetadataId]?.canUpdate === false;
return fieldMetadataIsRestrictedForUpdate;
};
@@ -1,80 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { isWorkflowRunJsonField } from '@/object-record/record-field/ui/meta-types/utils/isWorkflowRunJsonField';
import { isFieldActor } from '@/object-record/record-field/ui/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export type IsFieldReadOnlyBySystemParams = {
objectNameSingular: string;
fieldName?: string;
fieldType?: FieldMetadataType;
isCustom?: boolean;
};
export const isFieldReadOnlyBySystem = ({
objectNameSingular,
fieldName,
fieldType,
isCustom,
}: IsFieldReadOnlyBySystemParams) => {
if (
isWorkflowRunJsonField({
objectMetadataNameSingular: objectNameSingular,
fieldName,
})
) {
return false;
}
if (isWorkflowSubObjectMetadata(objectNameSingular) && !isCustom) {
return true;
}
if (objectNameSingular === CoreObjectNameSingular.CalendarEvent) {
return true;
}
if (
objectNameSingular === CoreObjectNameSingular.Workflow &&
fieldName !== 'name' &&
!isCustom
) {
return true;
}
if (
objectNameSingular !== CoreObjectNameSingular.Note &&
fieldName === 'noteTargets'
) {
return true;
}
if (
objectNameSingular !== CoreObjectNameSingular.Task &&
fieldName === 'taskTargets'
) {
return true;
}
const isFieldDateOrDateTime =
fieldType === FieldMetadataType.DATE ||
fieldType === FieldMetadataType.DATE_TIME;
const isFieldCreatedAtOrUpdatedAt =
fieldName === 'createdAt' || fieldName === 'updatedAt';
if (isFieldDateOrDateTime && isFieldCreatedAtOrUpdatedAt) {
return true;
}
if (
isDefined(fieldType) &&
(isFieldActor({ type: fieldType }) || isFieldRichText({ type: fieldType }))
) {
return true;
}
return false;
};
@@ -1,11 +0,0 @@
import { type ObjectPermission } from '~/generated/graphql';
type IsObjectReadOnlyParams = {
objectPermissions: ObjectPermission;
};
export const isObjectReadOnly = ({
objectPermissions,
}: IsObjectReadOnlyParams) => {
return !objectPermissions.canUpdateObjectRecords;
};
@@ -1,39 +0,0 @@
import {
isFieldReadOnlyByPermissions,
type IsFieldReadOnlyByPermissionParams,
} from '@/object-record/record-field/ui/hooks/read-only/utils/internal/isFieldReadOnlyByPermissions';
import {
isFieldReadOnlyBySystem,
type IsFieldReadOnlyBySystemParams,
} from '@/object-record/record-field/ui/hooks/read-only/utils/internal/isFieldReadOnlyBySystem';
type IsRecordFieldReadOnlyParams = {
isRecordReadOnly: boolean;
} & IsFieldReadOnlyByPermissionParams &
IsFieldReadOnlyBySystemParams;
export const isRecordFieldReadOnly = ({
isRecordReadOnly,
objectPermissions,
fieldMetadataId,
objectNameSingular,
fieldName,
fieldType,
isCustom,
}: IsRecordFieldReadOnlyParams) => {
const fieldReadOnlyByPermissions = isFieldReadOnlyByPermissions({
objectPermissions,
fieldMetadataId,
});
const fieldReadOnlyBySystem = isFieldReadOnlyBySystem({
objectNameSingular,
fieldName,
fieldType,
isCustom,
});
return (
isRecordReadOnly || fieldReadOnlyByPermissions || fieldReadOnlyBySystem
);
};
@@ -1,13 +0,0 @@
import { type ObjectPermission } from '~/generated/graphql';
type IsObjectReadOnlyParams = {
objectPermissions: ObjectPermission;
isRecordDeleted: boolean;
};
export const isRecordReadOnly = ({
objectPermissions,
isRecordDeleted,
}: IsObjectReadOnlyParams) => {
return isRecordDeleted || !objectPermissions.canUpdateObjectRecords;
};
@@ -29,7 +29,6 @@ import { recordStoreFamilySelector } from '@/object-record/record-store/states/s
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { isWorkflowRunJsonField } from '@/object-record/record-field/ui/meta-types/utils/isWorkflowRunJsonField';
import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray';
import { isFieldArrayValue } from '@/object-record/record-field/ui/types/guards/isFieldArrayValue';
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
@@ -143,13 +142,10 @@ export const usePersistField = ({
const fieldIsArray =
isFieldArray(fieldDefinition) && isFieldArrayValue(valueToPersist);
const isUnpersistableRawJsonField = isWorkflowRunJsonField({
objectMetadataNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular,
fieldName: fieldDefinition.metadata.fieldName,
});
const fieldIsUIReadOnly =
fieldDefinition.metadata.isUIReadOnly ?? false;
if (fieldIsRawJson && isUnpersistableRawJsonField) {
if (fieldIsRawJson && fieldIsUIReadOnly) {
return;
}
@@ -1,7 +1,6 @@
import styled from '@emotion/styled';
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
import { isWorkflowRunJsonField } from '@/object-record/record-field/ui/meta-types/utils/isWorkflowRunJsonField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
@@ -142,11 +141,7 @@ export const RawJsonFieldInput = () => {
dependencies: [handleShiftTab, draftValue],
});
const showEditingButton = !isWorkflowRunJsonField({
objectMetadataNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular,
fieldName: fieldDefinition.metadata.fieldName,
});
const showEditingButton = !fieldDefinition.metadata.isUIReadOnly;
const handleStartEditing = () => {
setIsEditing(true);
@@ -1,15 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
// FIXME: This is temporary. We'll soon introduce a new display mode for all fields and we'll have to remove this code.
export const isWorkflowRunJsonField = ({
objectMetadataNameSingular,
fieldName,
}: {
fieldName: string | undefined;
objectMetadataNameSingular: string | undefined;
}) => {
return (
objectMetadataNameSingular === CoreObjectNameSingular.WorkflowRun &&
fieldName === 'state'
);
};
@@ -15,4 +15,5 @@ export type FieldDefinition<T extends FieldMetadata> = {
infoTooltipContent?: string;
defaultValue?: any;
editButtonIcon?: IconComponent;
isUIReadOnly?: boolean;
};
@@ -15,6 +15,7 @@ type BaseFieldMetadata = {
fieldName: string;
objectMetadataNameSingular?: string;
isCustom?: boolean;
isUIReadOnly?: boolean;
};
export type FieldUuidMetadata = BaseFieldMetadata & {
@@ -132,6 +132,7 @@ const mockObjectMetadataItem: ObjectMetadataItem = {
isRemote: false,
isSearchable: true,
isSystem: false,
isUIReadOnly: false,
labelIdentifierFieldMetadataId: 'mock-id',
labelPlural: 'Tests',
labelSingular: 'Test',
@@ -224,6 +224,7 @@ describe('useRecordData', () => {
fieldName: 'updatedAt',
isCustom: false,
isNullable: false,
isUIReadOnly: false,
objectMetadataNameSingular: 'person',
options: null,
placeHolder: 'Last update',
@@ -2,13 +2,13 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import {
FieldContext,
type RecordUpdateHook,
type RecordUpdateHookParams,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { FieldFocusContextProvider } from '@/object-record/record-field/ui/contexts/FieldFocusContextProvider';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCellAnchoredPortalContext } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortalContext';
import { RecordInlineCellCloseOnCommandMenuOpeningEffect } from '@/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect';
@@ -1,8 +1,8 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
@@ -2,8 +2,8 @@ import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandard
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordFieldReadOnly';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
@@ -1,4 +1,5 @@
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { type IconComponent } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
@@ -35,7 +36,13 @@ export const RecordTableEmptyStateDisplay = (
props: RecordTableEmptyStateDisplayProps,
) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const isReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
return (
<AnimatedPlaceholderEmptyContainer>
@@ -1,7 +1,7 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -63,11 +63,10 @@ export const RecordTableCellFieldContextGeneric = ({
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly: isRecordReadOnly ?? false,
objectPermissions,
fieldMetadataId: columnDefinition.fieldMetadataId,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: columnDefinition.metadata.fieldName,
fieldType: columnDefinition.type,
isCustom: objectMetadataItem.isCustom,
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
isUIReadOnly: columnDefinition.metadata.isUIReadOnly ?? false,
},
}),
isForbidden: !hasObjectReadPermissions,
}}
@@ -1,6 +1,6 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isRecordFieldReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isRecordFieldReadOnly';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
@@ -70,13 +70,12 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
isLabelIdentifierCompact,
displayedMaxRows: 1,
isRecordFieldReadOnly: isRecordFieldReadOnly({
objectPermissions,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: columnDefinition.metadata.fieldName,
fieldType: columnDefinition.type,
isCustom: objectMetadataItem.isCustom,
fieldMetadataId: columnDefinition.fieldMetadataId,
isRecordReadOnly: isRecordReadOnly ?? false,
objectPermissions,
fieldMetadataItem: {
id: columnDefinition.fieldMetadataId,
isUIReadOnly: columnDefinition.metadata.isUIReadOnly ?? false,
},
}),
maxWidth: columnDefinition.size,
onRecordChipClick: () => {
@@ -1,6 +1,6 @@
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
@@ -2,8 +2,8 @@ import styled from '@emotion/styled';
import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly';
import { useUpdateRecordField } from '@/object-record/record-field/hooks/useUpdateRecordField';
import { isObjectReadOnly } from '@/object-record/record-field/ui/hooks/read-only/utils/isObjectReadOnly';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
@@ -241,8 +241,9 @@ export const RecordTableHeaderCell = ({
createNewIndexRecord();
};
const isReadOnly = isObjectReadOnly({
const isReadOnly = isObjectMetadataReadOnly({
objectPermissions,
objectMetadataItem,
});
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
@@ -1,5 +1,5 @@
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useIsRecordReadOnly } from '@/object-record/record-field/ui/hooks/read-only/useIsRecordReadOnly';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
@@ -22,6 +22,7 @@ describe('generateAggregateQuery', () => {
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
isUIReadOnly: false,
};
const mockRecordGqlFields = {
@@ -63,6 +64,7 @@ describe('generateAggregateQuery', () => {
isLabelSyncedWithName: true,
isRemote: false,
isSystem: false,
isUIReadOnly: false,
};
const mockRecordGqlFields = {