fix: show "Not shared" for junction relation fields when target or intermediate object is not readable (#18025)
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 <felix@twenty.com>
This commit is contained in:
+9
@@ -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,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
+5
@@ -10,6 +10,7 @@ import { isActivityTargetField } from '@/object-record/record-field-list/utils/c
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus';
|
||||
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useRelationFromManyFieldDisplay';
|
||||
import { ForbiddenFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/ForbiddenFieldDisplay';
|
||||
import { extractTargetRecordsFromJunction } from '@/object-record/record-field/ui/utils/junction/extractTargetRecordsFromJunction';
|
||||
import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig';
|
||||
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
|
||||
@@ -144,6 +145,10 @@ export const RelationFromManyFieldDisplay = () => {
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (fieldValue.some(isDefined) && targetRecordsWithMetadata.length === 0) {
|
||||
return <ForbiddenFieldDisplay />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExpandableList isChipCountDisplayed={isFocused}>
|
||||
{targetRecordsWithMetadata.map(({ record, objectMetadata }) => (
|
||||
|
||||
+75
@@ -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<FieldMetadataItem, 'settings' | 'relation'>;
|
||||
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,
|
||||
);
|
||||
};
|
||||
+22
-1
@@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
+8
@@ -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 <RecordInlineCellSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (isForbidden === true) {
|
||||
return <ForbiddenFieldDisplay />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledClickableContainer readonly={readonly} isCentered={isCentered}>
|
||||
<RecordInlineCellDisplayMode
|
||||
|
||||
+35
@@ -1,3 +1,4 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
|
||||
@@ -5,11 +6,15 @@ import { type RecordField } from '@/object-record/record-field/types/RecordField
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany';
|
||||
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 { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useContext, type ReactNode } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type RecordTableCellFieldContextGenericProps = {
|
||||
recordField: RecordField;
|
||||
@@ -30,6 +35,8 @@ export const RecordTableCellFieldContextGeneric = ({
|
||||
fieldDefinitionByFieldMetadataItemId,
|
||||
} = useRecordIndexContextOrThrow();
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const fieldDefinition =
|
||||
fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
|
||||
|
||||
@@ -51,6 +58,34 @@ export const RecordTableCellFieldContextGeneric = ({
|
||||
);
|
||||
|
||||
hasObjectReadPermissions = relationObjectPermissions.canReadObjectRecords;
|
||||
|
||||
if (
|
||||
hasObjectReadPermissions &&
|
||||
hasJunctionConfig(fieldDefinition.metadata.settings)
|
||||
) {
|
||||
const junctionConfig = getJunctionConfig({
|
||||
settings: fieldDefinition.metadata.settings,
|
||||
relationObjectMetadataId,
|
||||
sourceObjectMetadataId: objectMetadataItem.id,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
if (isDefined(junctionConfig)) {
|
||||
const targetObjectMetadataIds = junctionConfig.targetFields.flatMap(
|
||||
getTargetObjectMetadataIdsFromField,
|
||||
);
|
||||
|
||||
if (targetObjectMetadataIds.length > 0) {
|
||||
hasObjectReadPermissions = targetObjectMetadataIds.some(
|
||||
(targetId) =>
|
||||
getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
targetId,
|
||||
).canReadObjectRecords,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+9
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user