From 932910e71ef6558f6f3f773a4e22d989eb0ea643 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:42:27 +0200 Subject: [PATCH] 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 --- .../components/CalendarEventDetails.tsx | 8 +- .../components/ActivityRichTextEditor.tsx | 26 ++-- .../components/ActivityTargetsInlineCell.tsx | 3 +- .../components/EventFieldDiffValue.tsx | 2 +- .../contexts/RecordBoardContext.ts | 2 + .../components/RecordBoardCardBody.tsx | 31 +++-- .../RecordBoardCardDraggableContainer.tsx | 2 +- .../components/FieldContextProvider.tsx | 16 +-- .../record-field/contexts/FieldContext.ts | 2 +- .../hooks/__tests__/useGetButtonIcon.test.tsx | 2 +- .../hooks/__tests__/useIsFieldEmpty.test.tsx | 2 +- .../__tests__/useIsFieldInputOnly.test.tsx | 2 +- .../useIsFieldValueReadOnly.test.tsx | 29 ----- .../hooks/__tests__/usePersistField.test.tsx | 2 +- .../__tests__/useToggleEditOnlyInput.test.tsx | 2 +- .../read-only/useIsRecordFieldReadOnly.ts | 53 +++++++++ .../hooks/read-only/useIsRecordReadOnly.ts | 28 +++++ .../utils/__tests__/isObjectReadOnly.test.ts | 25 ++++ .../__tests__/isRecordFieldReadOnly.test.ts | 93 +++++++++++++++ .../utils/__tests__/isRecordReadonly.test.ts | 51 ++++++++ .../internal/isFieldReadOnlyByPermissions.ts | 21 ++++ .../internal/isFieldReadOnlyBySystem.ts} | 14 +-- .../hooks/read-only/utils/isObjectReadOnly.ts | 11 ++ .../read-only/utils/isRecordFieldReadOnly.ts | 39 ++++++ .../hooks/read-only/utils/isRecordReadOnly.ts | 13 ++ .../hooks/useIsFieldValueReadOnly.ts | 23 ---- ...ecordReadOnly.ts => useIsRecordDeleted.ts} | 13 +- ...ationFromManyFieldDisplay.perf.stories.tsx | 2 +- .../__stories__/AddressFieldInput.stories.tsx | 2 +- .../__stories__/ArrayFieldInput.stories.tsx | 2 +- .../__stories__/BooleanFieldInput.stories.tsx | 2 +- .../DateTimeFieldInput.stories.tsx | 2 +- .../__stories__/EmailsFieldInput.stories.tsx | 2 +- .../__stories__/LinksFieldInput.stories.tsx | 2 +- .../__stories__/NumberFieldInput.stories.tsx | 2 +- .../__stories__/PhonesFieldInput.stories.tsx | 2 +- .../__stories__/RatingFieldInput.stories.tsx | 2 +- .../RelationFromManyFieldInput.stories.tsx | 2 +- .../RelationToOneFieldInput.stories.tsx | 2 +- .../RichTextFieldInput.stories.tsx | 2 +- .../__stories__/TextFieldInput.stories.tsx | 2 +- .../__tests__/isFieldValueReadOnly.test.ts | 112 ------------------ .../components/RecordIndexBoardContainer.tsx | 6 + .../components/RecordInlineCell.tsx | 2 +- .../record-show/components/FieldsCard.tsx | 33 ++++-- .../ObjectRecordShowPageBreadcrumb.tsx | 7 +- .../record-show/components/SummaryCard.tsx | 7 +- .../RecordDetailRelationRecordsListItem.tsx | 37 +++--- .../RecordDetailRelationSectionDropdown.tsx | 22 ++-- .../RecordDetailRelationSection.stories.tsx | 2 +- .../components/RecordTableContextProvider.tsx | 6 + .../perf/RecordTableCell.perf.stories.tsx | 5 +- .../contexts/RecordTableContext.ts | 2 + .../contexts/RecordTableRowContext.ts | 2 +- .../RecordTableEmptyStateDisplay.tsx | 4 +- .../RecordTableCellBaseContainer.tsx | 7 +- .../components/RecordTableCellDisplayMode.tsx | 3 +- .../RecordTableCellFieldContextGeneric.tsx | 28 ++--- ...rdTableCellFieldContextLabelIdentifier.tsx | 20 ++-- .../components/RecordTableCellFieldInput.tsx | 2 +- .../RecordTableCellHotkeysEffect.tsx | 2 +- .../RecordTableCellHoveredPortal.tsx | 2 +- .../RecordTableCellPortalWrapper.tsx | 16 +-- .../useCloseRecordTableCellInGroup.test.tsx | 2 +- .../useCloseRecordTableCellNoGroup.test.tsx | 2 +- .../hooks/useOpenRecordTableCellFromCell.ts | 6 +- .../components/RecordTableHeaderCell.tsx | 14 +-- .../components/RecordTableTr.tsx | 13 +- .../components/RecordTitleCell.tsx | 5 +- .../SettingsDataModelFieldPreview.tsx | 2 +- .../decorators/RecordTableDecorator.tsx | 2 + .../testing/decorators/getFieldDecorator.tsx | 2 +- .../src/testing/hooks/useMockFieldContext.tsx | 2 +- .../src/testing/mock-data/users.ts | 2 + 74 files changed, 576 insertions(+), 346 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldValueReadOnly.test.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/useIsRecordReadOnly.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isObjectReadOnly.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isRecordFieldReadOnly.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isRecordReadonly.test.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyByPermissions.ts rename packages/twenty-front/src/modules/object-record/record-field/{utils/isFieldValueReadOnly.ts => hooks/read-only/utils/internal/isFieldReadOnlyBySystem.ts} (88%) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isObjectReadOnly.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isRecordReadOnly.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldValueReadOnly.ts rename packages/twenty-front/src/modules/object-record/record-field/hooks/{useIsRecordReadOnly.ts => useIsRecordDeleted.ts} (52%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx index f21b961772..5af372fc69 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx @@ -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) => ( [() => undefined, { loading: false }], maxWidth: 300, - isReadOnly: false, + isRecordFieldReadOnly: isRecordReadOnly, }} > 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} /> ); diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx index 85594b86fa..bb3857eaa2 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetsInlineCell.tsx @@ -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(); diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx index b3ae025d79..2a0e1e3683 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx @@ -49,7 +49,7 @@ export const EventFieldDiffValue = ({ }, defaultValue: fieldMetadataItem.defaultValue, }, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts b/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts index 266e1a4454..1524e40b9e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts @@ -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; recordBoardId: string; + objectPermissions: ObjectPermission; }; export const RecordBoardContext = createContext( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx index cd5a0c188b..34ca96bbd2 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx @@ -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 ( - {fieldDefinitions.map((fieldDefinition) => ( + {fieldDefinitionsWithReadOnly.map((fieldDefinition) => ( 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 ( {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts b/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts index c0e82dd3ed..d212098dbe 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/contexts/FieldContext.ts @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useGetButtonIcon.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useGetButtonIcon.test.tsx index 4a563e1e43..17d8cf5bfe 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useGetButtonIcon.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useGetButtonIcon.test.tsx @@ -22,7 +22,7 @@ const getWrapper = fieldDefinition, recordId, isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldEmpty.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldEmpty.test.tsx index ac8f8e9db1..f3a1473ab4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldEmpty.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldEmpty.test.tsx @@ -15,7 +15,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => ( fieldDefinition: phonesFieldDefinition, recordId, isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldInputOnly.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldInputOnly.test.tsx index f7cf7eb8c4..7add26a0d3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldInputOnly.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldInputOnly.test.tsx @@ -21,7 +21,7 @@ const getWrapper = fieldDefinition, recordId, isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldValueReadOnly.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldValueReadOnly.test.tsx deleted file mode 100644 index 29b5b5f97c..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useIsFieldValueReadOnly.test.tsx +++ /dev/null @@ -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); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx index 785531ecc8..dfa1525826 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx @@ -104,7 +104,7 @@ const getWrapper = recordId, isLabelIdentifier: false, useUpdateRecord: useUpdateOneRecordMutation, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx index 20525fa0b6..f967b0f1fc 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx @@ -346,7 +346,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => { recordId, isLabelIdentifier: false, useUpdateRecord: useUpdateOneRecordMutation, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > {children} diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly.ts new file mode 100644 index 0000000000..058eac6d0f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly.ts @@ -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, + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/useIsRecordReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/useIsRecordReadOnly.ts new file mode 100644 index 0000000000..d1fb92bb09 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/useIsRecordReadOnly.ts @@ -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, + }); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isObjectReadOnly.test.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isObjectReadOnly.test.ts new file mode 100644 index 0000000000..3128922571 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isObjectReadOnly.test.ts @@ -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); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isRecordFieldReadOnly.test.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isRecordFieldReadOnly.test.ts new file mode 100644 index 0000000000..02a514f0c6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isRecordFieldReadOnly.test.ts @@ -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); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isRecordReadonly.test.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isRecordReadonly.test.ts new file mode 100644 index 0000000000..005743a63d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/__tests__/isRecordReadonly.test.ts @@ -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); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyByPermissions.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyByPermissions.ts new file mode 100644 index 0000000000..9d8d8a70e6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyByPermissions.ts @@ -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; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyBySystem.ts similarity index 88% rename from packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts rename to packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyBySystem.ts index da57b241ad..0e98500203 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyBySystem.ts @@ -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, diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isObjectReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isObjectReadOnly.ts new file mode 100644 index 0000000000..627379b324 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isObjectReadOnly.ts @@ -0,0 +1,11 @@ +import { ObjectPermission } from '~/generated/graphql'; + +type IsObjectReadOnlyParams = { + objectPermissions: ObjectPermission; +}; + +export const isObjectReadOnly = ({ + objectPermissions, +}: IsObjectReadOnlyParams) => { + return !objectPermissions.canUpdateObjectRecords; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly.ts new file mode 100644 index 0000000000..a7033b6407 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly.ts @@ -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 + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isRecordReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isRecordReadOnly.ts new file mode 100644 index 0000000000..8ebe655cb5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/read-only/utils/isRecordReadOnly.ts @@ -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; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldValueReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldValueReadOnly.ts deleted file mode 100644 index 5435043e2e..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldValueReadOnly.ts +++ /dev/null @@ -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; -}; - -export const useIsFieldValueReadOnly = ({ - fieldDefinition, - isRecordReadOnly, -}: UseIsFieldValueReadOnlyParams) => { - const { metadata, type } = fieldDefinition; - - return isFieldValueReadOnly({ - objectNameSingular: metadata.objectMetadataNameSingular, - fieldName: metadata.fieldName, - fieldType: type, - isRecordReadOnly, - isCustom: metadata.isCustom, - }); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordDeleted.ts similarity index 52% rename from packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordReadOnly.ts rename to packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordDeleted.ts index 3b9a356dc3..2f13862d4d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsRecordDeleted.ts @@ -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( recordStoreFamilySelector({ recordId, @@ -20,9 +17,5 @@ export const useIsRecordReadOnly = ({ }), ); - const objectPermissions = useObjectPermissionsForObject(objectMetadataId); - - const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords; - - return !hasObjectUpdatePermissions || isDefined(recordDeletedAt); + return isDefined(recordDeletedAt); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx index e4cdaa747b..0f92a80d8e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx @@ -47,7 +47,7 @@ const meta: Meta = { fieldDefinition: { ...relationFromManyFieldDisplayMock.fieldDefinition, } as unknown as FieldDefinition, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx index 23eb77f546..1e8847c26a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx @@ -84,7 +84,7 @@ const AddressInputWithContext = ({ }, recordId: recordId ?? '123', isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx index de5c770165..36a1d2f1c1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/ArrayFieldInput.stories.tsx @@ -99,7 +99,7 @@ const ArrayInputWithContext = ({ }, recordId, isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, useUpdateRecord: () => [updateRecord, { loading: false }], }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx index 447e0394a3..27ffd33c23 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx @@ -66,7 +66,7 @@ const BooleanFieldInputWithContext = ({ }, recordId: recordId ?? '123', isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx index 8e9e788308..9df1846a32 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailsFieldInput.stories.tsx @@ -98,7 +98,7 @@ const EmailInputWithContext = ({ }, recordId, isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, useUpdateRecord: () => [updateRecord, { loading: false }], }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx index 12b0bae1c3..6592587a45 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/LinksFieldInput.stories.tsx @@ -107,7 +107,7 @@ const LinksInputWithContext = ({ }, recordId, isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, useUpdateRecord: () => [updateRecord, { loading: false }], }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx index d614e7000a..dab6d80a5d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -88,7 +88,7 @@ const NumberFieldInputWithContext = ({ }, recordId: '123', isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > {isReady && } diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx index 2cf8e48ade..9e79f4aa12 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhonesFieldInput.stories.tsx @@ -97,7 +97,7 @@ const PhoneInputWithContext = ({ }, recordId, isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, useUpdateRecord: () => [updateRecord, { loading: false }], }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx index c0f75995f9..0f84852851 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx @@ -76,7 +76,7 @@ const RatingFieldInputWithContext = ({ }, recordId: recordId ?? '123', isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx index 2276d0acb2..8f30846bc1 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFromManyFieldInput.stories.tsx @@ -100,7 +100,7 @@ const RelationManyFieldInputWithContext = () => { fieldDefinition, recordId: 'recordId', isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx index f0a98f3855..e7c1219197 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx @@ -94,7 +94,7 @@ const RelationToOneFieldInputWithContext = ({ }, recordId: recordId, isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts deleted file mode 100644 index f5395cf42a..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueReadOnly.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx index 044753baef..ba33b1f9bb 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx @@ -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, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx index 96210b576a..c2e58de272 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx @@ -43,7 +43,7 @@ export const RecordInlineCell = ({ isDisplayModeFixHeight, onOpenEditMode: onOpenEditModeFromContext, onCloseEditMode: onCloseEditModeFromContext, - isReadOnly, + isRecordFieldReadOnly: isReadOnly, } = useContext(FieldContext); const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode(); diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx index 19623ac1dd..08ea26caae 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx @@ -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, + }), }} > { - 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 ( <> @@ -260,7 +259,7 @@ export const RecordDetailRelationRecordsListItem = ({ accent="tertiary" /> - {!isFieldReadOnly && ( + {!parentIsRecordFieldReadOnly && ( { - 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 ; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx index 7427eb87b3..f6ebd47ecb 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx @@ -45,7 +45,7 @@ const meta: Meta = { )!, objectMetadataItem: mockedCompanyObjectMetadataItem, }), - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx index d7720d129f..6311fa8e1c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx @@ -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} diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx index 75e8d3b80d..58fa0639b4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx @@ -70,6 +70,9 @@ const meta: Meta = { visibleTableColumns: mockPerformance.visibleTableColumns as any, objectNameSingular: mockPerformance.objectMetadataItem.nameSingular, + objectPermissions: { + objectMetadataId: mockPerformance.objectMetadataItem.id, + }, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts index 0f7ce03dad..071d19b85e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts @@ -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[]; + objectPermissions: ObjectPermission; }; export const [RecordTableContextProvider, useRecordTableContextOrThrow] = diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts index 7eca8385e2..cca01ee0d0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts @@ -7,7 +7,7 @@ export type RecordTableRowContextValue = { rowIndex: number; isSelected: boolean; inView: boolean; - isReadOnly?: boolean; + isRecordReadOnly?: boolean; }; export const [RecordTableRowContextProvider, useRecordTableRowContextOrThrow] = diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx index 5312230d87..505df057ba 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx index 99c2905a02..c4fb599e73 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx @@ -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); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx index 6116a73fc8..05f42156da 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx @@ -10,7 +10,8 @@ export const RecordTableCellDisplayMode = ({ }: { children: ReactNode; }) => { - const { recordId, isReadOnly } = useContext(FieldContext); + const { recordId, isRecordFieldReadOnly: isReadOnly } = + useContext(FieldContext); const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx index 30123f8a19..c784e7487c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextGeneric.tsx @@ -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, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier.tsx index ab4e1fc154..30f9c4071b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextLabelIdentifier.tsx @@ -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); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx index edc956701d..d08f00c63b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx @@ -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, ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHotkeysEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHotkeysEffect.tsx index 810ffce1b0..dab37f13e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHotkeysEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHotkeysEffect.tsx @@ -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(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx index f649f0a5c0..7c5eb3a648 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellHoveredPortal.tsx @@ -53,7 +53,7 @@ const RecordTableCellHoveredPortalContent = () => { const isFirstColumn = hoverPosition?.column === 0; - const { isReadOnly } = useContext(FieldContext); + const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext); const isFieldInputOnly = useIsFieldInputOnly(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx index 7fdf7061be..e5d1ef7be9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellPortalWrapper.tsx @@ -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( ( fieldDefinition: textfieldDefinition, recordId: 'recordId', isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx index e1fda6e911..4ca99221fa 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx @@ -44,7 +44,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => ( fieldDefinition: textfieldDefinition, recordId: 'recordId', isLabelIdentifier: false, - isReadOnly: false, + isRecordFieldReadOnly: false, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts index 67d7affce2..82ae0148cc 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts @@ -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(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 3beb8b6989..6fd8a458a7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -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; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx index 2d7c70f9eb..a4d1e24891 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTableTr.tsx @@ -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 ( { - 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, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx index 6ff62ade0b..6afdd574ef 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx @@ -150,7 +150,7 @@ export const SettingsDataModelFieldPreview = ({ }, defaultValue: fieldMetadataItem.defaultValue, }, - isReadOnly: false, + isRecordFieldReadOnly: false, disableChipClick: true, }} > diff --git a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx index 052102e5e0..ba60ac3cb4 100644 --- a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx @@ -92,6 +92,8 @@ const InternalTableContextProviders = ({ recordTableId: objectMetadataItem.namePlural, viewBarId: 'view-bar', visibleTableColumns: visibleTableColumns, + objectPermissions: + objectPermissionsByObjectMetadataId[objectMetadataItem.id], }} > {children} diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 7516b83fd5..c62a866a03 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -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: {}, })), }, };