Refactor isReadOnly + introduce restrictedFields (#13533)

Closes https://github.com/twentyhq/core-team-issues/issues/1276 

We now need to take fields permissions into account when determining if
a cell is editable or not.

a record field (= a cell) is read only if one of the condition is met:
- the object of the record is read-only (can be determined at table
level) by permission setting
- the field of the object is read-only (can be determined at column
level) - either by permission settings or by system (some workflows
fields for intance)
- the record is deleted (can be determined at row level) 

we reorganized the code to avoid re-computing known information as much
as possible.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2025-08-04 18:42:27 +02:00
committed by GitHub
parent 42045667a4
commit 932910e71e
74 changed files with 576 additions and 346 deletions
@@ -7,6 +7,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/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';
@@ -94,6 +95,11 @@ export const CalendarEventDetails = ({
const { calendarEventParticipants } = calendarEvent;
const isRecordReadOnly = useIsRecordReadOnly({
recordId: calendarEvent.id,
objectMetadataId: objectMetadataItem.id,
});
const Fields = fieldsToDisplay.map((fieldName) => (
<StyledPropertyBox key={fieldName}>
<FieldContext.Provider
@@ -108,7 +114,7 @@ export const CalendarEventDetails = ({
}),
useUpdateRecord: () => [() => undefined, { loading: false }],
maxWidth: 300,
isReadOnly: false,
isRecordFieldReadOnly: isRecordReadOnly,
}}
>
<RecordFieldComponentInstanceContext.Provider
@@ -8,7 +8,6 @@ import { canCreateActivityState } from '@/activities/states/canCreateActivitySta
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { Key } from 'ts-key-enum';
@@ -29,7 +28,7 @@ import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly';
import { isInlineCellInEditModeFamilyState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeFamilyState';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
@@ -67,16 +66,9 @@ export const ActivityRichTextEditor = ({
objectNameSingular: activityObjectNameSingular,
});
const isRecordReadOnly = useIsRecordReadOnly({
recordId: activityId,
objectMetadataId: objectMetadataItemActivity.id,
});
const isReadOnly = isFieldValueReadOnly({
objectNameSingular: activityObjectNameSingular,
isRecordReadOnly,
isCustom: objectMetadataItemActivity.isCustom,
});
const bodyV2FieldMetadataItemId = objectMetadataItemActivity.fields.find(
(field) => field.name === 'bodyV2',
)?.id;
const { deleteManyRecords: deleteAttachments } = useDeleteManyRecords({
objectNameSingular: CoreObjectNameSingular.Attachment,
@@ -106,8 +98,14 @@ export const ActivityRichTextEditor = ({
activityObjectNameSingular: activityObjectNameSingular,
});
const isRecordFieldReadOnly = useIsRecordFieldReadOnly({
recordId: activityId,
objectMetadataId: objectMetadataItemActivity.id,
fieldMetadataId: bodyV2FieldMetadataItemId,
});
const persistBodyDebounced = useDebouncedCallback((blocknote: string) => {
if (isReadOnly) return;
if (isRecordFieldReadOnly === true) return;
const input = {
bodyV2: {
@@ -430,7 +428,7 @@ export const ActivityRichTextEditor = ({
onBlur={handlerBlockEditorBlur}
onChange={handleEditorChange}
editor={editor}
readonly={isReadOnly}
readonly={isRecordFieldReadOnly}
/>
</>
);
@@ -37,7 +37,8 @@ export const ActivityTargetsInlineCell = ({
const { closeInlineCell } = useInlineCell(componentInstanceId);
const { fieldDefinition, isReadOnly } = useContext(FieldContext);
const { fieldDefinition, isRecordFieldReadOnly: isReadOnly } =
useContext(FieldContext);
const { openActivityTargetCellEditMode } =
useOpenActivityTargetCellEditMode();
@@ -49,7 +49,7 @@ export const EventFieldDiffValue = ({
},
defaultValue: fieldMetadataItem.defaultValue,
},
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<FieldDisplay />
@@ -3,6 +3,7 @@ import { createContext } from 'react';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ObjectPermission } from '~/generated/graphql';
type RecordBoardContextProps = {
objectMetadataItem: ObjectMetadataItem;
@@ -17,6 +18,7 @@ type RecordBoardContextProps = {
}) => void;
deleteOneRecord: (idToDelete: string) => Promise<unknown>;
recordBoardId: string;
objectPermissions: ObjectPermission;
};
export const RecordBoardContext = createContext<RecordBoardContextProps>(
@@ -9,10 +9,10 @@ import {
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { useContext } from 'react';
@@ -24,7 +24,7 @@ export const RecordBoardCardBody = ({
}) => {
const { recordId, isRecordReadOnly } = useContext(RecordBoardCardContext);
const { updateOneRecord } = useContext(RecordBoardContext);
const { updateOneRecord, objectPermissions } = useContext(RecordBoardContext);
const useUpdateOneRecordHook: RecordUpdateHook = () => {
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
@@ -37,23 +37,32 @@ export const RecordBoardCardBody = ({
return [updateEntity, { loading: false }];
};
const fieldDefinitionsWithReadOnly = fieldDefinitions.map(
(fieldDefinition) => ({
...fieldDefinition,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions,
fieldMetadataId: fieldDefinition.fieldMetadataId,
fieldName: fieldDefinition.metadata.fieldName,
fieldType: fieldDefinition.type,
isCustom: fieldDefinition.metadata.isCustom,
objectNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular ?? '',
}),
}),
);
return (
<RecordBoardCardBodyContainer>
{fieldDefinitions.map((fieldDefinition) => (
{fieldDefinitionsWithReadOnly.map((fieldDefinition) => (
<StopPropagationContainer key={fieldDefinition.fieldMetadataId}>
<FieldContext.Provider
value={{
recordId,
maxWidth: 156,
isLabelIdentifier: false,
isReadOnly: isFieldValueReadOnly({
objectNameSingular:
fieldDefinition.metadata.objectMetadataNameSingular,
fieldName: fieldDefinition.metadata.fieldName,
fieldType: fieldDefinition.type,
isRecordReadOnly,
isCustom: fieldDefinition.metadata.isCustom,
}),
isRecordFieldReadOnly: fieldDefinition.isRecordFieldReadOnly,
fieldDefinition: {
disableTooltip: false,
fieldMetadataId: fieldDefinition.fieldMetadataId,
@@ -9,7 +9,7 @@ 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/hooks/useIsRecordReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
const StyledDraggableContainer = styled.div`
@@ -1,12 +1,12 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import {
FieldContext,
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly';
import { ReactNode } from 'react';
export const FieldContextProvider = ({
@@ -34,10 +34,6 @@ export const FieldContextProvider = ({
objectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const fieldMetadataItem = objectMetadataItem?.fields.find(
(field) => field.name === fieldMetadataName,
);
@@ -57,12 +53,16 @@ export const FieldContextProvider = ({
return [updateEntity, { loading: false }];
};
const isRecordFieldReadOnly = useIsRecordFieldReadOnly({
recordId: objectRecordId,
fieldMetadataId: fieldMetadataItem?.id,
objectMetadataId: objectMetadataItem.id,
});
if (!fieldMetadataItem) {
return null;
}
const isObjectReadOnly = !objectPermissions.canUpdateObjectRecords;
return (
<FieldContext.Provider
key={objectRecordId + fieldMetadataItem.id}
@@ -80,7 +80,7 @@ export const FieldContextProvider = ({
customUseUpdateOneObjectHook ?? useUpdateOneObjectMutation,
clearable,
overridenIsFieldEmpty,
isReadOnly: isObjectReadOnly,
isRecordFieldReadOnly,
}}
>
{children}
@@ -33,7 +33,7 @@ export type GenericFieldContextType = {
overridenIsFieldEmpty?: boolean;
displayedMaxRows?: number;
isDisplayModeFixHeight?: boolean;
isReadOnly: boolean;
isRecordFieldReadOnly: boolean;
disableChipClick?: boolean;
onRecordChipClick?: (event: MouseEvent) => void;
onOpenEditMode?: () => void;
@@ -22,7 +22,7 @@ const getWrapper =
fieldDefinition,
recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
@@ -15,7 +15,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
fieldDefinition: phonesFieldDefinition,
recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
@@ -21,7 +21,7 @@ const getWrapper =
fieldDefinition,
recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
@@ -1,29 +0,0 @@
import { renderHook } from '@testing-library/react';
import { phonesFieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { useIsFieldValueReadOnly } from '../useIsFieldValueReadOnly';
describe('useIsFieldValueReadOnly', () => {
it('should return true if the field is read only', () => {
const { result } = renderHook(() =>
useIsFieldValueReadOnly({
fieldDefinition: phonesFieldDefinition,
isRecordReadOnly: false,
}),
);
expect(result.current).toBe(false);
});
it('should return true if the record is read only', () => {
const { result } = renderHook(() =>
useIsFieldValueReadOnly({
fieldDefinition: phonesFieldDefinition,
isRecordReadOnly: true,
}),
);
expect(result.current).toBe(true);
});
});
@@ -104,7 +104,7 @@ const getWrapper =
recordId,
isLabelIdentifier: false,
useUpdateRecord: useUpdateOneRecordMutation,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
{children}
@@ -346,7 +346,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => {
recordId,
isLabelIdentifier: false,
useUpdateRecord: useUpdateOneRecordMutation,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
{children}
@@ -0,0 +1,53 @@
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/hooks/read-only/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
export type UseFieldIsReadOnlyParams = {
fieldMetadataId: string;
objectMetadataId: string;
recordId: string;
};
export const useIsRecordFieldReadOnly = ({
fieldMetadataId,
objectMetadataId,
recordId,
}: UseFieldIsReadOnlyParams) => {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const fieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === fieldMetadataId,
);
if (!fieldMetadataItem) {
throw new Error(
`Could not load read only property for field ${fieldMetadataId}`,
);
}
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataId,
);
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
objectMetadataId,
});
return isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions,
fieldMetadataId,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
});
};
@@ -0,0 +1,28 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { isRecordReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordReadOnly';
import { useIsRecordDeleted } from '@/object-record/record-field/hooks/useIsRecordDeleted';
type UseIsRecordReadOnlyParams = {
recordId: string;
objectMetadataId: string;
};
export const useIsRecordReadOnly = ({
recordId,
objectMetadataId,
}: UseIsRecordReadOnlyParams) => {
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataId,
);
const isRecordDeleted = useIsRecordDeleted({ recordId });
return isRecordReadOnly({
objectPermissions,
isRecordDeleted,
});
};
@@ -0,0 +1,25 @@
import { isObjectReadOnly } from '@/object-record/record-field/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);
});
});
@@ -0,0 +1,93 @@
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
import { FieldMetadataType } from '~/generated-metadata/graphql';
describe('isRecordFieldReadOnly', () => {
const mockObjectPermissions = {
canUpdateObjectRecords: true,
objectMetadataId: '123',
restrictedFields: {},
};
const mockParams = {
isRecordReadOnly: false,
objectPermissions: mockObjectPermissions,
fieldMetadataId: 'field-123',
objectNameSingular: 'person',
fieldName: 'firstName',
fieldType: FieldMetadataType.TEXT,
isCustom: false,
};
it('should return true when record is read-only', () => {
const result = isRecordFieldReadOnly({
...mockParams,
isRecordReadOnly: true,
});
expect(result).toBe(true);
});
it('should return true when object lacks update permissions', () => {
const result = isRecordFieldReadOnly({
...mockParams,
objectPermissions: {
...mockObjectPermissions,
canUpdateObjectRecords: false,
},
});
expect(result).toBe(true);
});
it('should return true when field is restricted by permissions', () => {
const result = isRecordFieldReadOnly({
...mockParams,
objectPermissions: {
...mockObjectPermissions,
restrictedFields: {
'field-123': { canUpdate: false },
},
},
});
expect(result).toBe(true);
});
it('should return true for system read-only fields like createdAt', () => {
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,
});
expect(result).toBe(true);
});
it('should return false when all conditions allow editing', () => {
const result = isRecordFieldReadOnly({
...mockParams,
});
expect(result).toBe(false);
});
});
@@ -0,0 +1,51 @@
import { isRecordReadOnly } from '@/object-record/record-field/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);
});
});
@@ -0,0 +1,21 @@
import { isObjectReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isObjectReadOnly';
import { 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;
};
@@ -7,25 +7,19 @@ import { isFieldRichText } from '@/object-record/record-field/types/guards/isFie
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type isFieldValueReadOnlyParams = {
objectNameSingular?: string;
export type IsFieldReadOnlyBySystemParams = {
objectNameSingular: string;
fieldName?: string;
fieldType?: FieldMetadataType;
isRecordReadOnly?: boolean;
isCustom?: boolean;
};
export const isFieldValueReadOnly = ({
export const isFieldReadOnlyBySystem = ({
objectNameSingular,
fieldName,
fieldType,
isCustom,
isRecordReadOnly = false,
}: isFieldValueReadOnlyParams) => {
if (isRecordReadOnly) {
return true;
}
}: IsFieldReadOnlyBySystemParams) => {
if (
isWorkflowRunJsonField({
objectMetadataNameSingular: objectNameSingular,
@@ -0,0 +1,11 @@
import { ObjectPermission } from '~/generated/graphql';
type IsObjectReadOnlyParams = {
objectPermissions: ObjectPermission;
};
export const isObjectReadOnly = ({
objectPermissions,
}: IsObjectReadOnlyParams) => {
return !objectPermissions.canUpdateObjectRecords;
};
@@ -0,0 +1,39 @@
import {
IsFieldReadOnlyByPermissionParams,
isFieldReadOnlyByPermissions,
} from '@/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyByPermissions';
import {
IsFieldReadOnlyBySystemParams,
isFieldReadOnlyBySystem,
} from '@/object-record/record-field/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
);
};
@@ -0,0 +1,13 @@
import { ObjectPermission } from '~/generated/graphql';
type IsObjectReadOnlyParams = {
objectPermissions: ObjectPermission;
isRecordDeleted: boolean;
};
export const isRecordReadOnly = ({
objectPermissions,
isRecordDeleted,
}: IsObjectReadOnlyParams) => {
return isRecordDeleted || !objectPermissions.canUpdateObjectRecords;
};
@@ -1,23 +0,0 @@
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
type UseIsFieldValueReadOnlyParams = {
isRecordReadOnly: boolean;
fieldDefinition: FieldDefinition<FieldMetadata>;
};
export const useIsFieldValueReadOnly = ({
fieldDefinition,
isRecordReadOnly,
}: UseIsFieldValueReadOnlyParams) => {
const { metadata, type } = fieldDefinition;
return isFieldValueReadOnly({
objectNameSingular: metadata.objectMetadataNameSingular,
fieldName: metadata.fieldName,
fieldType: type,
isRecordReadOnly,
isCustom: metadata.isCustom,
});
};
@@ -1,4 +1,3 @@
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilValue } from 'recoil';
@@ -6,13 +5,11 @@ import { isDefined } from 'twenty-shared/utils';
type UseIsRecordReadOnlyParams = {
recordId: string;
objectMetadataId: string;
};
export const useIsRecordReadOnly = ({
export const useIsRecordDeleted = ({
recordId,
objectMetadataId,
}: UseIsRecordReadOnlyParams) => {
}: UseIsRecordReadOnlyParams): boolean => {
const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
recordStoreFamilySelector({
recordId,
@@ -20,9 +17,5 @@ export const useIsRecordReadOnly = ({
}),
);
const objectPermissions = useObjectPermissionsForObject(objectMetadataId);
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
return !hasObjectUpdatePermissions || isDefined(recordDeletedAt);
return isDefined(recordDeletedAt);
};
@@ -47,7 +47,7 @@ const meta: Meta = {
fieldDefinition: {
...relationFromManyFieldDisplayMock.fieldDefinition,
} as unknown as FieldDefinition<FieldMetadata>,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RelationFieldValueSetterEffect />
@@ -84,7 +84,7 @@ const AddressInputWithContext = ({
},
recordId: recordId ?? '123',
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<AddressValueSetterEffect value={value} />
@@ -99,7 +99,7 @@ const ArrayInputWithContext = ({
},
recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
useUpdateRecord: () => [updateRecord, { loading: false }],
}}
>
@@ -66,7 +66,7 @@ const BooleanFieldInputWithContext = ({
},
recordId: recordId ?? '123',
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<BooleanFieldValueSetterEffect
@@ -101,7 +101,7 @@ const DateFieldInputWithContext = ({
},
recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<StorybookFieldInputDropdownFocusIdSetterEffect />
@@ -98,7 +98,7 @@ const EmailInputWithContext = ({
},
recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
useUpdateRecord: () => [updateRecord, { loading: false }],
}}
>
@@ -107,7 +107,7 @@ const LinksInputWithContext = ({
},
recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
useUpdateRecord: () => [updateRecord, { loading: false }],
}}
>
@@ -88,7 +88,7 @@ const NumberFieldInputWithContext = ({
},
recordId: '123',
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
{isReady && <StorybookFieldInputDropdownFocusIdSetterEffect />}
@@ -97,7 +97,7 @@ const PhoneInputWithContext = ({
},
recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
useUpdateRecord: () => [updateRecord, { loading: false }],
}}
>
@@ -76,7 +76,7 @@ const RatingFieldInputWithContext = ({
},
recordId: recordId ?? '123',
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RatingFieldValueSetterEffect value={value} />
@@ -100,7 +100,7 @@ const RelationManyFieldInputWithContext = () => {
fieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RelationWorkspaceSetterEffect />
@@ -94,7 +94,7 @@ const RelationToOneFieldInputWithContext = ({
},
recordId: recordId,
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RecordFieldComponentInstanceContext.Provider
@@ -75,7 +75,7 @@ const RichTextFieldInputWithContext = ({
},
},
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RichTextFieldInput
@@ -82,7 +82,7 @@ const TextFieldInputWithContext = ({
},
},
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<TextFieldValueSetterEffect value={value} />
@@ -1,112 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { FieldMetadataType } from '~/generated-metadata/graphql';
describe('isFieldValueReadOnly', () => {
it('should return true if record is read only', () => {
const result = isFieldValueReadOnly({
isRecordReadOnly: true,
});
expect(result).toBe(true);
});
it('should return true if object is a workflow sub object', () => {
const result = isFieldValueReadOnly({
objectNameSingular: 'workflowRun',
});
expect(result).toBe(true);
});
it('should return true if object is a calendar event', () => {
const result = isFieldValueReadOnly({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
});
expect(result).toBe(true);
});
it('should return true if object is a workflow and field is not name', () => {
const result = isFieldValueReadOnly({
objectNameSingular: CoreObjectNameSingular.Workflow,
fieldName: 'description',
});
expect(result).toBe(true);
});
it('should return false if object is a workflow and field is name', () => {
const result = isFieldValueReadOnly({
objectNameSingular: CoreObjectNameSingular.Workflow,
fieldName: 'name',
});
expect(result).toBe(false);
});
it('should return false if object is a workflow object and field is custom', () => {
const result = isFieldValueReadOnly({
objectNameSingular: CoreObjectNameSingular.Workflow,
fieldName: 'test',
isCustom: true,
});
expect(result).toBe(false);
});
it('should return false if object is a workflow sub object and field is custom', () => {
const result = isFieldValueReadOnly({
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
fieldName: 'test',
isCustom: true,
});
expect(result).toBe(false);
});
describe('when checking field types', () => {
it('should return true if fieldType is RICH_TEXT', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.RICH_TEXT,
});
expect(result).toBe(true);
});
it('should return false if fieldType is RICH_TEXT_V2', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.RICH_TEXT_V2,
});
expect(result).toBe(false);
});
it('should return true if fieldType is ACTOR', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.ACTOR,
});
expect(result).toBe(true);
});
it('should return false for other field types', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.TEXT,
});
expect(result).toBe(false);
});
});
it('should return false for standard editable fields', () => {
const result = isFieldValueReadOnly({
objectNameSingular: 'company',
fieldName: 'name',
fieldType: FieldMetadataType.TEXT,
isRecordReadOnly: false,
});
expect(result).toBe(false);
});
});
@@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordBoard } from '@/object-record/record-board/components/RecordBoard';
import { RecordBoardBodyEscapeHotkeyEffect } from '@/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect';
@@ -27,6 +28,10 @@ export const RecordIndexBoardContainer = ({
objectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const recordIndexKanbanFieldMetadataId = useRecoilValue(
recordIndexKanbanFieldMetadataIdState,
);
@@ -60,6 +65,7 @@ export const RecordIndexBoardContainer = ({
updateOneRecord,
deleteOneRecord,
recordBoardId,
objectPermissions,
}}
>
<RecordBoard />
@@ -43,7 +43,7 @@ export const RecordInlineCell = ({
isDisplayModeFixHeight,
onOpenEditMode: onOpenEditModeFromContext,
onCloseEditMode: onCloseEditModeFromContext,
isReadOnly,
isRecordFieldReadOnly: isReadOnly,
} = useContext(FieldContext);
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
@@ -8,9 +8,9 @@ import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/uti
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
@@ -135,12 +135,17 @@ export const FieldsCard = ({
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isReadOnly: isFieldValueReadOnly({
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
objectPermissionsByObjectMetadataId[
objectMetadataItem.id
],
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
isRecordReadOnly,
}),
}}
>
@@ -180,12 +185,17 @@ export const FieldsCard = ({
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isReadOnly: isFieldValueReadOnly({
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
objectPermissionsByObjectMetadataId[
objectMetadataItem.id
],
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
isRecordReadOnly,
}),
}}
>
@@ -227,7 +237,16 @@ export const FieldsCard = ({
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isReadOnly: isRecordReadOnly,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
objectPermissionsByObjectMetadataId[objectMetadataItem.id],
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: fieldMetadataItem.isCustom ?? false,
}),
}}
>
<RecordDetailRelationSection
@@ -2,7 +2,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/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';
@@ -67,9 +67,10 @@ export const ObjectRecordShowPageBreadcrumb = ({
objectRecordId,
});
const isRecordReadOnly = useIsRecordReadOnly({
const isLabelIdentifierReadOnly = useIsRecordFieldReadOnly({
recordId: objectRecordId,
objectMetadataId: objectMetadataItem.id,
fieldMetadataId: labelIdentifierFieldMetadataItem?.id,
});
const { navigateToIndexView, rankInView, totalCount } =
@@ -118,7 +119,7 @@ export const ObjectRecordShowPageBreadcrumb = ({
useUpdateRecord: useUpdateOneObjectRecordMutation,
isCentered: false,
isDisplayModeFixHeight: true,
isReadOnly: isRecordReadOnly,
isRecordFieldReadOnly: isLabelIdentifierReadOnly,
}}
>
<RecordTitleCell
@@ -2,7 +2,7 @@ import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandard
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/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';
@@ -60,8 +60,9 @@ export const SummaryCard = ({
objectNameSingular,
});
const isRecordReadOnly = useIsRecordReadOnly({
const isTitleReadOnly = useIsRecordFieldReadOnly({
recordId: objectRecordId,
fieldMetadataId: labelIdentifierFieldMetadataItem?.id,
objectMetadataId: objectMetadataItem.id,
});
@@ -98,7 +99,7 @@ export const SummaryCard = ({
useUpdateRecord: useUpdateOneObjectRecordMutation,
isCentered: !isMobile,
isDisplayModeFixHeight: true,
isReadOnly: isRecordReadOnly,
isRecordFieldReadOnly: isTitleReadOnly,
}}
>
<RecordTitleCell
@@ -17,8 +17,8 @@ import {
RecordUpdateHook,
RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
@@ -107,7 +107,11 @@ export const RecordDetailRelationRecordsListItem = ({
onClick,
relationRecord,
}: RecordDetailRelationRecordsListItemProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const {
fieldDefinition,
recordId,
isRecordFieldReadOnly: parentIsRecordFieldReadOnly,
} = useContext(FieldContext);
const { openModal } = useModal();
@@ -175,13 +179,13 @@ export const RecordDetailRelationRecordsListItem = ({
dropdownId,
);
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
({ id }) => id === relationFieldMetadataId,
);
const handleDetach = () => {
closeDropdown(dropdownInstanceId);
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
({ id }) => id === relationFieldMetadataId,
);
if (!relationFieldMetadataItem?.name) return;
if (isToOneObject) {
@@ -236,16 +240,11 @@ export const RecordDetailRelationRecordsListItem = ({
[isExpanded],
);
const isRecordReadOnly = useIsRecordReadOnly({
const isRelationRecordReadOnly = useIsRecordReadOnly({
recordId: relationRecord.id,
objectMetadataId: relationObjectMetadataItem.id,
});
const isFieldReadOnly = useIsFieldValueReadOnly({
fieldDefinition,
isRecordReadOnly,
});
return (
<>
<StyledListItem isDropdownOpen={isDropdownOpen}>
@@ -260,7 +259,7 @@ export const RecordDetailRelationRecordsListItem = ({
accent="tertiary"
/>
</StyledClickableZone>
{!isFieldReadOnly && (
{!parentIsRecordFieldReadOnly && (
<Dropdown
dropdownId={dropdownInstanceId}
dropdownPlacement="right-start"
@@ -312,7 +311,15 @@ export const RecordDetailRelationRecordsListItem = ({
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isReadOnly: isFieldReadOnly,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly: isRelationRecordReadOnly,
objectPermissions: relationObjectPermissions,
fieldMetadataId: fieldMetadataItem.id,
objectNameSingular: relationObjectMetadataNameSingular,
fieldName: fieldMetadataItem.name,
fieldType: fieldMetadataItem.type,
isCustom: relationObjectMetadataItem.isCustom,
}),
}}
>
<RecordFieldComponentInstanceContext.Provider
@@ -2,8 +2,7 @@ import { useContext } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { RecordDetailRelationSectionDropdownToMany } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToMany';
import { RecordDetailRelationSectionDropdownToOne } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne';
@@ -16,7 +15,8 @@ type RecordDetailRelationSectionDropdownProps = {
export const RecordDetailRelationSectionDropdown = ({
loading,
}: RecordDetailRelationSectionDropdownProps) => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const { fieldDefinition, isRecordFieldReadOnly, recordId } =
useContext(FieldContext);
const {
relationType,
objectMetadataNameSingular,
@@ -32,24 +32,24 @@ export const RecordDetailRelationSectionDropdown = ({
useObjectMetadataItem({
objectNameSingular: relationObjectMetadataNameSingular,
});
// TODO: use new relation type
const isToOneObject = relationType === RelationType.MANY_TO_ONE;
const isToManyObjects = relationType === RelationType.ONE_TO_MANY;
const isRecordReadOnly = useIsRecordReadOnly({
const isRecordReadOnlyFromRelatedRecordPerspective = useIsRecordReadOnly({
recordId,
objectMetadataId: isToOneObject
? recordObjectMetadataItem.id
: relationObjectMetadataItem.id,
});
const isFieldReadOnly = useIsFieldValueReadOnly({
fieldDefinition,
isRecordReadOnly,
});
if (loading || isFieldReadOnly) return null;
if (
loading ||
isRecordFieldReadOnly ||
isRecordReadOnlyFromRelatedRecordPerspective
) {
return null;
}
if (isToOneObject) {
return <RecordDetailRelationSectionDropdownToOne />;
@@ -45,7 +45,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
)!,
objectMetadataItem: mockedCompanyObjectMetadataItem,
}),
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<Story />
@@ -3,6 +3,7 @@ import { ReactNode } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordTableContextProvider as RecordTableContextInternalProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@@ -23,6 +24,10 @@ export const RecordTableContextProvider = ({
objectNameSingular,
});
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
recordTableId,
@@ -36,6 +41,7 @@ export const RecordTableContextProvider = ({
visibleTableColumns,
recordTableId,
objectNameSingular,
objectPermissions,
}}
>
{children}
@@ -70,6 +70,9 @@ const meta: Meta = {
visibleTableColumns: mockPerformance.visibleTableColumns as any,
objectNameSingular:
mockPerformance.objectMetadataItem.nameSingular,
objectPermissions: {
objectMetadataId: mockPerformance.objectMetadataItem.id,
},
}}
>
<RecordTableComponentInstance
@@ -120,7 +123,7 @@ const meta: Meta = {
fieldDefinition: {
...mockPerformance.fieldDefinition,
},
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RelationFieldValueSetterEffect />
@@ -1,6 +1,7 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectPermission } from '~/generated/graphql';
import { createRequiredContext } from '~/utils/createRequiredContext';
type RecordTableContextValue = {
@@ -9,6 +10,7 @@ type RecordTableContextValue = {
objectNameSingular: string;
objectMetadataItem: ObjectMetadataItem;
visibleTableColumns: ColumnDefinition<FieldMetadata>[];
objectPermissions: ObjectPermission;
};
export const [RecordTableContextProvider, useRecordTableContextOrThrow] =
@@ -7,7 +7,7 @@ export type RecordTableRowContextValue = {
rowIndex: number;
isSelected: boolean;
inView: boolean;
isReadOnly?: boolean;
isRecordReadOnly?: boolean;
};
export const [RecordTableRowContextProvider, useRecordTableRowContextOrThrow] =
@@ -1,5 +1,7 @@
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { IconComponent } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@@ -8,8 +10,6 @@ import {
AnimatedPlaceholderEmptyTitle,
AnimatedPlaceholderType,
} from 'twenty-ui/layout';
import { Button } from 'twenty-ui/input';
import { IconComponent } from 'twenty-ui/display';
type RecordTableEmptyStateDisplayButtonComponentProps = {
buttonComponent?: React.ReactNode;
@@ -59,8 +59,11 @@ export const RecordTableCellBaseContainer = ({
}: {
children: ReactNode;
}) => {
const { isReadOnly, fieldDefinition, isLabelIdentifier } =
useContext(FieldContext);
const {
isRecordFieldReadOnly: isReadOnly,
fieldDefinition,
isLabelIdentifier,
} = useContext(FieldContext);
const { setIsFocused } = useFieldFocus();
const { openTableCell } = useOpenRecordTableCellFromCell();
const { theme } = useContext(ThemeContext);
@@ -10,7 +10,8 @@ export const RecordTableCellDisplayMode = ({
}: {
children: ReactNode;
}) => {
const { recordId, isReadOnly } = useContext(FieldContext);
const { recordId, isRecordFieldReadOnly: isReadOnly } =
useContext(FieldContext);
const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
@@ -1,7 +1,7 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
@@ -18,26 +18,16 @@ type RecordTableCellFieldContextGenericProps = {
export const RecordTableCellFieldContextGeneric = ({
children,
}: RecordTableCellFieldContextGenericProps) => {
const { recordId, isReadOnly: isTableRowReadOnly } =
useRecordTableRowContextOrThrow();
const { recordId, isRecordReadOnly } = useRecordTableRowContextOrThrow();
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { objectMetadataItem, objectPermissions } =
useRecordTableContextOrThrow();
const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } =
useRecordIndexContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext);
const isFieldReadOnly = useIsFieldValueReadOnly({
fieldDefinition: columnDefinition,
isRecordReadOnly: isTableRowReadOnly ?? false,
});
const updateRecord = useContext(RecordUpdateContext);
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
let hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
if (
@@ -70,7 +60,15 @@ export const RecordTableCellFieldContextGeneric = ({
objectMetadataItem,
}),
displayedMaxRows: 1,
isReadOnly: isFieldReadOnly,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly: isRecordReadOnly ?? false,
objectPermissions,
fieldMetadataId: columnDefinition.fieldMetadataId,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: columnDefinition.metadata.fieldName,
fieldType: columnDefinition.type,
isCustom: objectMetadataItem.isCustom,
}),
isForbidden: !hasObjectReadPermissions,
}}
>
@@ -1,6 +1,6 @@
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/record-field/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';
@@ -26,8 +26,7 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
}: RecordTableCellFieldContextLabelIdentifierProps) => {
const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } =
useRecordIndexContextOrThrow();
const { recordId, isReadOnly: isTableRowReadOnly } =
useRecordTableRowContextOrThrow();
const { recordId, isRecordReadOnly } = useRecordTableRowContextOrThrow();
const { columnDefinition } = useContext(RecordTableCellContext);
const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow();
@@ -40,11 +39,6 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
isRecordTableScrolledLeftComponentState,
);
const isFieldReadOnly = useIsFieldValueReadOnly({
fieldDefinition: columnDefinition,
isRecordReadOnly: isTableRowReadOnly ?? false,
});
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
@@ -75,7 +69,15 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
isLabelIdentifier: true,
isLabelIdentifierCompact,
displayedMaxRows: 1,
isReadOnly: isFieldReadOnly,
isRecordFieldReadOnly: isRecordFieldReadOnly({
objectPermissions,
objectNameSingular: objectMetadataItem.nameSingular,
fieldName: columnDefinition.metadata.fieldName,
fieldType: columnDefinition.type,
isCustom: objectMetadataItem.isCustom,
fieldMetadataId: columnDefinition.fieldMetadataId,
isRecordReadOnly: isRecordReadOnly ?? false,
}),
maxWidth: columnDefinition.size,
onRecordChipClick: () => {
activateRecordTableRow(rowIndex);
@@ -14,7 +14,7 @@ import { useRecoilCallback } from 'recoil';
export const RecordTableCellFieldInput = () => {
const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
const { isReadOnly } = useContext(FieldContext);
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
const instanceId = useAvailableComponentInstanceId(
RecordFieldComponentInstanceContext,
);
@@ -18,7 +18,7 @@ import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritin
export const RecordTableCellHotkeysEffect = () => {
const { openTableCell } = useOpenRecordTableCellFromCell();
const { isReadOnly } = useContext(FieldContext);
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
const cellFocusId = useCurrentlyFocusedRecordTableCellFocusId();
const { onCloseTableCell } = useRecordTableBodyContextOrThrow();
@@ -53,7 +53,7 @@ const RecordTableCellHoveredPortalContent = () => {
const isFirstColumn = hoverPosition?.column === 0;
const { isReadOnly } = useContext(FieldContext);
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
const isFieldInputOnly = useIsFieldInputOnly();
@@ -1,6 +1,6 @@
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/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';
@@ -28,21 +28,21 @@ export const RecordTableCellPortalWrapper = ({
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
);
const recordId = allRecordIds.at(position.row);
const isRecordReadOnly = useIsRecordReadOnly({
recordId: recordId ?? '',
objectMetadataId: objectMetadataItem.id,
});
if (!isDefined(anchorElement) || !isDefined(recordId)) {
return null;
}
const isReadOnly = !objectPermissions.canUpdateObjectRecords;
return ReactDOM.createPortal(
<RecordTableRowContextProvider
value={{
@@ -55,7 +55,7 @@ export const RecordTableCellPortalWrapper = ({
objectNameSingular: objectMetadataItem.nameSingular,
}) + recordId,
objectNameSingular: objectMetadataItem.nameSingular,
isReadOnly,
isRecordReadOnly,
}}
>
<RecordTableCellContext.Provider
@@ -44,7 +44,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
@@ -44,7 +44,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
@@ -19,7 +19,11 @@ export type OpenTableCellArgs = {
};
export const useOpenRecordTableCellFromCell = () => {
const { recordId, fieldDefinition, isReadOnly } = useContext(FieldContext);
const {
recordId,
fieldDefinition,
isRecordFieldReadOnly: isReadOnly,
} = useContext(FieldContext);
const { pathToShowPage, objectNameSingular } =
useRecordTableRowContextOrThrow();
@@ -2,8 +2,7 @@ import styled from '@emotion/styled';
import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback } from 'recoil';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { isObjectReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isObjectReadOnly';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
@@ -115,7 +114,8 @@ type RecordTableHeaderCellProps = {
export const RecordTableHeaderCell = ({
column,
}: RecordTableHeaderCellProps) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const { objectMetadataItem, objectPermissions } =
useRecordTableContextOrThrow();
const resizeFieldOffsetState = useRecoilComponentCallbackStateV2(
resizeFieldOffsetComponentState,
@@ -223,11 +223,9 @@ export const RecordTableHeaderCell = ({
createNewIndexRecord();
};
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const isReadOnly = isObjectReadOnly({
objectPermissions,
});
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
@@ -1,5 +1,5 @@
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/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';
@@ -85,9 +85,7 @@ export const RecordTableTr = forwardRef<
RecordTableTrProps
>(({ children, recordId, focusIndex, isDragging = false, ...props }, ref) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const currentRowSelected = useRecoilComponentFamilyValueV2(
isRowSelectedComponentFamilyState,
recordId,
@@ -125,7 +123,10 @@ export const RecordTableTr = forwardRef<
const isNextRowActiveOrFocused =
(isRowFocusActive && isNextRowFocused) || isNextRowActive;
const isReadOnly = !objectPermissions.canUpdateObjectRecords;
const isRecordReadOnly = useIsRecordReadOnly({
recordId,
objectMetadataId: objectMetadataItem.id,
});
return (
<RecordTableRowContextProvider
@@ -139,7 +140,7 @@ export const RecordTableTr = forwardRef<
objectNameSingular: objectMetadataItem.nameSingular,
isSelected: currentRowSelected,
inView: isRowVisible,
isReadOnly,
isRecordReadOnly,
}}
>
<StyledTr
@@ -31,7 +31,8 @@ export const RecordTitleCell = ({
sizeVariant,
containerType,
}: RecordTitleCellProps) => {
const { fieldDefinition, recordId, isReadOnly } = useContext(FieldContext);
const { fieldDefinition, recordId, isRecordFieldReadOnly } =
useContext(FieldContext);
const isFieldInputOnly = useIsFieldInputOnly();
@@ -90,7 +91,7 @@ export const RecordTitleCell = ({
),
editModeContentOnly: isFieldInputOnly,
loading: loading,
isReadOnly,
isReadOnly: isRecordFieldReadOnly,
containerType,
};
@@ -150,7 +150,7 @@ export const SettingsDataModelFieldPreview = ({
},
defaultValue: fieldMetadataItem.defaultValue,
},
isReadOnly: false,
isRecordFieldReadOnly: false,
disableChipClick: true,
}}
>
@@ -92,6 +92,8 @@ const InternalTableContextProviders = ({
recordTableId: objectMetadataItem.namePlural,
viewBarId: 'view-bar',
visibleTableColumns: visibleTableColumns,
objectPermissions:
objectPermissionsByObjectMetadataId[objectMetadataItem.id],
}}
>
<RecordTableBodyContextProvider
@@ -136,7 +136,7 @@ export const getFieldDecorator =
position: 0,
objectMetadataItem,
}),
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
<RecordMockSetterEffect
@@ -51,7 +51,7 @@ export const useMockFieldContext = ({
}),
useUpdateRecord: useUpdateOneObjectMutation,
clearable,
isReadOnly: false,
isRecordFieldReadOnly: false,
}}
>
{children}
@@ -136,6 +136,7 @@ export const mockedUserData: MockedUser = {
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
restrictedFields: {},
})),
},
locale: 'en',
@@ -165,6 +166,7 @@ export const mockedLimitedPermissionsUserData: MockedUser = {
canUpdateObjectRecords: true,
canSoftDeleteObjectRecords: true,
canDestroyObjectRecords: true,
restrictedFields: {},
})),
},
};