From 8674cf973e0ea6124dc3d9a33905b79f8d010885 Mon Sep 17 00:00:00 2001 From: Weiko Date: Wed, 4 Feb 2026 17:49:39 +0100 Subject: [PATCH] Display "Not shared" indicator for RLS-restricted relation fields (#17713) ## Context Previously, if for example a Person had a companyId but the company relation was null due to RLS, the company column appeared empty. Now it displays ForbiddenFieldDisplay to indicate the relation exists but is inaccessible. When RLS restricts access to a related record, the frontend now shows a "Not shared" indicator (with lock icon) instead of an empty field. Screenshot 2026-02-04 at 15 37 53 --- .../components/RelationToOneFieldDisplay.tsx | 13 +++++++++++-- .../hooks/useRelationToOneFieldDisplay.ts | 12 ++++++++++++ .../ui/utils/junction/getJoinColumnNameOrThrow.ts | 13 +++++++++++++ .../SettingsDataModelFieldRelationFormCard.tsx | 2 ++ .../helpers/process-nested-relations-v2.helper.ts | 3 --- 5 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/getJoinColumnNameOrThrow.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RelationToOneFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RelationToOneFieldDisplay.tsx index ee6d5c7df3..3dd320c683 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RelationToOneFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/RelationToOneFieldDisplay.tsx @@ -1,16 +1,25 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { RecordChip } from '@/object-record/components/RecordChip'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay'; import { useRelationToOneFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay'; import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; export const RelationToOneFieldDisplay = () => { - const { fieldValue, fieldDefinition, generateRecordChipData } = - useRelationToOneFieldDisplay(); + const { + fieldValue, + fieldDefinition, + generateRecordChipData, + foreignKeyFieldValue, + } = useRelationToOneFieldDisplay(); const { disableChipClick, triggerEvent } = useContext(FieldContext); + if (!isDefined(fieldValue) && isDefined(foreignKeyFieldValue)) { + return ; + } + if ( !isDefined(fieldValue) || !isDefined(fieldDefinition?.metadata.relationObjectMetadataNameSingular) diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts index 257ea7ce07..7956827c0d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts @@ -11,6 +11,7 @@ import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateD import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { getJoinColumnNameOrThrow } from '@/object-record/record-field/ui/utils/junction/getJoinColumnNameOrThrow'; import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue'; import { isDefined } from 'twenty-shared/utils'; @@ -41,6 +42,16 @@ export const useRelationToOneFieldDisplay = () => { fieldDefinition, ); + const joinColumnName = getJoinColumnNameOrThrow( + fieldDefinition.metadata.settings, + ); + + const foreignKeyFieldValue = useRecordFieldValue( + recordId, + joinColumnName, + { type: FieldMetadataType.UUID, metadata: { fieldName: joinColumnName } }, + ); + const maxWidthForField = isDefined(button) && isDefined(maxWidth) ? maxWidth - FIELD_EDIT_BUTTON_WIDTH @@ -69,6 +80,7 @@ export const useRelationToOneFieldDisplay = () => { return { fieldDefinition, fieldValue, + foreignKeyFieldValue, maxWidth: maxWidthForField, recordId, generateRecordChipData, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/getJoinColumnNameOrThrow.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/getJoinColumnNameOrThrow.ts new file mode 100644 index 0000000000..4326a3e4bd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/getJoinColumnNameOrThrow.ts @@ -0,0 +1,13 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { getJoinColumnName } from '@/object-record/record-field/ui/utils/junction/getJoinColumnName'; +import { isDefined } from 'twenty-shared/utils'; + +export const getJoinColumnNameOrThrow = ( + settings: FieldMetadataItem['settings'], +): string => { + const joinColumnName = getJoinColumnName(settings); + if (!isDefined(joinColumnName)) { + throw new Error('Join column name is not defined'); + } + return joinColumnName; +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationFormCard.tsx index a7bff24f2c..15444f68e5 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationFormCard.tsx @@ -84,6 +84,7 @@ export const SettingsDataModelFieldRelationFormCard = ({ type: FieldMetadataType.RELATION, settings: { relationType, + joinColumnName: 'previewJoinColumnId', }, }} shrink @@ -107,6 +108,7 @@ export const SettingsDataModelFieldRelationFormCard = ({ type: FieldMetadataType.RELATION, settings: { relationType: oppositeRelationType, + joinColumnName: 'previewJoinColumnId', }, }} shrink diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index 80601fc367..62a96925cf 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -417,9 +417,6 @@ export class ProcessNestedRelationsV2Helper { (rel) => rel[joinField] === item.id, ); } else { - if (relationResults.length === 0) { - item[`${sourceFieldName}Id`] = null; - } item[sourceFieldName] = relationResults.find( (rel) => rel.id === item[`${sourceFieldName}Id`],