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.

<img width="1275" height="399" alt="Screenshot 2026-02-04 at 15 37 53"
src="https://github.com/user-attachments/assets/870306f8-f811-4dc0-b23b-a33e89043a37"
/>
This commit is contained in:
Weiko
2026-02-04 17:49:39 +01:00
committed by GitHub
parent 96c37b30c5
commit 8674cf973e
5 changed files with 38 additions and 5 deletions
@@ -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 <ForbiddenFieldDisplay />;
}
if (
!isDefined(fieldValue) ||
!isDefined(fieldDefinition?.metadata.relationObjectMetadataNameSingular)
@@ -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<string | null | undefined>(
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,
@@ -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;
};