mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-12 18:08:58 -04:00
fix(front): show readable targets in morph relation picker (#21513)
## Problem The **morph relation picker is broken** when the user does not have read permission on *every* object a morph relation can point to. A morph (polymorphic) relation can target several objects. The single-record picker passed **all** of those target objects to the `search` query via `includedObjectNameSingulars`. The backend runs the per-object searches inside a single `Promise.all`, so if **one** target object is forbidden, the whole search rejects and the picker shows **"No records found"** — even for the target objects the user *can* read. This makes the morph relation picker unusable in any workspace where a role restricts read access to one of the morph targets (e.g. the demo workspace's `Object-restricted` role, which denies reading `Rocket` — the picker for a Pet's polymorphic owner then shows nothing, hiding the readable `Survey result` records too). ## Fix Filter the searched target objects down to the ones the current user is allowed to read before querying, in `useSingleRecordPickerPerformSearch`. This mirrors what the multiple-record picker (`useMultipleRecordPickerPerformSearch`) already does via `filteredSearchableObjectMetadataItems`. For a normal (single-target) relation this is a no-op; for a morph relation the picker now lists records from every target the user can read and silently skips the forbidden ones. ## Test Added `useSingleRecordPickerPerformSearch.test.tsx`: - excludes morph target objects the user cannot read from the search - keeps all targets when the user can read all of them ## Verification Reproduced locally on a Pet's "Polymorphic Owner" morph relation (targets `Rocket` + `Survey result`) with `Rocket` read denied: - **Before:** picker shows "No records found". - **After:** picker lists the readable `Survey result` records and omits the `Rocket` ones. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21513?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -3,11 +3,13 @@ import { useStore } from 'jotai';
|
||||
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords';
|
||||
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext';
|
||||
import { singleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { CustomError, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -33,6 +35,26 @@ export const useSingleRecordPickerPerformSearch = ({
|
||||
);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const readableObjectNameSingulars = objectNameSingulars.filter(
|
||||
(objectNameSingular) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
}).canReadObjectRecords === true
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const hasSelectedIds = selectedIds.length > 0;
|
||||
const selectedIdsFilter = hasSelectedIds
|
||||
@@ -41,7 +63,7 @@ export const useSingleRecordPickerPerformSearch = ({
|
||||
|
||||
const { loading: selectedRecordsLoading, searchRecords: selectedRecords } =
|
||||
useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
objectNameSingulars: readableObjectNameSingulars,
|
||||
filter: selectedIdsFilter,
|
||||
skip: !hasSelectedIds,
|
||||
searchInput: '',
|
||||
@@ -51,7 +73,7 @@ export const useSingleRecordPickerPerformSearch = ({
|
||||
loading: filteredSelectedRecordsLoading,
|
||||
searchRecords: filteredSelectedRecords,
|
||||
} = useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
objectNameSingulars: readableObjectNameSingulars,
|
||||
filter: selectedIdsFilter,
|
||||
skip: !hasSelectedIds,
|
||||
searchInput: searchFilter,
|
||||
@@ -63,7 +85,7 @@ export const useSingleRecordPickerPerformSearch = ({
|
||||
: undefined;
|
||||
const { loading: recordsToSelectLoading, searchRecords: recordsToSelect } =
|
||||
useObjectRecordSearchRecords({
|
||||
objectNameSingulars,
|
||||
objectNameSingulars: readableObjectNameSingulars,
|
||||
filter: notFilter,
|
||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||
searchInput: searchFilter,
|
||||
@@ -92,14 +114,14 @@ export const useSingleRecordPickerPerformSearch = ({
|
||||
instanceId: singleRecordPickerInstanceId,
|
||||
}),
|
||||
objectMetadataItems.filter((objectMetadataItem) =>
|
||||
objectNameSingulars.includes(objectMetadataItem.nameSingular),
|
||||
readableObjectNameSingulars.includes(objectMetadataItem.nameSingular),
|
||||
),
|
||||
);
|
||||
}, [
|
||||
allSearchRecords,
|
||||
store,
|
||||
objectMetadataItems,
|
||||
objectNameSingulars,
|
||||
readableObjectNameSingulars,
|
||||
singleRecordPickerInstanceId,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user