first step in the show record page with a morph relation. (#15109)
Adds the field on the Show page and side panel for Morph relation fields. Updates `packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput.tsx` to use the correct `recordPickerInstanceId` when opening the picker so the helper consistently renders (fixes [core-team-issues #1324](https://github.com/twentyhq/core-team-issues/issues/1324)). --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
+1
@@ -36,6 +36,7 @@ export const useCreateOneRecordInCache = <T extends ObjectRecord>({
|
||||
record: prefilledRecord,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
const fragment = gql`
|
||||
fragment Create${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
|
||||
+45
-30
@@ -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) => (
|
||||
<FieldContext.Provider
|
||||
key={objectRecordId + fieldMetadataItem.id}
|
||||
value={{
|
||||
recordId: objectRecordId,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: index,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isDisplayModeFixHeight: true,
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly,
|
||||
objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
{boxedRelationFieldMetadataItems
|
||||
.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION ||
|
||||
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
.map((fieldMetadataItem, index) => (
|
||||
<FieldContext.Provider
|
||||
key={objectRecordId + fieldMetadataItem.id}
|
||||
value={{
|
||||
recordId: objectRecordId,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
|
||||
field: fieldMetadataItem,
|
||||
position: index,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
fieldMetadataItem: {
|
||||
id: fieldMetadataItem.id,
|
||||
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordDetailRelationSection
|
||||
loading={isPrefetchLoading || recordLoading}
|
||||
/>
|
||||
</FieldContext.Provider>
|
||||
))}
|
||||
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 ? (
|
||||
<RecordDetailMorphRelationSection
|
||||
loading={isPrefetchLoading || recordLoading}
|
||||
/>
|
||||
) : (
|
||||
<RecordDetailRelationSection
|
||||
loading={isPrefetchLoading || recordLoading}
|
||||
/>
|
||||
)}
|
||||
</FieldContext.Provider>
|
||||
))}
|
||||
|
||||
<RecordFieldListCellHoveredPortal
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
|
||||
+33
-18
@@ -45,7 +45,8 @@ export const useFieldListFieldMetadataItems = ({
|
||||
(fieldMetadataItem.name !== 'createdAt' &&
|
||||
fieldMetadataItem.name !== 'deletedAt')) &&
|
||||
(showRelationSections ||
|
||||
fieldMetadataItem.type !== FieldMetadataType.RELATION),
|
||||
(fieldMetadataItem.type !== FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.type !== FieldMetadataType.MORPH_RELATION)),
|
||||
)
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
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,
|
||||
|
||||
+131
@@ -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 (
|
||||
<FieldInputEventContext.Provider
|
||||
value={{
|
||||
onSubmit: handleSubmit,
|
||||
}}
|
||||
>
|
||||
<RecordDetailSectionContainer
|
||||
title={fieldDefinition.label}
|
||||
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
|
||||
areRecordsAvailable={relationRecordsCount > 0}
|
||||
rightAdornment={
|
||||
<RecordDetailMorphRelationSectionDropdown loading={loading} />
|
||||
}
|
||||
>
|
||||
{relationRecordsCount > 0 && (
|
||||
<RecordDetailRelationRecordsList
|
||||
recordsWithObjectNameSingular={recordsWithObjectNameSingular}
|
||||
/>
|
||||
)}
|
||||
</RecordDetailSectionContainer>
|
||||
</FieldInputEventContext.Provider>
|
||||
);
|
||||
};
|
||||
+73
@@ -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 <RecordDetailMorphRelationSectionDropdownManyToOne />;
|
||||
} else if (isToManyObjects) {
|
||||
return <RecordDetailMorphRelationSectionDropdownOneToMany />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
+142
@@ -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 (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="left-start"
|
||||
onClose={handleCloseRelationPickerDropdown}
|
||||
onOpen={handleOpenRelationPickerDropdown}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
Icon={IconPencil}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<SingleRecordPicker
|
||||
focusId={dropdownId}
|
||||
componentInstanceId={dropdownId}
|
||||
EmptyIcon={IconForbid}
|
||||
onMorphItemSelected={handleRelationPickerEntitySelected}
|
||||
objectNameSingulars={relationObjectMetadataItems.map(
|
||||
(relationObjectMetadataItem) =>
|
||||
relationObjectMetadataItem.nameSingular,
|
||||
)}
|
||||
recordPickerInstanceId={dropdownId}
|
||||
onCancel={() => closeDropdown(dropdownId)}
|
||||
layoutDirection={
|
||||
dropdownPlacement?.includes('end')
|
||||
? 'search-bar-on-bottom'
|
||||
: 'search-bar-on-top'
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+182
@@ -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 (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="left-start"
|
||||
onClose={handleCloseRelationPickerDropdown}
|
||||
onOpen={handleOpenRelationPickerDropdown}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
Icon={IconPlus}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<MultipleRecordPicker
|
||||
focusId={dropdownId}
|
||||
componentInstanceId={dropdownId}
|
||||
onChange={(morphItem) => {
|
||||
updateMorphRelationOneToMany(morphItem);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
onClickOutside={() => {
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
layoutDirection={
|
||||
dropdownPlacement?.includes('end')
|
||||
? 'search-bar-on-bottom'
|
||||
: 'search-bar-on-top'
|
||||
}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+36
-17
@@ -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 (
|
||||
<RecordDetailRecordsListContainer>
|
||||
{relationRecords.slice(0, 5).map((relationRecord) => (
|
||||
<Fragment key={relationRecord.id}>
|
||||
<RecordDetailRelationRecordsListItemEffect
|
||||
key={`${relationRecord.id}-effect`}
|
||||
relationRecordId={relationRecord.id}
|
||||
/>
|
||||
<RecordDetailRelationRecordsListItem
|
||||
key={relationRecord.id}
|
||||
isExpanded={expandedItem === relationRecord.id}
|
||||
onClick={handleItemClick}
|
||||
relationRecord={relationRecord}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
{recordsWithObjectNameSingular
|
||||
.slice(0, 5)
|
||||
.map((recordWithObjectNameSingular) => (
|
||||
<Fragment
|
||||
key={`${recordWithObjectNameSingular.value.id}-${recordWithObjectNameSingular.fieldMetadataId}`}
|
||||
>
|
||||
<RecordDetailRelationRecordsListItemEffect
|
||||
key={`${recordWithObjectNameSingular.value.id}-effect`}
|
||||
relationRecordId={recordWithObjectNameSingular.value.id}
|
||||
relationObjectMetadataNameSingular={
|
||||
recordWithObjectNameSingular.objectNameSingular
|
||||
}
|
||||
/>
|
||||
<RecordDetailRelationRecordsListItem
|
||||
key={recordWithObjectNameSingular.value.id}
|
||||
isExpanded={
|
||||
expandedItem === recordWithObjectNameSingular.value.id
|
||||
}
|
||||
onClick={handleItemClick}
|
||||
relationRecord={recordWithObjectNameSingular.value}
|
||||
relationObjectMetadataNameSingular={
|
||||
recordWithObjectNameSingular.objectNameSingular
|
||||
}
|
||||
relationFieldMetadataId={
|
||||
recordWithObjectNameSingular.fieldMetadataId
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</RecordDetailRecordsListContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+48
-13
@@ -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 (
|
||||
<>
|
||||
<StyledListItem isDropdownOpen={isDropdownOpen}>
|
||||
|
||||
+3
-8
@@ -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,
|
||||
|
||||
+50
-16
@@ -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<string, any>) | 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 && (
|
||||
<RecordDetailRelationRecordsList relationRecords={relationRecords} />
|
||||
<RecordDetailRelationRecordsList
|
||||
recordsWithObjectNameSingular={relationRecords.map(
|
||||
(relationRecord) => ({
|
||||
value: relationRecord,
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
fieldMetadataId: relationFieldMetadataId,
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</RecordDetailSectionContainer>
|
||||
</FieldInputEventContext.Provider>
|
||||
|
||||
+26
-1
@@ -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<string, any>) | ObjectRecord[] | null
|
||||
@@ -87,6 +110,8 @@ export const RecordDetailRelationSectionDropdownToMany = () => {
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
useAddNewRecordAndOpenRightDrawer({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationObjectMetadataItem,
|
||||
relationFieldMetadataItem,
|
||||
|
||||
+36
-2
@@ -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<string, any>) | ObjectRecord[] | null
|
||||
>(recordStoreFamilySelector({ recordId, fieldName }));
|
||||
@@ -89,6 +121,8 @@ export const RecordDetailRelationSectionDropdownToOne = () => {
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
useAddNewRecordAndOpenRightDrawer({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataNameSingular,
|
||||
relationObjectMetadataItem,
|
||||
relationFieldMetadataItem,
|
||||
|
||||
+61
@@ -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;
|
||||
};
|
||||
+22
-4
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+16
-13
@@ -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 (
|
||||
<Fragment key={morphValueWithObjectNameSingular.objectNameSingular}>
|
||||
{morphValueWithObjectNameSingular.value.map((record) => {
|
||||
return (
|
||||
<RecordChip
|
||||
key={record.id}
|
||||
objectNameSingular={
|
||||
morphValueWithObjectNameSingular.objectNameSingular
|
||||
}
|
||||
record={record}
|
||||
forceDisableClick={disableChipClick}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{morphValueWithObjectNameSingular.value.map(
|
||||
(record: ObjectRecord) => {
|
||||
return (
|
||||
<RecordChip
|
||||
key={record.id}
|
||||
objectNameSingular={
|
||||
morphValueWithObjectNameSingular.objectNameSingular
|
||||
}
|
||||
record={record}
|
||||
forceDisableClick={disableChipClick}
|
||||
triggerEvent={triggerEvent}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
+22
-1
@@ -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<ObjectRecord>();
|
||||
|
||||
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,
|
||||
|
||||
+22
-1
@@ -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,
|
||||
|
||||
+3
-3
@@ -81,7 +81,7 @@ const RelationManyToOneFieldInputWithContext = ({
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
fieldDefinition: {
|
||||
fieldMetadataId: 'relation',
|
||||
fieldMetadataId: 'e82262eb-7f58-4167-a23c-fc51ec584d1b',
|
||||
label: 'Relation',
|
||||
type: FieldMetadataType.RELATION,
|
||||
iconName: 'IconLink',
|
||||
@@ -90,9 +90,9 @@ const RelationManyToOneFieldInputWithContext = ({
|
||||
relationObjectMetadataNamePlural: 'companies',
|
||||
relationObjectMetadataNameSingular:
|
||||
CoreObjectNameSingular.Company,
|
||||
relationObjectMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e',
|
||||
relationObjectMetadataId: '4a45f524-b8cb-40e8-8450-28e402b442cf',
|
||||
objectMetadataNameSingular: 'person',
|
||||
relationFieldMetadataId: '20202020-8c37-4163-ba06-1dada334ce3e',
|
||||
relationFieldMetadataId: '3c211c59-02a1-4904-ad0f-5bb30b736461',
|
||||
},
|
||||
},
|
||||
recordId: recordId,
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ const RelationOneToManyFieldInputWithContext = () => {
|
||||
|
||||
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',
|
||||
},
|
||||
}),
|
||||
[],
|
||||
|
||||
+31
-24
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ export const useMorphPersistManyToOne = ({
|
||||
|
||||
return;
|
||||
},
|
||||
[updateOneRecord, objectMetadataItems, objectMetadataNameSingular],
|
||||
[objectMetadataItems, updateOneRecord, objectMetadataNameSingular],
|
||||
);
|
||||
|
||||
return { persistMorphManyToOne };
|
||||
|
||||
+9
-6
@@ -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,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector =
|
||||
objectNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
value: (get(recordStoreFamilyState(recordId))?.[fieldName] ||
|
||||
[]) as ObjectRecord[],
|
||||
[]) as ObjectRecord[] | ObjectRecord,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 = <T extends ObjectRecord>({
|
||||
[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]];
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user