From 12189b378f4c7c98a7d1042661e4d6f7bb885d59 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 6 Jan 2026 16:26:26 +0100 Subject: [PATCH] Fix add related records to activity target after object renaming (#16953) Fixes https://github.com/twentyhq/twenty/issues/16928 and https://github.com/twentyhq/private-issues/issues/400 When an object is renamed, its corresponding relation field on noteTarget and taskTarget are not renamed. This caused an issue as in the FE, we used the related record object name to compute the field name used in the payload. This PR addresses it by, instead, finding the field name after identifying it thanks to the object id. To reproduce, you can rename an object then try to tie it to a note. --- .../useActivityTargetsForTargetableObjects.ts | 6 +- .../hooks/useOpenCreateActivityDrawer.ts | 30 ++++++++- .../hooks/useUpdateActivityTargetFromCell.ts | 34 +++++++--- .../getActivityTargetFieldNameForObject.ts | 38 +++++++++++ .../utils/getActivityTargetObjectRecords.ts | 39 +++++++++-- .../utils/getActivityTargetsFilter.ts | 65 +++++++++++++++---- ...ityTargetsToCreateFromTargetableObjects.ts | 43 ------------ 7 files changed, 178 insertions(+), 77 deletions(-) create mode 100644 packages/twenty-front/src/modules/activities/utils/getActivityTargetFieldNameForObject.ts delete mode 100644 packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts index c8637e77cb..2db1e69035 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetsForTargetableObjects.ts @@ -28,12 +28,14 @@ export const useActivityTargetsForTargetableObjects = ({ activityTargetsOrderByVariables: RecordGqlOperationOrderBy; limit: number; }) => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const activityTargetsFilter = getActivityTargetsFilter({ targetableObjects: targetableObjects, + activityObjectNameSingular: objectNameSingular, + objectMetadataItems, }); - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE = findActivityTargetsOperationSignatureFactory({ objectNameSingular, diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index 58f58c628d..200fa450d9 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -13,7 +13,9 @@ import { type NoteTarget } from '@/activities/types/NoteTarget'; import { type Task } from '@/activities/types/Task'; import { type TaskTarget } from '@/activities/types/TaskTarget'; import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { isDefined } from 'twenty-shared/utils'; export const useOpenCreateActivityDrawer = ({ activityObjectNameSingular, @@ -52,6 +54,8 @@ export const useOpenCreateActivityDrawer = ({ const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); + const { objectMetadataItems } = useObjectMetadataItems(); + const openCreateActivityDrawer = async ({ targetableObjects, customAssignee, @@ -72,7 +76,31 @@ export const useOpenCreateActivityDrawer = ({ }); if (targetableObjects.length > 0) { - const targetableObjectRelationIdName = `${targetableObjects[0].targetObjectNameSingular}Id`; + const activityTargetObjectNameSingular = + activityObjectNameSingular === CoreObjectNameSingular.Task + ? CoreObjectNameSingular.TaskTarget + : CoreObjectNameSingular.NoteTarget; + + const activityTargetObjectMetadata = objectMetadataItems.find( + (item) => item.nameSingular === activityTargetObjectNameSingular, + ); + + const targetObjectMetadataItem = objectMetadataItems.find( + (item) => + item.nameSingular === targetableObjects[0].targetObjectNameSingular, + ); + + const targetField = activityTargetObjectMetadata?.fields.find( + (field) => + field.relation?.targetObjectMetadata.id === + targetObjectMetadataItem?.id, + ); + + const joinColumnName = targetField?.settings?.joinColumnName; + + const targetableObjectRelationIdName = isDefined(joinColumnName) + ? joinColumnName + : `${targetableObjects[0].targetObjectNameSingular}Id`; await createOneActivityTarget({ ...(activityObjectNameSingular === CoreObjectNameSingular.Task diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts index d94417c3a0..6b777cbcf8 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts @@ -1,6 +1,7 @@ import { type ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject'; import { type NoteTarget } from '@/activities/types/NoteTarget'; import { type TaskTarget } from '@/activities/types/TaskTarget'; +import { getActivityTargetFieldNameForObject } from '@/activities/utils/getActivityTargetFieldNameForObject'; import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -65,18 +66,31 @@ export const useUpdateActivityTargetFromCell = ({ ? 'task' : 'note'; - const pickedObjectMetadataItem = snapshot + const objectMetadataItems = snapshot .getLoadable(objectMetadataItemsState) - .getValue() - .find( - (objectMetadataItem) => - objectMetadataItem.id === morphItem.objectMetadataId, - ); + .getValue(); + + const pickedObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.id === morphItem.objectMetadataId, + ); if (!isDefined(pickedObjectMetadataItem)) { throw new Error('Could not find object metadata item'); } + const targetFieldName = getActivityTargetFieldNameForObject({ + activityObjectNameSingular, + targetObjectMetadataId: morphItem.objectMetadataId, + objectMetadataItems, + }); + + if (!isDefined(targetFieldName)) { + throw new Error( + `Could not find field on activity target for object ${pickedObjectMetadataItem.nameSingular}`, + ); + } + let activityTargetsAfterUpdate: (TaskTarget | NoteTarget)[] = []; const existingActivityTarget = activityTargetWithTargetRecords.find( @@ -132,7 +146,7 @@ export const useUpdateActivityTargetFromCell = ({ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, - [pickedObjectMetadataItem.nameSingular]: targetRecord, + [targetFieldName]: targetRecord, } : { id: v4(), @@ -146,7 +160,7 @@ export const useUpdateActivityTargetFromCell = ({ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, - [pickedObjectMetadataItem.nameSingular]: targetRecord, + [targetFieldName]: targetRecord, }; activityTargetsAfterUpdate = [ @@ -159,8 +173,8 @@ export const useUpdateActivityTargetFromCell = ({ await createOneActivityTarget({ ...activityTarget, [targetObjectName]: undefined, - [pickedObjectMetadataItem.nameSingular]: undefined, - [`${pickedObjectMetadataItem.nameSingular}Id`]: morphItem.recordId, + [targetFieldName]: undefined, + [`${targetFieldName}Id`]: morphItem.recordId, } as Partial); } diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetFieldNameForObject.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetFieldNameForObject.ts new file mode 100644 index 0000000000..9793c5fa7b --- /dev/null +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetFieldNameForObject.ts @@ -0,0 +1,38 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isDefined } from 'twenty-shared/utils'; + +type GetActivityTargetFieldNameForObjectArgs = { + activityObjectNameSingular: + | CoreObjectNameSingular.Note + | CoreObjectNameSingular.Task; + targetObjectMetadataId: string; + objectMetadataItems: ObjectMetadataItem[]; +}; + +export const getActivityTargetFieldNameForObject = ({ + activityObjectNameSingular, + targetObjectMetadataId, + objectMetadataItems, +}: GetActivityTargetFieldNameForObjectArgs): string | undefined => { + const activityTargetObjectNameSingular = + activityObjectNameSingular === CoreObjectNameSingular.Task + ? CoreObjectNameSingular.TaskTarget + : CoreObjectNameSingular.NoteTarget; + + const activityTargetObjectMetadata = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === activityTargetObjectNameSingular, + ); + + if (!isDefined(activityTargetObjectMetadata)) { + return undefined; + } + + const targetField = activityTargetObjectMetadata.fields.find( + (field) => + field.relation?.targetObjectMetadata.id === targetObjectMetadataId, + ); + + return targetField?.name; +}; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectRecords.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectRecords.ts index 8623abb8c5..802ce23397 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectRecords.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetObjectRecords.ts @@ -24,6 +24,8 @@ export const getActivityTargetObjectRecords = ({ return []; } + const isNote = isDefined(activityRecord) && 'noteTargets' in activityRecord; + const targets = activityTargets ? activityTargets : activityRecord && @@ -36,27 +38,50 @@ export const getActivityTargetObjectRecords = ({ ? activityRecord.taskTargets : []; + const activityTargetObjectNameSingular = isNote + ? CoreObjectNameSingular.NoteTarget + : CoreObjectNameSingular.TaskTarget; + + const activityTargetObjectMetadata = objectMetadataItems.find( + (item) => item.nameSingular === activityTargetObjectNameSingular, + ); + + const activityTargetRelationFields = + activityTargetObjectMetadata?.fields.filter( + (field) => + isDefined(field.relation?.targetObjectMetadata.id) && + ![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes( + field.relation?.targetObjectMetadata + .nameSingular as CoreObjectNameSingular, + ), + ) ?? []; + const activityTargetObjectRecords = targets .map((activityTarget) => { if (!isDefined(activityTarget)) { throw new Error('Cannot find activity target'); } + const matchingField = activityTargetRelationFields.find((field) => + isDefined(activityTarget[field.name]), + ); + + if (!matchingField || !matchingField.relation) { + return undefined; + } const correspondingObjectMetadataItem = objectMetadataItems.find( (objectMetadataItem) => - isDefined(activityTarget[objectMetadataItem.nameSingular]) && - ![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes( - objectMetadataItem.nameSingular as CoreObjectNameSingular, - ), + objectMetadataItem.id === + matchingField.relation?.targetObjectMetadata.id, ); if (!correspondingObjectMetadataItem) { return undefined; } - const targetObjectRecord = activityTarget[ - correspondingObjectMetadataItem.nameSingular - ] as ObjectRecord | undefined; + const targetObjectRecord = activityTarget[matchingField.name] as + | ObjectRecord + | undefined; if (!isDefined(targetObjectRecord)) { throw new Error( diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts index 9b2fdf1c63..b73b724473 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsFilter.ts @@ -1,25 +1,62 @@ import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isDefined } from 'twenty-shared/utils'; export const getActivityTargetsFilter = ({ targetableObjects, + activityObjectNameSingular, + objectMetadataItems, }: { targetableObjects: ActivityTargetableObject[]; + activityObjectNameSingular: + | CoreObjectNameSingular.Note + | CoreObjectNameSingular.Task; + objectMetadataItems: ObjectMetadataItem[]; }) => { - const findManyActivitiyTargetsQueryFilter = Object.fromEntries( - targetableObjects.map((targetableObject) => { - const targetObjectFieldName = getActivityTargetObjectFieldIdName({ - nameSingular: targetableObject.targetObjectNameSingular, - }); + const activityTargetObjectNameSingular = + activityObjectNameSingular === CoreObjectNameSingular.Task + ? CoreObjectNameSingular.TaskTarget + : CoreObjectNameSingular.NoteTarget; - return [ - targetObjectFieldName, - { - eq: targetableObject.id, - }, - ]; - }), + const activityTargetObjectMetadata = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === activityTargetObjectNameSingular, ); - return findManyActivitiyTargetsQueryFilter; + const findManyActivityTargetsQueryFilter = Object.fromEntries( + targetableObjects + .map((targetableObject) => { + const targetObjectMetadataItem = objectMetadataItems.find( + (item) => + item.nameSingular === targetableObject.targetObjectNameSingular, + ); + + if (!isDefined(targetObjectMetadataItem)) { + return undefined; + } + + const targetField = activityTargetObjectMetadata?.fields.find( + (field) => + field.relation?.targetObjectMetadata.id === + targetObjectMetadataItem.id, + ); + + const joinColumnName = targetField?.settings?.joinColumnName; + + if (!isDefined(joinColumnName)) { + return undefined; + } + + return [ + joinColumnName, + { + eq: targetableObject.id, + }, + ]; + }) + .filter(isDefined), + ); + + return findManyActivityTargetsQueryFilter; }; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts b/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts deleted file mode 100644 index 3d086397df..0000000000 --- a/packages/twenty-front/src/modules/activities/utils/getActivityTargetsToCreateFromTargetableObjects.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { v4 } from 'uuid'; - -import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { type Note } from '@/activities/types/Note'; -import { type NoteTarget } from '@/activities/types/NoteTarget'; -import { type Task } from '@/activities/types/Task'; -import { type TaskTarget } from '@/activities/types/TaskTarget'; -import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; - -export const makeActivityTargetsToCreateFromTargetableObjects = ({ - targetableObjects, - activity, - targetObjectRecords, -}: { - targetableObjects: ActivityTargetableObject[]; - activity: Task | Note; - targetObjectRecords: ObjectRecord[]; -}): Partial[] => { - const activityTargetsToCreate = targetableObjects.map((targetableObject) => { - const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({ - nameSingular: targetableObject.targetObjectNameSingular, - }); - - const relatedObjectRecord = targetObjectRecords.find( - (record) => record.id === targetableObject.id, - ); - - const activityTarget = { - [targetableObject.targetObjectNameSingular]: relatedObjectRecord, - [targetableObjectFieldIdName]: targetableObject.id, - activity, - activityId: activity.id, - id: v4(), - updatedAt: new Date().toISOString(), - createdAt: new Date().toISOString(), - } as Partial; - - return activityTarget; - }); - - return activityTargetsToCreate; -};