From 6aaafb76b6e82cf0afd279f24ac5c9ce0730c6bd Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:05:45 +0500 Subject: [PATCH] fix: show "Not shared" for junction relation fields when target or intermediate object is not readable (#18025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user's role lacks read permission on the target object (e.g., Company) or the intermediate junction object (e.g., EmploymentHistory), junction relation fields like "Previous Companies" displayed as blank instead of showing "Not shared." - In RecordFieldList, junction fields now check the junction object's read permission and set isForbidden on the field context so FieldDisplay renders "Not shared" instead of an empty field. - In RelationFromManyFieldDisplay, if junction records exist but all nested target records are null (permission-denied by the API), the component renders "Not shared" instead of an empty list. --------- Co-authored-by: Félix Malfait --- .../components/RecordFieldList.tsx | 9 +++ .../RelationFromManyFieldDisplay.tsx | 5 ++ .../junction/isJunctionRelationForbidden.ts | 75 +++++++++++++++++++ .../RecordInlineCellAnchoredPortal.tsx | 23 +++++- .../components/RecordInlineCellValue.tsx | 8 ++ .../RecordTableCellFieldContextGeneric.tsx | 35 +++++++++ .../fields/components/FieldsWidget.tsx | 9 +++ 7 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldList.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldList.tsx index 37052b8017..37e910044c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldList.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/components/RecordFieldList.tsx @@ -1,5 +1,6 @@ import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; @@ -15,6 +16,7 @@ import { RecordFieldListComponentInstanceContext } from '@/object-record/record- import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { isJunctionRelationForbidden } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden'; 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'; @@ -53,6 +55,7 @@ export const RecordFieldList = ({ }); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const { objectMetadataItems } = useObjectMetadataItems(); const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ objectNameSingular, @@ -187,6 +190,12 @@ export const RecordFieldList = ({ fieldName: fieldMetadataItem.name, prefix: instanceId, })}`, + isForbidden: isJunctionRelationForbidden({ + fieldMetadataItem, + sourceObjectMetadataId: objectMetadataItem.id, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + }), }} > { }) .filter(isDefined); + if (fieldValue.some(isDefined) && targetRecordsWithMetadata.length === 0) { + return ; + } + return ( {targetRecordsWithMetadata.map(({ record, objectMetadata }) => ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden.ts new file mode 100644 index 0000000000..ec5e785231 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden.ts @@ -0,0 +1,75 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; +import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig'; +import { getTargetObjectMetadataIdsFromField } from '@/object-record/record-field/ui/utils/junction/getTargetObjectMetadataIdsFromField'; +import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig'; +import { type ObjectPermissions } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; + +import { type JunctionObjectMetadataItem } from './getJunctionConfig'; + +type ObjectPermissionsByObjectMetadataId = Record< + string, + ObjectPermissions & { objectMetadataId: string } +>; + +// Returns true if a junction field's intermediate or final target object +// is not readable by the current user. +export const isJunctionRelationForbidden = ({ + fieldMetadataItem, + sourceObjectMetadataId, + objectMetadataItems, + objectPermissionsByObjectMetadataId, +}: { + fieldMetadataItem: Pick; + sourceObjectMetadataId: string; + objectMetadataItems: JunctionObjectMetadataItem[]; + objectPermissionsByObjectMetadataId: ObjectPermissionsByObjectMetadataId; +}): boolean => { + if (!hasJunctionConfig(fieldMetadataItem.settings)) { + return false; + } + + const junctionObjectMetadataId = + fieldMetadataItem.relation?.targetObjectMetadata.id; + + if (!isDefined(junctionObjectMetadataId)) { + return false; + } + + const junctionPermissions = getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + junctionObjectMetadataId, + ); + + if (!junctionPermissions.canReadObjectRecords) { + return true; + } + + const junctionConfig = getJunctionConfig({ + settings: fieldMetadataItem.settings, + relationObjectMetadataId: junctionObjectMetadataId, + sourceObjectMetadataId, + objectMetadataItems, + }); + + if (!isDefined(junctionConfig)) { + return false; + } + + const targetObjectMetadataIds = junctionConfig.targetFields.flatMap( + getTargetObjectMetadataIdsFromField, + ); + + if (targetObjectMetadataIds.length === 0) { + return false; + } + + return !targetObjectMetadataIds.some( + (targetId) => + getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + targetId, + ).canReadObjectRecords, + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal.tsx index 4d1ed2e0a6..4eee7a2c31 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal.tsx @@ -1,6 +1,8 @@ +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly'; import { @@ -10,6 +12,7 @@ import { } from '@/object-record/record-field/ui/contexts/FieldContext'; import { FieldFocusContextProvider } from '@/object-record/record-field/ui/contexts/FieldFocusContextProvider'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { isJunctionRelationForbidden } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden'; import { RecordInlineCellAnchoredPortalContext } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortalContext'; import { RecordInlineCellCloseOnCommandMenuOpeningEffect } from '@/object-record/record-inline-cell/components/RecordInlineCellCloseOnCommandMenuOpeningEffect'; import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; @@ -19,7 +22,14 @@ import { isDefined } from 'twenty-shared/utils'; type RecordInlineCellAnchoredPortalProps = { fieldMetadataItem: Pick< FieldMetadataItem, - 'id' | 'name' | 'type' | 'createdAt' | 'updatedAt' | 'label' + | 'id' + | 'name' + | 'type' + | 'createdAt' + | 'updatedAt' + | 'label' + | 'settings' + | 'relation' >; objectMetadataItem: ObjectMetadataItem; recordId: string; @@ -52,6 +62,16 @@ export const RecordInlineCellAnchoredPortal = ({ recordId: recordId ?? '', }); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const { objectMetadataItems } = useObjectMetadataItems(); + + const isForbidden = isJunctionRelationForbidden({ + fieldMetadataItem, + sourceObjectMetadataId: objectMetadataItem.id, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + }); + const { updateOneRecord } = useUpdateOneRecord(); const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => { @@ -88,6 +108,7 @@ export const RecordInlineCellAnchoredPortal = ({ useUpdateRecord: useUpdateOneObjectRecordMutation, isDisplayModeFixHeight: true, isRecordFieldReadOnly, + isForbidden, onCloseEditMode, }} > diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx index bb84725723..ba5dfc37a5 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx @@ -1,8 +1,11 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import { useContext } from 'react'; import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus'; +import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay'; import { useRecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext'; import { RecordInlineCellDisplayMode } from '@/object-record/record-inline-cell/components/RecordInlineCellDisplayMode'; import { RecordInlineCellSkeletonLoader } from '@/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader'; @@ -33,11 +36,16 @@ export const RecordInlineCellValue = () => { const { readonly, loading, isCentered, onOpenEditMode } = useRecordInlineCellContext(); const { isFocused } = useFieldFocus(); + const { isForbidden } = useContext(FieldContext); if (loading === true) { return ; } + if (isForbidden === true) { + return ; + } + return ( 0) { + hasObjectReadPermissions = targetObjectMetadataIds.some( + (targetId) => + getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + targetId, + ).canReadObjectRecords, + ); + } + } + } } return ( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx index b458f47657..c93db53823 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx @@ -1,5 +1,6 @@ import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; @@ -11,6 +12,7 @@ import { recordFieldListHoverPositionComponentState } from '@/object-record/reco import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { isJunctionRelationForbidden } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader'; import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions'; @@ -73,6 +75,7 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => { }); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const { objectMetadataItems } = useObjectMetadataItems(); const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ objectNameSingular: targetRecord.targetObjectNameSingular, @@ -181,6 +184,12 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => { fieldName: fieldMetadataItem.name, prefix: instanceId, })}`, + isForbidden: isJunctionRelationForbidden({ + fieldMetadataItem, + sourceObjectMetadataId: objectMetadataItem.id, + objectMetadataItems, + objectPermissionsByObjectMetadataId, + }), }} > {isActivityTarget ? (