diff --git a/packages/twenty-front/src/modules/metadata-store/effect-components/MetadataStoreSSEEffect.tsx b/packages/twenty-front/src/modules/metadata-store/effect-components/MetadataStoreSSEEffect.tsx index db665dca5d..6e2513b14f 100644 --- a/packages/twenty-front/src/modules/metadata-store/effect-components/MetadataStoreSSEEffect.tsx +++ b/packages/twenty-front/src/modules/metadata-store/effect-components/MetadataStoreSSEEffect.tsx @@ -1,4 +1,5 @@ import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent'; +import { useCleanMorphRelationsTargetingObjectMetadataId } from '@/metadata-store/hooks/useCleanMorphRelationsTargetingObjectMetadataId'; import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState'; import { type MetadataEntityTypeMap } from '@/metadata-store/types/MetadataEntityTypeMap'; @@ -10,6 +11,8 @@ type AnyMetadataEntity = MetadataEntityTypeMap[MetadataEntityKey]; export const MetadataStoreSSEEffect = () => { const { addToDraft, removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft(); + const { cleanMorphRelations } = + useCleanMorphRelationsTargetingObjectMetadataId(); useListenToMetadataOperationBrowserEvent({ onMetadataOperationBrowserEvent: (eventDetail) => { @@ -50,6 +53,10 @@ export const MetadataStoreSSEEffect = () => { itemIds: [eventDetail.operation.deletedRecordId], collectionHash, }); + + if (entityKey === 'objectMetadataItems') { + cleanMorphRelations(eventDetail.operation.deletedRecordId); + } break; } } diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useCleanMorphRelationsTargetingObjectMetadataId.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useCleanMorphRelationsTargetingObjectMetadataId.ts new file mode 100644 index 0000000000..113af6db51 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/hooks/useCleanMorphRelationsTargetingObjectMetadataId.ts @@ -0,0 +1,38 @@ +import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; +import { metadataStoreState } from '@/metadata-store/states/metadataStoreState'; +import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; +import { cleanMorphRelationsTargetingObjectMetadataId } from '@/metadata-store/utils/cleanMorphRelationsTargetingObjectMetadataId'; +import { useStore } from 'jotai'; +import { useCallback } from 'react'; + +export const useCleanMorphRelationsTargetingObjectMetadataId = () => { + const store = useStore(); + const { updateInDraft } = useUpdateMetadataStoreDraft(); + + const cleanMorphRelations = useCallback( + (deletedObjectMetadataId: string) => { + const entry = store.get( + metadataStoreState.atomFamily('fieldMetadataItems'), + ); + + const baseFieldMetadataItems = ( + entry.status === 'draft-pending' ? entry.draft : entry.current + ) as FlatFieldMetadataItem[]; + + const cleanedFieldMetadataItems = + cleanMorphRelationsTargetingObjectMetadataId( + baseFieldMetadataItems, + deletedObjectMetadataId, + ); + + if (cleanedFieldMetadataItems.length === 0) { + return; + } + + updateInDraft('fieldMetadataItems', cleanedFieldMetadataItems); + }, + [store, updateInDraft], + ); + + return { cleanMorphRelations }; +}; diff --git a/packages/twenty-front/src/modules/metadata-store/utils/__tests__/cleanMorphRelationsTargetingObjectMetadataId.test.ts b/packages/twenty-front/src/modules/metadata-store/utils/__tests__/cleanMorphRelationsTargetingObjectMetadataId.test.ts new file mode 100644 index 0000000000..2dce0e4cf3 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/utils/__tests__/cleanMorphRelationsTargetingObjectMetadataId.test.ts @@ -0,0 +1,66 @@ +import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; +import { cleanMorphRelationsTargetingObjectMetadataId } from '@/metadata-store/utils/cleanMorphRelationsTargetingObjectMetadataId'; + +const buildMorphRelation = (targetObjectMetadataId: string) => + ({ + targetObjectMetadata: { + id: targetObjectMetadataId, + nameSingular: targetObjectMetadataId, + namePlural: `${targetObjectMetadataId}s`, + }, + }) as unknown as NonNullable[number]; + +const buildField = ( + id: string, + morphRelations: FlatFieldMetadataItem['morphRelations'], +): FlatFieldMetadataItem => + ({ + id, + name: id, + objectMetadataId: 'source-object', + morphRelations, + }) as unknown as FlatFieldMetadataItem; + +describe('cleanMorphRelationsTargetingObjectMetadataId', () => { + it('should remove morph relations targeting the deleted object and return only changed fields', () => { + const morphField = buildField('morph-field', [ + buildMorphRelation('company-id'), + buildMorphRelation('meeting-id'), + ]); + const untouchedField = buildField('other-morph-field', [ + buildMorphRelation('company-id'), + ]); + const nonMorphField = buildField('text-field', null); + + const result = cleanMorphRelationsTargetingObjectMetadataId( + [morphField, untouchedField, nonMorphField], + 'meeting-id', + ); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('morph-field'); + expect(result[0].morphRelations).toEqual([ + buildMorphRelation('company-id'), + ]); + }); + + it('should keep a morph field with an empty morphRelations array when all its targets are deleted', () => { + const result = cleanMorphRelationsTargetingObjectMetadataId( + [buildField('exclusive-morph', [buildMorphRelation('meeting-id')])], + 'meeting-id', + ); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe('exclusive-morph'); + expect(result[0].morphRelations).toEqual([]); + }); + + it('should return an empty array when no morph relation targets the deleted object', () => { + const result = cleanMorphRelationsTargetingObjectMetadataId( + [buildField('morph-field', [buildMorphRelation('company-id')])], + 'meeting-id', + ); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/twenty-front/src/modules/metadata-store/utils/cleanMorphRelationsTargetingObjectMetadataId.ts b/packages/twenty-front/src/modules/metadata-store/utils/cleanMorphRelationsTargetingObjectMetadataId.ts new file mode 100644 index 0000000000..f7a0238061 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-store/utils/cleanMorphRelationsTargetingObjectMetadataId.ts @@ -0,0 +1,33 @@ +import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem'; +import { isDefined } from 'twenty-shared/utils'; + +export const cleanMorphRelationsTargetingObjectMetadataId = ( + fieldMetadataItems: FlatFieldMetadataItem[], + deletedObjectMetadataId: string, +): FlatFieldMetadataItem[] => { + const cleanedFieldMetadataItems: FlatFieldMetadataItem[] = []; + + for (const fieldMetadataItem of fieldMetadataItems) { + const morphRelations = fieldMetadataItem.morphRelations; + + if ( + !isDefined(morphRelations) || + !morphRelations.some( + (morphRelation) => + morphRelation.targetObjectMetadata.id === deletedObjectMetadataId, + ) + ) { + continue; + } + + cleanedFieldMetadataItems.push({ + ...fieldMetadataItem, + morphRelations: morphRelations.filter( + (morphRelation) => + morphRelation.targetObjectMetadata.id !== deletedObjectMetadataId, + ), + }); + } + + return cleanedFieldMetadataItems; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts index 601778ea0a..aec2bd2c56 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts @@ -2,6 +2,7 @@ import { useMutation } from '@apollo/client/react'; import { DeleteOneObjectMetadataItemDocument } from '~/generated-metadata/graphql'; import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore'; +import { useCleanMorphRelationsTargetingObjectMetadataId } from '@/metadata-store/hooks/useCleanMorphRelationsTargetingObjectMetadataId'; import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft'; import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; @@ -19,6 +20,8 @@ export const useDeleteOneObjectMetadataItem = () => { const { enqueueErrorSnackBar } = useSnackBar(); const { removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft(); const { invalidateMetadataStore } = useInvalidateMetadataStore(); + const { cleanMorphRelations } = + useCleanMorphRelationsTargetingObjectMetadataId(); const deleteOneObjectMetadataItem = async ( idToDelete: string, @@ -35,6 +38,7 @@ export const useDeleteOneObjectMetadataItem = () => { }); removeFromDraft({ key: 'objectMetadataItems', itemIds: [idToDelete] }); + cleanMorphRelations(idToDelete); applyChanges(); invalidateMetadataStore();