diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts index d5257a1fad..a08716ec63 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useCreateOneRecordInCache.ts @@ -36,6 +36,7 @@ export const useCreateOneRecordInCache = ({ record: prefilledRecord, depth: 1, }); + const fragment = gql` fragment Create${capitalize( objectMetadataItem.nameSingular, 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 30265bbae4..75f0018dd6 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 @@ -9,6 +9,7 @@ import { RecordFieldListCellEditModePortal } from '@/object-record/record-field- import { RecordFieldListCellHoveredPortal } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortal'; import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems'; import { RecordDetailDuplicatesSection } from '@/object-record/record-field-list/record-detail-section/duplicate/components/RecordDetailDuplicatesSection'; +import { RecordDetailMorphRelationSection } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSection'; import { RecordDetailRelationSection } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSection'; import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext'; import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState'; @@ -22,6 +23,7 @@ import { useRecordShowContainerData } from '@/object-record/record-show/hooks/us import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { FieldMetadataType } from 'twenty-shared/types'; type RecordFieldListProps = { instanceId: string; @@ -212,37 +214,50 @@ export const RecordFieldList = ({ objectNameSingular={objectNameSingular} /> )} - {boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => ( - + fieldMetadataItem.type === FieldMetadataType.RELATION || + fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION, + ) + .map((fieldMetadataItem, index) => ( + - - - ))} + useUpdateRecord: useUpdateOneObjectRecordMutation, + isDisplayModeFixHeight: true, + isRecordFieldReadOnly: isRecordFieldReadOnly({ + isRecordReadOnly, + objectPermissions: + getObjectPermissionsFromMapByObjectMetadataId({ + objectPermissionsByObjectMetadataId, + objectMetadataId: objectMetadataItem.id, + }), + fieldMetadataItem: { + id: fieldMetadataItem.id, + isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false, + }, + }), + }} + > + {fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION ? ( + + ) : ( + + )} + + ))} fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name), @@ -63,7 +64,8 @@ export const useFieldListFieldMetadataItems = ({ fieldMetadataItem.type !== FieldMetadataType.RICH_TEXT_V2, ), (fieldMetadataItem) => - fieldMetadataItem.type === FieldMetadataType.RELATION + fieldMetadataItem.type === FieldMetadataType.RELATION || + fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION ? 'relationFieldMetadataItems' : 'inlineFieldMetadataItems', ); @@ -78,22 +80,35 @@ export const useFieldListFieldMetadataItems = ({ fieldMetadataItem.name === 'taskTargets'), ); - const boxedRelationFieldMetadataItems = ( - relationFieldMetadataItems ?? [] - ).filter( - (fieldMetadataItem) => - !( - (objectNameSingular === CoreObjectNameSingular.Note && - fieldMetadataItem.name === 'noteTargets') || - (objectNameSingular === CoreObjectNameSingular.Task && - fieldMetadataItem.name === 'taskTargets') - ) && - isDefined(fieldMetadataItem.relation?.targetObjectMetadata.id) && - getObjectPermissionsForObject( - objectPermissionsByObjectMetadataId, - fieldMetadataItem.relation?.targetObjectMetadata.id, - ).canReadObjectRecords, - ); + const boxedRelationFieldMetadataItems = (relationFieldMetadataItems ?? []) + .filter( + (fieldMetadataItem) => + !( + (objectNameSingular === CoreObjectNameSingular.Note && + fieldMetadataItem.name === 'noteTargets') || + (objectNameSingular === CoreObjectNameSingular.Task && + fieldMetadataItem.name === 'taskTargets') + ), + ) + .filter((fieldMetadataItem) => { + const canReadRelation = + isDefined(fieldMetadataItem.relation?.targetObjectMetadata.id) && + getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + fieldMetadataItem.relation?.targetObjectMetadata.id, + ).canReadObjectRecords; + + const canReadMorphRelation = fieldMetadataItem?.morphRelations?.every( + (morphRelation) => + isDefined(morphRelation.targetObjectMetadata.id) && + getObjectPermissionsForObject( + objectPermissionsByObjectMetadataId, + morphRelation.targetObjectMetadata.id, + ).canReadObjectRecords, + ); + + return canReadRelation || canReadMorphRelation; + }); return { inlineFieldMetadataItems, diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSection.tsx new file mode 100644 index 0000000000..4a94a1595b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSection.tsx @@ -0,0 +1,131 @@ +import { useContext } from 'react'; + +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { RecordDetailSectionContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailSectionContainer'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { + FieldInputEventContext, + type FieldInputEvent, +} from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; +import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId'; +import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; + +import { RecordDetailMorphRelationSectionDropdown } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdown'; +import { RecordDetailRelationRecordsList } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsList'; +import { useGetMorphRelationRelatedRecordsWithObjectNameSingular } from '@/object-record/record-field-list/record-detail-section/relation/components/hooks/useGetMorphRelationRelatedRecordsWithObjectNameSingular'; +import { useMorphPersistManyToOne } from '@/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne'; +import { CustomError, isDefined } from 'twenty-shared/utils'; + +type RecordDetailMorphRelationSectionProps = { + loading: boolean; +}; + +export const RecordDetailMorphRelationSection = ({ + loading, +}: RecordDetailMorphRelationSectionProps) => { + const { recordId, fieldDefinition } = useContext(FieldContext); + + const metadata = fieldDefinition.metadata as FieldMorphRelationMetadata; + const { objectMetadataNameSingular } = metadata; + + const isMobile = useIsMobile(); + + const recordsWithObjectNameSingular = + useGetMorphRelationRelatedRecordsWithObjectNameSingular({ + recordId, + morphRelations: metadata.morphRelations, + }); + + const relationRecordsCount = recordsWithObjectNameSingular.length; + + const dropdownId = getRecordFieldCardRelationPickerDropdownId({ + fieldDefinition, + recordId, + }); + + const isDropdownOpen = useRecoilComponentValue( + isDropdownOpenComponentState, + dropdownId, + ); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItemToFind) => + objectMetadataItemToFind.nameSingular === objectMetadataNameSingular, + ); + + if (!objectMetadataItem) { + throw new CustomError( + 'Object metadata item not found', + 'OBJECT_METADATA_ITEM_NOT_FOUND', + ); + } + + const { persistMorphManyToOne } = useMorphPersistManyToOne({ + objectMetadataNameSingular: objectMetadataItem.nameSingular, + }); + + const handleSubmit: FieldInputEvent = ({ newValue }) => { + if (!isDefined(newValue)) { + throw new CustomError( + 'Value to persist is required', + 'VALUE_TO_PERSIST_IS_REQUIRED', + ); + } + + const { id, objectMetadataId } = newValue as { + id: string; + objectMetadataId: string; + }; + + if (!isDefined(id)) { + throw new CustomError('Id is required', 'ID_IS_REQUIRED'); + } + const targetObjectMetadataNameSingular = objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.id === objectMetadataId, + )?.nameSingular; + + if (!isDefined(targetObjectMetadataNameSingular)) { + throw new CustomError( + 'Target object metadata name singular is required', + 'TARGET_OBJECT_METADATA_NAME_SINGULAR_IS_REQUIRED', + ); + } + + persistMorphManyToOne({ + recordId, + fieldDefinition, + valueToPersist: id, + targetObjectMetadataNameSingular, + }); + }; + + if (loading) return null; + + return ( + + 0} + rightAdornment={ + + } + > + {relationRecordsCount > 0 && ( + + )} + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdown.tsx new file mode 100644 index 0000000000..44d8ff17b5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdown.tsx @@ -0,0 +1,73 @@ +import { useContext } from 'react'; + +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { isRecordReadOnly } from '@/object-record/read-only/utils/isRecordReadOnly'; +import { RecordDetailMorphRelationSectionDropdownManyToOne } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdownManyToOne'; +import { RecordDetailMorphRelationSectionDropdownOneToMany } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdownOneToMany'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { useIsRecordDeleted } from '@/object-record/record-field/ui/hooks/useIsRecordDeleted'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; +import { isDefined } from 'twenty-shared/utils'; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; + +type RecordDetailMorphRelationSectionDropdownProps = { + loading: boolean; +}; + +export const RecordDetailMorphRelationSectionDropdown = ({ + loading, +}: RecordDetailMorphRelationSectionDropdownProps) => { + const { fieldDefinition, isRecordFieldReadOnly, recordId } = + useContext(FieldContext); + + assertFieldMetadata( + FieldMetadataType.MORPH_RELATION, + isFieldMorphRelation, + fieldDefinition, + ); + const { relationType, morphRelations } = fieldDefinition.metadata; + + const { objectMetadataItems } = useObjectMetadataItems(); + const relatedObjectMetadataItems = morphRelations + .map((morphRelation) => morphRelation.targetObjectMetadata.id) + .map((objectMetadataId) => + objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.id === objectMetadataId, + ), + ) + .filter(isDefined); + + const isToOneObject = relationType === RelationType.MANY_TO_ONE; + const isToManyObjects = relationType === RelationType.ONE_TO_MANY; + + const isDeleted = useIsRecordDeleted({ recordId }); + + const isRecordReadOnlyFromRelatedRecordPerspective = + relatedObjectMetadataItems.some((relatedObjectMetadataItem) => { + return isRecordReadOnly({ + objectPermissions: { + canUpdateObjectRecords: true, + objectMetadataId: relatedObjectMetadataItem.id, + }, + isRecordDeleted: isDeleted, + objectMetadataItem: relatedObjectMetadataItem, + }); + }); + + if ( + loading || + isRecordFieldReadOnly || + isRecordReadOnlyFromRelatedRecordPerspective + ) { + return null; + } + + if (isToOneObject) { + return ; + } else if (isToManyObjects) { + return ; + } else { + return null; + } +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdownManyToOne.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdownManyToOne.tsx new file mode 100644 index 0000000000..0b80fc219d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdownManyToOne.tsx @@ -0,0 +1,142 @@ +import { useCallback, useContext } from 'react'; + +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { useGetMorphRelationRelatedRecordsWithObjectNameSingular } from '@/object-record/record-field-list/record-detail-section/relation/components/hooks/useGetMorphRelationRelatedRecordsWithObjectNameSingular'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; +import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; +import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen'; +import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; +import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; +import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { IconForbid, IconPencil } from 'twenty-ui/display'; +import { LightIconButton } from 'twenty-ui/input'; + +export const RecordDetailMorphRelationSectionDropdownManyToOne = () => { + const { recordId, fieldDefinition } = useContext(FieldContext); + + assertFieldMetadata( + FieldMetadataType.MORPH_RELATION, + isFieldMorphRelation, + fieldDefinition, + ); + + const { morphRelations } = fieldDefinition.metadata; + const { objectMetadataItems } = useObjectMetadataItems(); + const relationObjectMetadataItems = morphRelations + .map((morphRelation) => morphRelation.targetObjectMetadata) + .map((relationObjectMetadataItem) => + objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.id === relationObjectMetadataItem.id, + ), + ) + .filter(isDefined); + + const recordsWithObjectNameSingular = + useGetMorphRelationRelatedRecordsWithObjectNameSingular({ + recordId, + morphRelations, + }); + + const dropdownId = getRecordFieldCardRelationPickerDropdownId({ + fieldDefinition, + recordId, + }); + + const { closeDropdown } = useCloseDropdown(); + + const dropdownPlacement = useRecoilComponentValue( + dropdownPlacementComponentState, + dropdownId, + ); + + const setSingleRecordPickerSearchFilter = useSetRecoilComponentState( + singleRecordPickerSearchFilterComponentState, + dropdownId, + ); + + const setSingleRecordPickerSelectedId = useSetRecoilComponentState( + singleRecordPickerSelectedIdComponentState, + dropdownId, + ); + + const handleCloseRelationPickerDropdown = useCallback(() => { + setSingleRecordPickerSearchFilter(''); + }, [setSingleRecordPickerSearchFilter]); + + const { onSubmit } = useContext(FieldInputEventContext); + + const handleRelationPickerEntitySelected = ( + selectedMorphItem?: RecordPickerPickableMorphItem, + ) => { + closeDropdown(dropdownId); + + if (!selectedMorphItem?.recordId) return; + + onSubmit?.({ + newValue: { + id: selectedMorphItem.recordId, + objectMetadataId: selectedMorphItem.objectMetadataId, + }, + }); + }; + + const { openSingleRecordPicker } = useSingleRecordPickerOpen(); + + const handleOpenRelationPickerDropdown = () => { + setSingleRecordPickerSearchFilter(''); + openSingleRecordPicker(dropdownId); + + if (recordsWithObjectNameSingular.length > 0) { + setSingleRecordPickerSelectedId( + recordsWithObjectNameSingular[0]?.value.id, + ); + } + }; + + return ( + + } + dropdownComponents={ + + relationObjectMetadataItem.nameSingular, + )} + recordPickerInstanceId={dropdownId} + onCancel={() => closeDropdown(dropdownId)} + layoutDirection={ + dropdownPlacement?.includes('end') + ? 'search-bar-on-bottom' + : 'search-bar-on-top' + } + /> + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdownOneToMany.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdownOneToMany.tsx new file mode 100644 index 0000000000..6e9935a2e0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailMorphRelationSectionDropdownOneToMany.tsx @@ -0,0 +1,182 @@ +import { useCallback, useContext } from 'react'; + +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { useGetMorphRelationRelatedRecordsWithObjectNameSingular } from '@/object-record/record-field-list/record-detail-section/relation/components/hooks/useGetMorphRelationRelatedRecordsWithObjectNameSingular'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { useUpdateMorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationOneToManyFieldInput'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; +import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; +import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen'; +import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch'; +import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; +import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState'; +import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState'; +import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; +import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { CustomError, isDefined } from 'twenty-shared/utils'; +import { IconPlus } from 'twenty-ui/display'; +import { LightIconButton } from 'twenty-ui/input'; + +export const RecordDetailMorphRelationSectionDropdownOneToMany = () => { + const { recordId, fieldDefinition } = useContext(FieldContext); + + assertFieldMetadata( + FieldMetadataType.MORPH_RELATION, + isFieldMorphRelation, + fieldDefinition, + ); + + const { morphRelations } = fieldDefinition.metadata; + const { objectMetadataItems } = useObjectMetadataItems(); + + const relationObjectMetadataItems = morphRelations + .map((morphRelation) => morphRelation.targetObjectMetadata) + .map((relationObjectMetadataItem) => + objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.id === relationObjectMetadataItem.id, + ), + ) + .filter(isDefined); + + const recordsWithObjectNameSingular = + useGetMorphRelationRelatedRecordsWithObjectNameSingular({ + recordId, + morphRelations: morphRelations, + }); + + const dropdownId = getRecordFieldCardRelationPickerDropdownId({ + fieldDefinition, + recordId, + }); + + const { closeDropdown } = useCloseDropdown(); + + const dropdownPlacement = useRecoilComponentValue( + dropdownPlacementComponentState, + dropdownId, + ); + + const setMultipleRecordPickerSearchFilter = useSetRecoilComponentState( + multipleRecordPickerSearchFilterComponentState, + dropdownId, + ); + + const setMultipleRecordPickerPickableMorphItems = useSetRecoilComponentState( + multipleRecordPickerPickableMorphItemsComponentState, + dropdownId, + ); + + const setMultipleRecordPickerSearchableObjectMetadataItems = + useSetRecoilComponentState( + multipleRecordPickerSearchableObjectMetadataItemsComponentState, + dropdownId, + ); + + const { performSearch: multipleRecordPickerPerformSearch } = + useMultipleRecordPickerPerformSearch(); + + const { openMultipleRecordPicker } = useMultipleRecordPickerOpen(); + + const handleCloseRelationPickerDropdown = useCallback(() => { + setMultipleRecordPickerSearchFilter(''); + }, [setMultipleRecordPickerSearchFilter]); + + const handleOpenRelationPickerDropdown = () => { + setMultipleRecordPickerSearchableObjectMetadataItems( + relationObjectMetadataItems, + ); + setMultipleRecordPickerSearchFilter(''); + setMultipleRecordPickerPickableMorphItems( + recordsWithObjectNameSingular.map((record) => { + const objectMetadataId = morphRelations.find( + (morphRelation) => + morphRelation.targetObjectMetadata.nameSingular === + record.objectNameSingular, + )?.targetObjectMetadata.id; + if (!objectMetadataId) { + throw new CustomError( + 'Sorry, we could not determine the correct object type for this related record. Please contact support if the issue persists.', + ); + } + return { + recordId: record.value.id, + objectMetadataId: objectMetadataId, + isSelected: true, + isMatchingSearchFilter: true, + }; + }), + ); + + openMultipleRecordPicker(dropdownId); + + multipleRecordPickerPerformSearch({ + multipleRecordPickerInstanceId: dropdownId, + forceSearchFilter: '', + forceSearchableObjectMetadataItems: relationObjectMetadataItems, + forcePickableMorphItems: recordsWithObjectNameSingular.map((record) => { + const objectMetadataId = morphRelations.find( + (morphRelation) => + morphRelation.targetObjectMetadata.nameSingular === + record.objectNameSingular, + )?.targetObjectMetadata.id; + if (!objectMetadataId) { + throw new Error('ObjectMetadataId is required'); + } + return { + recordId: record.value.id, + objectMetadataId: objectMetadataId, + isSelected: true, + isMatchingSearchFilter: true, + }; + }), + }); + }; + + const { updateMorphRelationOneToMany } = + useUpdateMorphRelationOneToManyFieldInput(); + + return ( + + } + dropdownComponents={ + { + updateMorphRelationOneToMany(morphItem); + }} + onSubmit={() => { + closeDropdown(dropdownId); + }} + onClickOutside={() => { + closeDropdown(dropdownId); + }} + layoutDirection={ + dropdownPlacement?.includes('end') + ? 'search-bar-on-bottom' + : 'search-bar-on-top' + } + dropdownWidth={GenericDropdownContentWidth.ExtraLarge} + /> + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsList.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsList.tsx index ac02ca7494..ee350ce568 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsList.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsList.tsx @@ -6,12 +6,16 @@ import { RecordDetailRelationRecordsListItemEffect } from '@/object-record/recor import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; type RecordDetailRelationRecordsListProps = { - relationRecords: ObjectRecord[]; + objectNameSingular: string; + value: ObjectRecord; + fieldMetadataId: string; }; export const RecordDetailRelationRecordsList = ({ - relationRecords, -}: RecordDetailRelationRecordsListProps) => { + recordsWithObjectNameSingular, +}: { + recordsWithObjectNameSingular: RecordDetailRelationRecordsListProps[]; +}) => { const [expandedItem, setExpandedItem] = useState(''); const handleItemClick = (recordId: string) => @@ -19,20 +23,35 @@ export const RecordDetailRelationRecordsList = ({ return ( - {relationRecords.slice(0, 5).map((relationRecord) => ( - - - - - ))} + {recordsWithObjectNameSingular + .slice(0, 5) + .map((recordWithObjectNameSingular) => ( + + + + + ))} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx index 14b8b8651c..1346356b9f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx @@ -4,6 +4,7 @@ import { motion } from 'framer-motion'; import { useCallback, useContext } from 'react'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { RecordChip } from '@/object-record/components/RecordChip'; @@ -30,6 +31,10 @@ import { useIsInRightDrawerOrThrow } from '@/ui/layout/right-drawer/contexts/Rig import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { createPortal } from 'react-dom'; +import { + computeMorphRelationFieldName, + CustomError, +} from 'twenty-shared/utils'; import { IconChevronDown, IconDotsVertical, @@ -40,7 +45,7 @@ import { import { LightIconButton } from 'twenty-ui/input'; import { MenuItem } from 'twenty-ui/navigation'; import { AnimatedEaseInOut } from 'twenty-ui/utilities'; -import { RelationType } from '~/generated-metadata/graphql'; +import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; const StyledListItem = styled(RecordDetailRecordsListItemContainer)<{ isDropdownOpen?: boolean; @@ -84,12 +89,16 @@ type RecordDetailRelationRecordsListItemProps = { isExpanded: boolean; onClick: (relationRecordId: string) => void; relationRecord: ObjectRecord; + relationObjectMetadataNameSingular: string; + relationFieldMetadataId: string; }; export const RecordDetailRelationRecordsListItem = ({ isExpanded, onClick, relationRecord, + relationObjectMetadataNameSingular, + relationFieldMetadataId, }: RecordDetailRelationRecordsListItemProps) => { const { fieldDefinition, @@ -100,12 +109,23 @@ export const RecordDetailRelationRecordsListItem = ({ const { onSubmit } = useContext(FieldInputEventContext); const { openModal } = useModal(); + const { isInRightDrawer } = useIsInRightDrawerOrThrow(); - const { - relationFieldMetadataId, - relationObjectMetadataNameSingular, - relationType, - } = fieldDefinition.metadata as FieldRelationMetadata; + const { relationType, objectMetadataNameSingular } = + fieldDefinition.metadata as FieldRelationMetadata; + + const { objectMetadataItems } = useObjectMetadataItems(); + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItemToFind) => + objectMetadataItemToFind.nameSingular === objectMetadataNameSingular, + ); + + if (!objectMetadataItem) { + throw new CustomError( + 'Object metadata item not found', + 'OBJECT_METADATA_ITEM_NOT_FOUND', + ); + } const isToOneObject = relationType === RelationType.MANY_TO_ONE; const { objectMetadataItem: relationObjectMetadataItem } = @@ -153,6 +173,27 @@ export const RecordDetailRelationRecordsListItem = ({ ({ id }) => id === relationFieldMetadataId, ); + const relationFieldMetadataIsMorphRelation = + relationFieldMetadataItem?.type === FieldMetadataType.MORPH_RELATION; + + const computedName = relationFieldMetadataItem + ? computeMorphRelationFieldName({ + fieldName: relationFieldMetadataItem.name, + relationType: relationFieldMetadataItem.settings.relationType, + targetObjectMetadataNameSingular: objectMetadataItem.nameSingular, + targetObjectMetadataNamePlural: objectMetadataItem.namePlural, + }) + : ''; + + const updateOneRecordInput = relationFieldMetadataIsMorphRelation + ? { + [getForeignKeyNameFromRelationFieldName(computedName)]: null, + } + : { + [getForeignKeyNameFromRelationFieldName( + relationFieldMetadataItem?.name ?? '', + )]: null, + }; const handleDetach = () => { closeDropdown(dropdownInstanceId); @@ -163,11 +204,7 @@ export const RecordDetailRelationRecordsListItem = ({ } else { updateOneRelationRecord({ idToUpdate: relationRecord.id, - updateOneRecordInput: { - [getForeignKeyNameFromRelationFieldName( - relationFieldMetadataItem.name, - )]: null, - }, + updateOneRecordInput, }); } @@ -199,8 +236,6 @@ export const RecordDetailRelationRecordsListItem = ({ [isExpanded], ); - const { isInRightDrawer } = useIsInRightDrawerOrThrow(); - return ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItemEffect.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItemEffect.tsx index 88358779f3..54e1ef830d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItemEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItemEffect.tsx @@ -1,24 +1,19 @@ -import { useContext, useEffect } from 'react'; +import { useEffect } from 'react'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; -import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { isDefined } from 'twenty-shared/utils'; type RecordDetailRelationRecordsListItemEffectProps = { relationRecordId: string; + relationObjectMetadataNameSingular: string; }; export const RecordDetailRelationRecordsListItemEffect = ({ relationRecordId, + relationObjectMetadataNameSingular, }: RecordDetailRelationRecordsListItemEffectProps) => { - const { fieldDefinition } = useContext(FieldContext); - - const { relationObjectMetadataNameSingular } = - fieldDefinition.metadata as FieldRelationMetadata; - const { record } = useFindOneRecord({ objectNameSingular: relationObjectMetadataNameSingular, objectRecordId: relationRecordId, diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSection.tsx index 6f03493f8f..eb6efc551d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSection.tsx @@ -25,10 +25,16 @@ import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/sta import { useLingui } from '@lingui/react/macro'; import { AppPath, + FieldMetadataType, ViewFilterOperand, type RecordGqlOperationFilter, } from 'twenty-shared/types'; -import { getAppPath } from 'twenty-shared/utils'; +import { + computeMorphRelationFieldName, + CustomError, + getAppPath, + isDefined, +} from 'twenty-shared/utils'; import { RelationType } from '~/generated-metadata/graphql'; type RecordDetailRelationSectionProps = { @@ -60,6 +66,19 @@ export const RecordDetailRelationSection = ({ ({ id }) => id === relationFieldMetadataId, ); + const { objectMetadataItems } = useObjectMetadataItems(); + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItemToFind) => + objectMetadataItemToFind.nameSingular === objectMetadataNameSingular, + ); + + if (!objectMetadataItem) { + throw new CustomError( + 'Object metadata item not found', + 'OBJECT_METADATA_ITEM_NOT_FOUND', + ); + } + const fieldValue = useRecoilValue< ({ id: string } & Record) | ObjectRecord[] | null >(recordStoreFamilySelector({ recordId, fieldName })); @@ -108,9 +127,29 @@ export const RecordDetailRelationSection = ({ filterQueryParams, ); + const persistField = usePersistField({ + objectMetadataItemId: objectMetadataItem?.id ?? '', + }); + + const relationFieldMetadataIsMorphRelation = + relationFieldMetadataItem?.type === FieldMetadataType.MORPH_RELATION; + + const computedName = isDefined(relationFieldMetadataItem) + ? computeMorphRelationFieldName({ + fieldName: relationFieldMetadataItem.name, + relationType: relationFieldMetadataItem.settings.relationType, + targetObjectMetadataNameSingular: objectMetadataItem.nameSingular, + targetObjectMetadataNamePlural: objectMetadataItem.namePlural, + }) + : undefined; + + const gqlFieldName = relationFieldMetadataIsMorphRelation + ? computedName + : relationFieldMetadataItem?.name; + const filtersForAggregate = isToManyObjects ? ({ - [`${relationFieldMetadataItem?.name}Id`]: { + [`${gqlFieldName}Id`]: { in: [recordId], }, } satisfies RecordGqlOperationFilter) @@ -127,19 +166,6 @@ export const RecordDetailRelationSection = ({ }, }); - // TODO: refactor this when we have refactored columnDefinitions and field definitions because - // we should be able to get the objectMetadataItem from a context way more easily - const { objectMetadataItems } = useObjectMetadataItems(); - - const objectMetadataItem = objectMetadataItems.find( - (objectMetadataItemToFind) => - objectMetadataItemToFind.nameSingular === objectMetadataNameSingular, - ); - - const persistField = usePersistField({ - objectMetadataItemId: objectMetadataItem?.id ?? '', - }); - const handleSubmit: FieldInputEvent = ({ newValue }) => { persistField({ recordId: recordId, @@ -178,7 +204,15 @@ export const RecordDetailRelationSection = ({ } > {relationRecords.length > 0 && ( - + ({ + value: relationRecord, + objectNameSingular: relationObjectMetadataNameSingular, + fieldMetadataId: relationFieldMetadataId, + }), + )} + /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx index d2d972b0ce..4c38b28053 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx @@ -2,8 +2,11 @@ import { useCallback, useContext } from 'react'; import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; +import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput'; import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker'; import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen'; @@ -19,18 +22,32 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { CustomError } from 'twenty-shared/utils'; import { IconPlus } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; -import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput'; export const RecordDetailRelationSectionDropdownToMany = () => { const { recordId, fieldDefinition } = useContext(FieldContext); + const { fieldMetadataId } = fieldDefinition; const { fieldName, relationFieldMetadataId, relationObjectMetadataNameSingular, } = fieldDefinition.metadata as FieldRelationMetadata; + const { objectMetadataItems } = useObjectMetadataItems(); + const { fieldMetadataItem, objectMetadataItem } = getFieldMetadataItemById({ + fieldMetadataId, + objectMetadataItems, + }); + + if (!fieldMetadataItem || !objectMetadataItem) { + throw new CustomError( + 'Field metadata item or object metadata item not found', + 'FIELD_METADATA_ITEM_OR_OBJECT_METADATA_ITEM_NOT_FOUND', + ); + } + const { objectMetadataItem: relationObjectMetadataItem } = useObjectMetadataItem({ objectNameSingular: relationObjectMetadataNameSingular, @@ -39,6 +56,12 @@ export const RecordDetailRelationSectionDropdownToMany = () => { const relationFieldMetadataItem = relationObjectMetadataItem.fields.find( ({ id }) => id === relationFieldMetadataId, ); + if (!relationFieldMetadataItem) { + throw new CustomError( + 'Relation field metadata item not found', + 'RELATION_FIELD_METADATA_ITEM_NOT_FOUND', + ); + } const fieldValue = useRecoilValue< ({ id: string } & Record) | ObjectRecord[] | null @@ -87,6 +110,8 @@ export const RecordDetailRelationSectionDropdownToMany = () => { const { createNewRecordAndOpenRightDrawer } = useAddNewRecordAndOpenRightDrawer({ + fieldMetadataItem, + objectMetadataItem, relationObjectMetadataNameSingular, relationObjectMetadataItem, relationFieldMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx index e99456949c..2017218c3d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx @@ -6,7 +6,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; -import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen'; import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; @@ -20,16 +19,42 @@ import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; + +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { CustomError } from 'twenty-shared/utils'; import { IconForbid, IconPencil } from 'twenty-ui/display'; import { LightIconButton } from 'twenty-ui/input'; export const RecordDetailRelationSectionDropdownToOne = () => { const { recordId, fieldDefinition } = useContext(FieldContext); + assertFieldMetadata( + FieldMetadataType.RELATION, + isFieldRelation, + fieldDefinition, + ); + const { fieldMetadataId } = fieldDefinition; const { fieldName, relationFieldMetadataId, relationObjectMetadataNameSingular, - } = fieldDefinition.metadata as FieldRelationMetadata; + } = fieldDefinition.metadata; + + const { objectMetadataItems } = useObjectMetadataItems(); + const { fieldMetadataItem, objectMetadataItem } = getFieldMetadataItemById({ + fieldMetadataId, + objectMetadataItems, + }); + + if (!fieldMetadataItem || !objectMetadataItem) { + throw new CustomError( + 'Field metadata item or object metadata item not found', + 'FIELD_METADATA_ITEM_OR_OBJECT_METADATA_ITEM_NOT_FOUND', + ); + } const { objectMetadataItem: relationObjectMetadataItem } = useObjectMetadataItem({ @@ -40,6 +65,13 @@ export const RecordDetailRelationSectionDropdownToOne = () => { ({ id }) => id === relationFieldMetadataId, ); + if (!relationFieldMetadataItem) { + throw new CustomError( + 'Relation field metadata item not found', + 'RELATION_FIELD_METADATA_ITEM_NOT_FOUND', + ); + } + const fieldValue = useRecoilValue< ({ id: string } & Record) | ObjectRecord[] | null >(recordStoreFamilySelector({ recordId, fieldName })); @@ -89,6 +121,8 @@ export const RecordDetailRelationSectionDropdownToOne = () => { const { createNewRecordAndOpenRightDrawer } = useAddNewRecordAndOpenRightDrawer({ + fieldMetadataItem, + objectMetadataItem, relationObjectMetadataNameSingular, relationObjectMetadataItem, relationFieldMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/hooks/useGetMorphRelationRelatedRecordsWithObjectNameSingular.ts b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/hooks/useGetMorphRelationRelatedRecordsWithObjectNameSingular.ts new file mode 100644 index 0000000000..f3b1f00aad --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/hooks/useGetMorphRelationRelatedRecordsWithObjectNameSingular.ts @@ -0,0 +1,61 @@ +import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation'; +import { recordStoreMorphOneToManyValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector'; +import { useRecoilValue } from 'recoil'; +import { CustomError, isNonEmptyArray } from 'twenty-shared/utils'; + +export const useGetMorphRelationRelatedRecordsWithObjectNameSingular = ({ + recordId, + morphRelations, +}: { + recordId: string; + morphRelations: FieldMetadataItemRelation[]; +}) => { + const morphRelationObjectNameSingularWithValues = useRecoilValue( + recordStoreMorphOneToManyValueWithObjectNameFamilySelector({ + recordId, + morphRelations, + }), + ); + + if (morphRelations.length < 1) { + throw new CustomError( + 'Cannot load relation records: no morph relations were provided to this component.', + 'MORPH_RELATIONS_REQUIRED', + ); + } + + const morphRelationObjectNameSingularWithValuesArray = + morphRelationObjectNameSingularWithValues + .map((recordWithObjectNameSingular) => ({ + ...recordWithObjectNameSingular, + value: Array.isArray(recordWithObjectNameSingular.value) + ? recordWithObjectNameSingular.value + : recordWithObjectNameSingular.value + ? [recordWithObjectNameSingular.value] + : [], + })) + .filter((recordWithObjectNameSingular) => + isNonEmptyArray(recordWithObjectNameSingular.value), + ); + + const recordsWithObjectNameSingular = + morphRelationObjectNameSingularWithValuesArray.flatMap( + (recordWithObjectNameSingular) => { + const fieldMetadataId = morphRelations.find( + (morphRelation) => + morphRelation.targetObjectMetadata.nameSingular === + recordWithObjectNameSingular.objectNameSingular, + )?.targetFieldMetadata.id; + if (!fieldMetadataId) { + return []; + } + + return recordWithObjectNameSingular.value.map((value) => ({ + objectNameSingular: recordWithObjectNameSingular.objectNameSingular, + value: value, + fieldMetadataId, + })); + }, + ); + return recordsWithObjectNameSingular; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts index 440c2cc24d..509328cdb8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/hooks/usePersistField.ts @@ -190,10 +190,28 @@ export const usePersistField = ({ .getValue(); if (fieldIsRelationManyToOne) { - if ( - fieldIsRelationManyToOne && - valueToPersist?.id === currentValue?.id - ) { + if (valueToPersist?.id === currentValue?.id) { + return; + } + + const newRecord = await updateOneRecord?.({ + idToUpdate: recordId, + updateOneRecordInput: { + [getForeignKeyNameFromRelationFieldName(fieldName)]: + valueToPersist?.id ?? null, + }, + }); + + upsertRecordsInStore([ + getRecordFromRecordNode({ + recordNode: newRecord, + }), + ]); + return; + } + + if (fieldIsMorphRelationManyToOne) { + if (valueToPersist?.id === currentValue?.id) { return; } diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay.tsx index 0ad4ae0214..ccd923a82a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay.tsx @@ -3,6 +3,7 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFocus'; import { useMorphRelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useMorphRelationFromManyFieldDisplay'; +import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; import { Fragment, useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; @@ -24,19 +25,21 @@ export const MorphRelationOneToManyFieldDisplay = () => { .map((morphValueWithObjectNameSingular) => { return ( - {morphValueWithObjectNameSingular.value.map((record) => { - return ( - - ); - })} + {morphValueWithObjectNameSingular.value.map( + (record: ObjectRecord) => { + return ( + + ); + }, + )} ); })} diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx index 622723430c..132d088d0d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx @@ -2,6 +2,8 @@ import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts import { useRelationField } from '../../hooks/useRelationField'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById'; import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState'; @@ -15,13 +17,24 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import { useLingui } from '@lingui/react/macro'; import { useContext } from 'react'; -import { isDefined } from 'twenty-shared/utils'; +import { CustomError, isDefined } from 'twenty-shared/utils'; import { IconForbid } from 'twenty-ui/display'; export const RelationManyToOneFieldInput = () => { const { t } = useLingui(); const { fieldDefinition, recordId } = useRelationField(); + const { objectMetadataItems } = useObjectMetadataItems(); + const { fieldMetadataItem, objectMetadataItem } = getFieldMetadataItemById({ + fieldMetadataId: fieldDefinition.fieldMetadataId, + objectMetadataItems, + }); + if (!fieldMetadataItem || !objectMetadataItem) { + throw new CustomError( + 'Field metadata item or object metadata item not found', + 'FIELD_METADATA_ITEM_OR_OBJECT_METADATA_ITEM_NOT_FOUND', + ); + } const { onSubmit, onCancel } = useContext(FieldInputEventContext); const instanceId = useAvailableComponentInstanceIdOrThrow( @@ -46,9 +59,17 @@ export const RelationManyToOneFieldInput = () => { const relationFieldMetadataItem = relationObjectMetadataItem.fields.find( ({ id }) => id === fieldDefinition.metadata.relationFieldMetadataId, ); + if (!relationFieldMetadataItem) { + throw new CustomError( + 'Relation field metadata item not found', + 'RELATION_FIELD_METADATA_ITEM_NOT_FOUND', + ); + } const { createNewRecordAndOpenRightDrawer } = useAddNewRecordAndOpenRightDrawer({ + fieldMetadataItem, + objectMetadataItem, relationObjectMetadataNameSingular: fieldDefinition.metadata.relationObjectMetadataNameSingular, relationObjectMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput.tsx index 6d30151eba..b051dee978 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput.tsx @@ -5,7 +5,9 @@ import { useUpdateActivityTargetFromCell } from '@/activities/inline-cell/hooks/ import { type NoteTarget } from '@/activities/types/NoteTarget'; import { type TaskTarget } from '@/activities/types/TaskTarget'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; import { useRelationField } from '@/object-record/record-field/ui/meta-types/hooks/useRelationField'; @@ -22,7 +24,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; +import { CustomError, isDefined } from 'twenty-shared/utils'; export const RelationOneToManyFieldInput = () => { const { fieldDefinition, recordId } = useContext(FieldContext); @@ -34,6 +36,17 @@ export const RelationOneToManyFieldInput = () => { const { updateRelation } = useUpdateRelationOneToManyFieldInput(); const fieldName = fieldDefinition.metadata.fieldName; + const { objectMetadataItems } = useObjectMetadataItems(); + const { fieldMetadataItem, objectMetadataItem } = getFieldMetadataItemById({ + fieldMetadataId: fieldDefinition.fieldMetadataId, + objectMetadataItems, + }); + if (!fieldMetadataItem || !objectMetadataItem) { + throw new CustomError( + 'Field metadata item or object metadata item not found', + 'FIELD_METADATA_ITEM_OR_OBJECT_METADATA_ITEM_NOT_FOUND', + ); + } const objectMetadataNameSingular = fieldDefinition.metadata.objectMetadataNameSingular; @@ -73,9 +86,17 @@ export const RelationOneToManyFieldInput = () => { const relationFieldMetadataItem = relationObjectMetadataItem.fields.find( ({ id }) => id === relationFieldDefinition.metadata.relationFieldMetadataId, ); + if (!relationFieldMetadataItem) { + throw new CustomError( + 'Relation field metadata item not found', + 'RELATION_FIELD_METADATA_ITEM_NOT_FOUND', + ); + } const { createNewRecordAndOpenRightDrawer } = useAddNewRecordAndOpenRightDrawer({ + fieldMetadataItem, + objectMetadataItem, relationObjectMetadataNameSingular: relationFieldDefinition.metadata.relationObjectMetadataNameSingular, relationObjectMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationManyToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationManyToOneFieldInput.stories.tsx index 993dd1b55b..9f713dd5e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationManyToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/__stories__/RelationManyToOneFieldInput.stories.tsx @@ -81,7 +81,7 @@ const RelationManyToOneFieldInputWithContext = ({ { const fieldDefinition = useMemo( () => ({ - fieldMetadataId: 'relation', + fieldMetadataId: 'e82262eb-7f58-4167-a23c-fc51ec584d1b', label: 'People', type: FieldMetadataType.RELATION, iconName: 'IconLink', @@ -53,7 +53,7 @@ const RelationOneToManyFieldInputWithContext = () => { relationObjectMetadataNamePlural: 'companies', relationObjectMetadataNameSingular: CoreObjectNameSingular.Company, objectMetadataNameSingular: 'person', - relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e', + relationFieldMetadataId: '3c211c59-02a1-4904-ad0f-5bb30b736461', }, }), [], diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts index aff98748d4..35138db34f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer.ts @@ -8,26 +8,31 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; + +import { useUpdateOneRecordV2 } from '@/object-record/hooks/useUpdateOneRecordV2'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { getOperationName } from '@apollo/client/utilities'; -import { isDefined } from 'twenty-shared/utils'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql'; -type RecordDetailRelationSectionProps = { +type useAddNewRecordAndOpenRightDrawerProps = { + fieldMetadataItem: FieldMetadataItem; + objectMetadataItem: ObjectMetadataItem; relationObjectMetadataNameSingular: string; relationObjectMetadataItem: ObjectMetadataItem; - relationFieldMetadataItem?: FieldMetadataItem; + relationFieldMetadataItem: FieldMetadataItem; recordId: string; }; export const useAddNewRecordAndOpenRightDrawer = ({ + fieldMetadataItem, + objectMetadataItem, relationObjectMetadataNameSingular, relationObjectMetadataItem, relationFieldMetadataItem, recordId, -}: RecordDetailRelationSectionProps) => { +}: useAddNewRecordAndOpenRightDrawerProps) => { const setViewableRecordId = useSetRecoilState(viewableRecordIdState); const setViewableRecordNameSingular = useSetRecoilState( viewableRecordNameSingularState, @@ -37,11 +42,7 @@ export const useAddNewRecordAndOpenRightDrawer = ({ objectNameSingular: relationObjectMetadataNameSingular, }); - const { updateOneRecord } = useUpdateOneRecord({ - objectNameSingular: - relationFieldMetadataItem?.relation?.targetObjectMetadata.nameSingular ?? - 'workspaceMember', - }); + const { updateOneRecord } = useUpdateOneRecordV2(); const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); @@ -49,14 +50,14 @@ export const useAddNewRecordAndOpenRightDrawer = ({ if ( relationObjectMetadataNameSingular === 'workspaceMember' || - !isDefined( - relationFieldMetadataItem?.relation?.targetObjectMetadata.nameSingular, - ) + !isDefined(objectMetadataItem.nameSingular) ) { return { createNewRecordAndOpenRightDrawer: undefined, }; } + const relationFieldMetadataItemRelationType = + relationFieldMetadataItem.settings?.relationType; return { createNewRecordAndOpenRightDrawer: async (searchInput?: string) => { @@ -84,24 +85,30 @@ export const useAddNewRecordAndOpenRightDrawer = ({ } : { id: newRecordId, name: searchInput ?? '' }; - if ( - relationFieldMetadataItem?.relation?.type === RelationType.MANY_TO_ONE - ) { - createRecordPayload[ - `${relationFieldMetadataItem?.relation?.sourceFieldMetadata.name}Id` - ] = recordId; + if (relationFieldMetadataItemRelationType === RelationType.MANY_TO_ONE) { + const gqlField = + relationFieldMetadataItem.type === FieldMetadataType.RELATION + ? relationFieldMetadataItem.name + : computeMorphRelationFieldName({ + fieldName: relationFieldMetadataItem.name, + relationType: relationFieldMetadataItemRelationType, + targetObjectMetadataNameSingular: + objectMetadataItem.nameSingular, + targetObjectMetadataNamePlural: objectMetadataItem.namePlural, + }); + + createRecordPayload[`${gqlField}Id`] = recordId; } await createOneRecord(createRecordPayload); - if ( - relationFieldMetadataItem?.relation?.type === RelationType.ONE_TO_MANY - ) { + if (relationFieldMetadataItemRelationType === RelationType.ONE_TO_MANY) { await updateOneRecord({ + objectNameSingular: + objectMetadataItem.nameSingular ?? 'workspaceMember', idToUpdate: recordId, updateOneRecordInput: { - [`${relationFieldMetadataItem?.relation?.targetFieldMetadata.name}Id`]: - newRecordId, + [`${fieldMetadataItem.name}Id`]: newRecordId, }, }); } diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne.ts index 139d91a6f6..ff9f83aa3a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne.ts @@ -111,7 +111,7 @@ export const useMorphPersistManyToOne = ({ return; }, - [updateOneRecord, objectMetadataItems, objectMetadataNameSingular], + [objectMetadataItems, updateOneRecord, objectMetadataNameSingular], ); return { persistMorphManyToOne }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput.tsx index cb8bef8249..c5d5a4f6f7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput.tsx @@ -14,6 +14,7 @@ import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFi import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; import { useRecoilCallback } from 'recoil'; +import { type ObjectRecord } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; export const useOpenMorphRelationOneToManyFieldInput = () => { @@ -83,12 +84,14 @@ export const useOpenMorphRelationOneToManyFieldInput = () => { return []; } - return recordWithObjectNameSingular.value.map((recordValue) => ({ - objectMetadataId: objectMetadataItem.id, - recordId: recordValue.id, - isSelected: true, - isMatchingSearchFilter: true, - })); + return recordWithObjectNameSingular.value.map( + (recordValue: ObjectRecord) => ({ + objectMetadataId: objectMetadataItem.id, + recordId: recordValue.id, + isSelected: true, + isMatchingSearchFilter: true, + }), + ); }, ); diff --git a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts index ade8333d96..cf3ae03970 100644 --- a/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector.ts @@ -32,7 +32,7 @@ export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector = objectNameSingular: morphRelation.targetObjectMetadata.nameSingular, value: (get(recordStoreFamilyState(recordId))?.[fieldName] || - []) as ObjectRecord[], + []) as ObjectRecord[] | ObjectRecord, }; }, ); diff --git a/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts b/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts index c3e2e1def1..5d0df98125 100644 --- a/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts +++ b/packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts @@ -4,7 +4,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue'; -import { isDefined } from 'twenty-shared/utils'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; import { FieldMetadataType, RelationType } from '~/generated/graphql'; type PrefillRecordArgs = { @@ -35,6 +35,28 @@ export const prefillRecord = ({ [fieldMetadataItem.settings?.joinColumnName, joinColumnValue], ]; } + if ( + fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION && + fieldMetadataItem.settings?.relationType === RelationType.MANY_TO_ONE + ) { + const gqlFields = fieldMetadataItem.morphRelations?.map( + (morphRelation) => { + return computeMorphRelationFieldName({ + fieldName: fieldMetadataItem.name, + relationType: morphRelation.type, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }); + }, + ); + + return gqlFields?.flatMap((gqlField) => [ + [gqlField, fieldValue], + [`${gqlField}Id`, input[`${gqlField}Id`] ?? null], + ]); + } return [[fieldMetadataItem.name, fieldValue]]; })