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.
This commit is contained in:
+4
-2
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
+24
-10
@@ -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<NoteTarget | TaskTarget>);
|
||||
}
|
||||
|
||||
|
||||
+38
@@ -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;
|
||||
};
|
||||
+32
-7
@@ -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<ActivityTargetWithTargetRecord | undefined>((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(
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
-43
@@ -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<NoteTarget | TaskTarget>[] => {
|
||||
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<NoteTarget | TaskTarget>;
|
||||
|
||||
return activityTarget;
|
||||
});
|
||||
|
||||
return activityTargetsToCreate;
|
||||
};
|
||||
Reference in New Issue
Block a user