Morph many to one picker (#14155)

This PR follows the multiSelect PR merged previously. It will enable
morph relation Many to One to be handled from the table, using a
singleSelect picker

Main point : I decided to change the singleSelect API to take an array
of **objectMetadataName** instead of only one to deal with both our
usecases.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Guillim
2025-10-07 17:25:11 +02:00
committed by GitHub
parent 9f9c2d85ca
commit f60817a1e1
138 changed files with 2116 additions and 1763 deletions
@@ -1,187 +0,0 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateMultipleRecordsFromManyObjects } from '@/object-record/hooks/useUpdateMultipleRecordsFromManyObjects';
import { getTargetFieldMetadataName } from '@/object-record/multiple-objects/utils/getTargetFieldMetadataName';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useAttachMorphRelatedRecordFromRecord = () => {
const apolloCoreClient = useApolloCoreClient();
const { fieldDefinition } = useContext(FieldContext);
const { objectMetadataItems } = useObjectMetadataItems();
const { updateMultipleRecordsFromManyObjects } =
useUpdateMultipleRecordsFromManyObjects();
if (!isFieldMorphRelation(fieldDefinition)) {
throw new Error('Field is not a morph relation');
}
const objectNameSingular =
fieldDefinition.metadata.morphRelations[0].sourceObjectMetadata
.nameSingular;
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (!isDefined(objectMetadataItem)) {
throw new Error('Could not find object metadata item');
}
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateOneRecordAndAttachMorphRelations = useRecoilCallback(
({ set, snapshot }) =>
async ({
recordId,
relatedRecordId,
objectNameSingulars,
}: {
recordId: string;
relatedRecordId: string;
objectNameSingulars: string[];
}) => {
const relatedObjectMetadataItems = objectMetadataItems.filter(
(objectMetadataItem) =>
objectNameSingulars.includes(objectMetadataItem.nameSingular),
);
const parentRecord = snapshot
.getLoadable(recordStoreFamilyState(recordId))
.getValue();
if (isDefined(parentRecord)) {
relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => {
const currentMorphFieldValue =
parentRecord[fieldDefinition.metadata.fieldName];
const objectRecordFromCache = getRecordFromCache({
objectMetadataItem: relatedObjectMetadataItem,
recordId: relatedRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
if (!isDefined(objectRecordFromCache)) {
return;
}
set(recordStoreFamilyState(recordId), {
...parentRecord,
[fieldDefinition.metadata.fieldName]: [
...currentMorphFieldValue,
objectRecordFromCache,
],
});
});
}
const relatedObjectMetadataItemsWithCachedRecord =
relatedObjectMetadataItems
.map((objectMetadataItem) => {
const cachedRelatedRecord = getRecordFromCache<ObjectRecord>({
objectMetadataItem,
recordId: relatedRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
return {
cachedRelatedRecord,
objectMetadataItem,
};
})
.find((item) => isDefined(item.cachedRelatedRecord));
if (!relatedObjectMetadataItemsWithCachedRecord) {
throw new Error('Could not find cached related record');
}
const {
objectMetadataItem: relatedObjectMetadataItem,
cachedRelatedRecord,
} = relatedObjectMetadataItemsWithCachedRecord;
if (
!isDefined(cachedRelatedRecord) ||
!isDefined(cachedRelatedRecord.id)
) {
throw new Error('Could not find cached related record');
}
const fieldOnRelatedObject = getTargetFieldMetadataName({
fieldDefinition,
objectNameSingular: relatedObjectMetadataItem.nameSingular,
});
if (!isDefined(fieldOnRelatedObject)) {
throw new Error('Could not find field on related object');
}
const previousRecordId =
cachedRelatedRecord?.[`${fieldOnRelatedObject}Id`];
if (isDefined(previousRecordId)) {
const previousRecord = getRecordFromCache<ObjectRecord>({
objectMetadataItem,
recordId: previousRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
const previousRecordWithRelation = {
...cachedRelatedRecord,
[fieldOnRelatedObject]: previousRecord,
};
const gqlFields = computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem: relatedObjectMetadataItem,
record: previousRecordWithRelation,
});
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem: relatedObjectMetadataItem,
cache: apolloCoreClient.cache,
record: {
...cachedRelatedRecord,
[fieldOnRelatedObject]: previousRecord,
},
recordGqlFields: gqlFields,
objectPermissionsByObjectMetadataId,
});
}
const updatedManyRecordsArgs = [
{
idToUpdate: relatedRecordId,
objectNameSingulars,
relatedRecordId: recordId,
// recordGqlFields: gqlFields,
},
];
await updateMultipleRecordsFromManyObjects(updatedManyRecordsArgs);
},
[
fieldDefinition,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
updateMultipleRecordsFromManyObjects,
apolloCoreClient.cache,
objectMetadataItem,
],
);
return { updateOneRecordAndAttachMorphRelations };
};
@@ -1,118 +0,0 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { CustomError, isDefined } from 'twenty-shared/utils';
type useAttachRelatedRecordFromRecordProps = {
recordObjectNameSingular: string;
fieldNameOnRecordObject: string;
};
export const useAttachRelatedRecordFromRecord = ({
recordObjectNameSingular,
fieldNameOnRecordObject,
}: useAttachRelatedRecordFromRecordProps) => {
const apolloCoreClient = useApolloCoreClient();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: recordObjectNameSingular,
});
const fieldOnObject = objectMetadataItem.readableFields.find((field) => {
return field.name === fieldNameOnRecordObject;
});
const relatedRecordObjectNameSingular =
fieldOnObject?.relation?.targetObjectMetadata.nameSingular;
if (!relatedRecordObjectNameSingular) {
throw new CustomError(
`Could not find record related to ${recordObjectNameSingular}`,
'RELATED_RECORD_NOT_FOUND',
);
}
const { objectMetadataItem: relatedObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: relatedRecordObjectNameSingular,
});
const fieldOnRelatedObject =
fieldOnObject?.relation?.targetFieldMetadata.name;
if (!fieldOnRelatedObject) {
throw new CustomError(
`Missing target field for ${fieldNameOnRecordObject}`,
'MISSING_TARGET_FIELD',
);
}
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: relatedRecordObjectNameSingular,
});
const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: recordObjectNameSingular,
});
const getRelatedRecordFromCache = useGetRecordFromCache({
objectNameSingular: relatedRecordObjectNameSingular,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateOneRecordAndAttachRelations = async ({
recordId,
relatedRecordId,
}: {
recordId: string;
relatedRecordId: string;
}) => {
const cachedRelatedRecord =
getRelatedRecordFromCache<ObjectRecord>(relatedRecordId);
if (!cachedRelatedRecord) {
throw new Error('Could not find cached related record');
}
const previousRecordId = cachedRelatedRecord?.[`${fieldOnRelatedObject}Id`];
if (isDefined(previousRecordId)) {
const previousRecord = getRecordFromCache<ObjectRecord>(previousRecordId);
const previousRecordWithRelation = {
...cachedRelatedRecord,
[fieldOnRelatedObject]: previousRecord,
};
const gqlFields = computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem: relatedObjectMetadataItem,
record: previousRecordWithRelation,
});
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem: relatedObjectMetadataItem,
cache: apolloCoreClient.cache,
record: {
...cachedRelatedRecord,
[fieldOnRelatedObject]: previousRecord,
},
recordGqlFields: gqlFields,
objectPermissionsByObjectMetadataId,
});
}
await updateOneRecord({
idToUpdate: relatedRecordId,
updateOneRecordInput: {
[`${fieldOnRelatedObject}Id`]: recordId,
},
});
};
return { updateOneRecordAndAttachRelations };
};
@@ -1,170 +0,0 @@
import { type Reference } from '@apollo/client';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRefName } from '@/object-record/cache/utils/getRefName';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateMultipleRecordsFromManyObjects } from '@/object-record/hooks/useUpdateMultipleRecordsFromManyObjects';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const useDetachMorphRelatedRecordFromRecord = () => {
const apolloCoreClient = useApolloCoreClient();
const { fieldDefinition } = useContext(FieldContext);
const { objectMetadataItems } = useObjectMetadataItems();
const { updateMultipleRecordsFromManyObjects } =
useUpdateMultipleRecordsFromManyObjects();
if (!isFieldMorphRelation(fieldDefinition)) {
throw new Error('Field is not a morph relation');
}
const objectNameSingular =
fieldDefinition.metadata.morphRelations[0].sourceObjectMetadata
.nameSingular;
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (!isDefined(objectMetadataItem)) {
throw new Error('Could not find object metadata item');
}
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateOneRecordAndDetachMorphRelations = useRecoilCallback(
({ set, snapshot }) =>
async ({
recordId,
relatedRecordId,
objectNameSingulars,
}: {
recordId: string;
relatedRecordId: string;
objectNameSingulars: string[];
}) => {
const relatedObjectMetadataItems = objectMetadataItems.filter(
(objectMetadataItem) =>
objectNameSingulars.includes(objectMetadataItem.nameSingular),
);
const parentRecord = snapshot
.getLoadable(recordStoreFamilyState(recordId))
.getValue();
if (isDefined(parentRecord)) {
relatedObjectMetadataItems.forEach((relatedObjectMetadataItem) => {
const currentMorphFieldValue = parentRecord[
fieldDefinition.metadata.fieldName
] as ObjectRecord[];
const objectRecordFromCache = getRecordFromCache({
objectMetadataItem: relatedObjectMetadataItem,
recordId: relatedRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
if (!isDefined(objectRecordFromCache)) {
return;
}
if (Array.isArray(currentMorphFieldValue)) {
set(recordStoreFamilyState(recordId), {
...parentRecord,
[fieldDefinition.metadata.fieldName]:
currentMorphFieldValue.filter(
(record) => record.id !== relatedRecordId,
),
});
}
});
}
const relatedObjectMetadataItemsWithCachedRecord =
relatedObjectMetadataItems
.map((objectMetadataItem) => {
const cachedRelatedRecord = getRecordFromCache<ObjectRecord>({
objectMetadataItem,
recordId: relatedRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
return {
cachedRelatedRecord,
objectMetadataItem,
};
})
.find((item) => isDefined(item.cachedRelatedRecord));
if (!relatedObjectMetadataItemsWithCachedRecord) {
throw new Error('Could not find cached related record');
}
const { objectMetadataItem: relatedObjectMetadataItem } =
relatedObjectMetadataItemsWithCachedRecord;
modifyRecordFromCache({
objectMetadataItem,
cache: apolloCoreClient.cache,
fieldModifiers: {
[fieldDefinition.metadata.fieldName]: (
fieldNameOnRecordObjectConnection,
{ readField },
) => {
const edges = readField<{ node: Reference }[]>(
'edges',
fieldNameOnRecordObjectConnection,
);
if (!edges) return fieldNameOnRecordObjectConnection;
return {
...fieldNameOnRecordObjectConnection,
edges: edges.filter(
(edge) =>
!(
edge.node.__ref ===
getRefName(
relatedObjectMetadataItem.nameSingular,
relatedRecordId,
)
),
),
};
},
},
recordId,
});
const updatedManyRecordsArgs = [
{
idToUpdate: relatedRecordId,
objectNameSingulars,
relatedRecordId: null,
},
];
await updateMultipleRecordsFromManyObjects(updatedManyRecordsArgs);
},
[
fieldDefinition,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
updateMultipleRecordsFromManyObjects,
apolloCoreClient.cache,
objectMetadataItem,
],
);
return { updateOneRecordAndDetachMorphRelations };
};
@@ -1,89 +0,0 @@
import { type Reference } from '@apollo/client';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getRefName } from '@/object-record/cache/utils/getRefName';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
type useDetachRelatedRecordFromRecordProps = {
recordObjectNameSingular: string;
fieldNameOnRecordObject: string;
};
export const useDetachRelatedRecordFromRecord = ({
recordObjectNameSingular,
fieldNameOnRecordObject,
}: useDetachRelatedRecordFromRecordProps) => {
const apolloCoreClient = useApolloCoreClient();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: recordObjectNameSingular,
});
const fieldOnObject = objectMetadataItem.readableFields.find((field) => {
return field.name === fieldNameOnRecordObject;
});
const relatedRecordObjectNameSingular =
fieldOnObject?.relation?.targetObjectMetadata.nameSingular;
const fieldOnRelatedObject =
fieldOnObject?.relation?.targetFieldMetadata.name;
if (!relatedRecordObjectNameSingular) {
throw new Error(
`Could not find record related to ${recordObjectNameSingular}`,
);
}
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: relatedRecordObjectNameSingular,
});
const updateOneRecordAndDetachRelations = async ({
recordId,
relatedRecordId,
}: {
recordId: string;
relatedRecordId: string;
}) => {
modifyRecordFromCache({
objectMetadataItem,
cache: apolloCoreClient.cache,
fieldModifiers: {
[fieldNameOnRecordObject]: (
fieldNameOnRecordObjectConnection,
{ readField },
) => {
const edges = readField<{ node: Reference }[]>(
'edges',
fieldNameOnRecordObjectConnection,
);
if (!edges) return fieldNameOnRecordObjectConnection;
return {
...fieldNameOnRecordObjectConnection,
edges: edges.filter(
(edge) =>
!(
edge.node.__ref ===
getRefName(relatedRecordObjectNameSingular, relatedRecordId)
),
),
};
},
},
recordId,
});
await updateOneRecord({
idToUpdate: relatedRecordId,
updateOneRecordInput: {
[`${fieldOnRelatedObject}Id`]: null,
},
});
};
return { updateOneRecordAndDetachRelations };
};
@@ -0,0 +1,111 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordV2 } from '@/object-record/hooks/useUpdateOneRecordV2';
import { CustomError, isDefined } from 'twenty-shared/utils';
export const useRecordOneToManyFieldAttachTargetRecord = () => {
const apolloCoreClient = useApolloCoreClient();
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { updateOneRecord } = useUpdateOneRecordV2();
const recordOneToManyFieldAttachTargetRecord = async ({
sourceObjectNameSingular,
targetObjectNameSingular,
targetGQLFieldName,
sourceRecordId,
targetRecordId,
}: {
sourceObjectNameSingular: string;
targetObjectNameSingular: string;
targetGQLFieldName: string;
sourceRecordId: string;
targetRecordId: string;
}) => {
const targetObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === targetObjectNameSingular,
);
const sourceObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === sourceObjectNameSingular,
);
if (!targetObjectMetadataItem) {
throw new CustomError(
`Could not find related object metadata for ${targetObjectNameSingular}`,
'RELATED_OBJECT_METADATA_NOT_FOUND',
);
}
if (!sourceObjectMetadataItem) {
throw new CustomError(
`Could not find source object metadata for ${sourceObjectNameSingular}`,
'SOURCE_OBJECT_METADATA_NOT_FOUND',
);
}
const cachedTargetRecord = getRecordFromCache({
objectMetadataItem: targetObjectMetadataItem,
recordId: targetRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
if (!cachedTargetRecord) {
throw new Error('Could not find cached related record');
}
const previousRecordId = cachedTargetRecord?.[`${targetGQLFieldName}Id`];
if (isDefined(previousRecordId)) {
const previousRecord = getRecordFromCache({
objectMetadataItem: sourceObjectMetadataItem,
recordId: previousRecordId,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
const previousRecordWithRelation = {
...cachedTargetRecord,
[targetGQLFieldName]: previousRecord,
};
const gqlFields = computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem: targetObjectMetadataItem,
record: previousRecordWithRelation,
});
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem: targetObjectMetadataItem,
cache: apolloCoreClient.cache,
record: {
...cachedTargetRecord,
[targetGQLFieldName]: previousRecord,
},
recordGqlFields: gqlFields,
objectPermissionsByObjectMetadataId,
});
}
await updateOneRecord({
objectNameSingular: targetObjectNameSingular,
idToUpdate: targetRecordId,
updateOneRecordInput: {
[`${targetGQLFieldName}Id`]: sourceRecordId,
},
});
};
return { recordOneToManyFieldAttachTargetRecord };
};
@@ -0,0 +1,92 @@
import { type Reference } from '@apollo/client';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getRefName } from '@/object-record/cache/utils/getRefName';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { useUpdateOneRecordV2 } from '@/object-record/hooks/useUpdateOneRecordV2';
import { CustomError } from 'twenty-shared/utils';
export const useRecordOneToManyFieldDetachTargetRecord = () => {
const apolloCoreClient = useApolloCoreClient();
const { objectMetadataItems } = useObjectMetadataItems();
const { updateOneRecord } = useUpdateOneRecordV2();
const recordOneToManyFieldDetachTargetRecord = async ({
sourceObjectNameSingular,
targetObjectNameSingular,
targetGQLFieldName,
sourceRecordId,
targetRecordId,
}: {
sourceObjectNameSingular: string;
targetObjectNameSingular: string;
targetGQLFieldName: string;
sourceRecordId: string;
targetRecordId: string;
}) => {
const targetObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === targetObjectNameSingular,
);
const sourceObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === sourceObjectNameSingular,
);
if (!targetObjectMetadataItem) {
throw new CustomError(
`Could not find related object metadata for ${targetObjectNameSingular}`,
'RELATED_OBJECT_METADATA_NOT_FOUND',
);
}
if (!sourceObjectMetadataItem) {
throw new CustomError(
`Could not find source object metadata for ${sourceObjectNameSingular}`,
'SOURCE_OBJECT_METADATA_NOT_FOUND',
);
}
modifyRecordFromCache({
objectMetadataItem: sourceObjectMetadataItem,
cache: apolloCoreClient.cache,
fieldModifiers: {
[targetGQLFieldName]: (
fieldNameOnRecordObjectConnection,
{ readField },
) => {
const edges = readField<{ node: Reference }[]>(
'edges',
fieldNameOnRecordObjectConnection,
);
if (!edges) return fieldNameOnRecordObjectConnection;
return {
...fieldNameOnRecordObjectConnection,
edges: edges.filter(
(edge) =>
!(
edge.node.__ref ===
getRefName(targetObjectNameSingular, targetRecordId)
),
),
};
},
},
recordId: sourceRecordId,
});
await updateOneRecord({
objectNameSingular: targetObjectNameSingular,
idToUpdate: targetRecordId,
updateOneRecordInput: {
[`${targetGQLFieldName}Id`]: null,
},
});
};
return { recordOneToManyFieldDetachTargetRecord };
};
@@ -1,284 +0,0 @@
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { generateUpdateOneRecordMutation } from '@/object-record/multiple-objects/utils/generateUpdateOneRecordMutation';
import { getTargetFieldMetadataName } from '@/object-record/multiple-objects/utils/getTargetFieldMetadataName';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { getAggregateQueryName } from '@/object-record/utils/getAggregateQueryName';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isNull } from '@sniptt/guards';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { CustomError, isDefined } from 'twenty-shared/utils';
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
type UpdateManyRecordArgs = {
idToUpdate: string;
relatedRecordId: string | null;
objectNameSingulars: string[];
recordGqlFields?: Record<string, any>;
};
export const useUpdateMultipleRecordsFromManyObjects = () => {
const { fieldDefinition } = useContext(FieldContext);
const apolloCoreClient = useApolloCoreClient();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateMultipleRecordsFromManyObjects = async (
updatedManyRecordsArgs: UpdateManyRecordArgs[],
) => {
for (const {
idToUpdate,
objectNameSingulars,
relatedRecordId,
recordGqlFields,
} of updatedManyRecordsArgs) {
const objectMetadataItemArray = objectMetadataItems.filter(
(objectMetadataItem) =>
objectNameSingulars.includes(objectMetadataItem.nameSingular),
);
if (objectMetadataItemArray.length === 0) {
throw new CustomError(
`Object metadata item not found ${objectNameSingulars.join(', ')}`,
'OBJECT_METADATA_ITEM_NOT_FOUND',
);
}
const objectMetadataItemWithCachedRecord = objectMetadataItemArray
.map((objectMetadataItem) => {
const cachedRecord = getRecordFromCache({
objectMetadataItem,
recordId: idToUpdate,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
return {
cachedRecord,
objectMetadataItem,
};
})
.find((item) => isDefined(item.cachedRecord));
if (!isDefined(objectMetadataItemWithCachedRecord)) {
throw new CustomError(
`Record not found ${idToUpdate}`,
'RECORD_NOT_FOUND_IN_OBJECT_METADATA_ITEM',
);
}
const { objectMetadataItem, cachedRecord } =
objectMetadataItemWithCachedRecord;
if (
!isFieldRelation(fieldDefinition) &&
!isFieldMorphRelation(fieldDefinition)
) {
throw new CustomError(
`Should never happen`,
'TARGET_FIELD_NAME_NOT_FOUND',
);
}
const targetFieldName = getTargetFieldMetadataName({
fieldDefinition,
objectNameSingular: objectMetadataItem.nameSingular,
});
if (!isDefined(targetFieldName)) {
throw new CustomError(
`Cannot find Target field name for the (morph) relation field ${fieldDefinition.metadata.fieldName} on ${objectMetadataItem.nameSingular}`,
'TARGET_FIELD_NAME_NOT_FOUND',
);
}
const updateOneRecordInput = {
[`${targetFieldName}Id`]: relatedRecordId,
};
const optimisticRecordInput = computeOptimisticRecordFromInput({
objectMetadataItem,
currentWorkspaceMember: currentWorkspaceMember,
recordInput: updateOneRecordInput,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
const computedRecordGqlFields =
recordGqlFields ??
generateDepthOneRecordGqlFields({ objectMetadataItem });
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
record: cachedRecord,
objectMetadataItem,
objectMetadataItems,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
});
const computedOptimisticRecord = {
...cachedRecord,
...optimisticRecordInput,
id: idToUpdate,
__typename: getObjectTypename(objectMetadataItem.nameSingular),
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord,
objectMetadataItem,
objectMetadataItems,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
});
const shouldHandleOptimisticCache =
!isNull(cachedRecord) &&
isDefined(optimisticRecordWithConnection) &&
isDefined(cachedRecordWithConnection);
if (shouldHandleOptimisticCache) {
const recordGqlFields = computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem,
record: optimisticRecordInput,
});
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloCoreClient.cache,
record: computedOptimisticRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloCoreClient.cache,
objectMetadataItem,
currentRecord: cachedRecordWithConnection,
updatedRecord: optimisticRecordWithConnection,
objectMetadataItems,
});
}
const mutationResponseField = getUpdateOneRecordMutationResponseField(
objectMetadataItem.nameSingular,
);
const sanitizedInput = {
...sanitizeRecordInput({
objectMetadataItem,
recordInput: updateOneRecordInput,
}),
};
const updateOneRecordMutation = generateUpdateOneRecordMutation({
objectMetadataItem,
objectMetadataItems,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
objectPermissionsByObjectMetadataId,
});
const updatedRecord = await apolloCoreClient
.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate,
input: sanitizedInput,
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
if (!isDefined(record)) return;
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
currentRecord: computedOptimisticRecord,
updatedRecord: record,
objectMetadataItems,
});
},
})
.catch((error: Error) => {
if (!shouldHandleOptimisticCache) {
throw error;
}
const cachedRecordKeys = new Set(Object.keys(cachedRecord));
const recordKeysAddedByOptimisticCache = Object.keys(
optimisticRecordInput,
).filter((diffKey) => !cachedRecordKeys.has(diffKey));
const recordGqlFields = {
...computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem,
record: cachedRecord,
}),
...buildRecordFromKeysWithSameValue(
recordKeysAddedByOptimisticCache,
true,
),
};
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloCoreClient.cache,
record: {
...cachedRecord,
...buildRecordFromKeysWithSameValue(
recordKeysAddedByOptimisticCache,
null,
),
},
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloCoreClient.cache,
objectMetadataItem,
currentRecord: optimisticRecordWithConnection,
updatedRecord: cachedRecordWithConnection,
objectMetadataItems,
});
throw error;
});
const refetchAggregateQueries = async () => {
const queryName = getAggregateQueryName(objectMetadataItem.namePlural);
await apolloCoreClient.refetchQueries({
include: [queryName],
});
};
await refetchAggregateQueries();
return updatedRecord?.data?.[mutationResponseField] ?? null;
}
};
return {
updateMultipleRecordsFromManyObjects,
};
};
@@ -2,10 +2,10 @@ import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { generateUpdateOneRecordMutation } from '@/object-record/multiple-objects/utils/generateUpdateOneRecordMutation';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useUpdateOneRecordMutation = ({
@@ -0,0 +1,222 @@
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation';
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
import { sanitizeRecordInput } from '@/object-record/utils/sanitizeRecordInput';
import { isNull } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { buildRecordFromKeysWithSameValue } from '~/utils/array/buildRecordFromKeysWithSameValue';
type UpdateOneRecordArgs<UpdatedObjectRecord> = {
idToUpdate: string;
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
optimisticRecord?: Partial<ObjectRecord>;
objectNameSingular: string;
recordGqlFields?: Record<string, any>;
};
export const useUpdateOneRecordV2 = () => {
const apolloCoreClient = useApolloCoreClient();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const updateOneRecord = async <
UpdatedObjectRecord extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
recordGqlFields,
idToUpdate,
updateOneRecordInput,
optimisticRecord,
}: UpdateOneRecordArgs<UpdatedObjectRecord>) => {
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
if (!objectMetadataItem) {
throw new Error(
`Object metadata item not found for ${objectNameSingular}`,
);
}
const computedRecordGqlFields =
recordGqlFields ??
generateDepthOneRecordGqlFields({ objectMetadataItem });
const optimisticRecordInput =
optimisticRecord ??
computeOptimisticRecordFromInput({
objectMetadataItem,
currentWorkspaceMember: currentWorkspaceMember,
recordInput: updateOneRecordInput,
cache: apolloCoreClient.cache,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
const cachedRecord = getRecordFromCache({
cache: apolloCoreClient.cache,
objectMetadataItem,
objectMetadataItems,
recordId: idToUpdate,
recordGqlFields: computedRecordGqlFields,
objectPermissionsByObjectMetadataId,
});
const cachedRecordWithConnection = getRecordNodeFromRecord<ObjectRecord>({
record: cachedRecord,
objectMetadataItem,
objectMetadataItems,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
});
const computedOptimisticRecord = {
...cachedRecord,
...optimisticRecordInput,
id: idToUpdate,
__typename: getObjectTypename(objectMetadataItem.nameSingular),
};
const optimisticRecordWithConnection =
getRecordNodeFromRecord<ObjectRecord>({
record: computedOptimisticRecord,
objectMetadataItem,
objectMetadataItems,
recordGqlFields: computedRecordGqlFields,
computeReferences: false,
});
const shouldHandleOptimisticCache =
!isNull(cachedRecord) &&
isDefined(optimisticRecordWithConnection) &&
isDefined(cachedRecordWithConnection);
if (shouldHandleOptimisticCache) {
const recordGqlFields = computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem,
record: optimisticRecordInput,
});
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloCoreClient.cache,
record: computedOptimisticRecord,
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloCoreClient.cache,
objectMetadataItem,
currentRecord: cachedRecordWithConnection,
updatedRecord: optimisticRecordWithConnection,
objectMetadataItems,
});
}
const mutationResponseField =
getUpdateOneRecordMutationResponseField(objectNameSingular);
const sanitizedInput = {
...sanitizeRecordInput({
objectMetadataItem,
recordInput: updateOneRecordInput,
}),
};
const updateOneRecordMutation = generateUpdateOneRecordMutation({
objectMetadataItem,
objectMetadataItems,
recordGqlFields,
computeReferences: false,
objectPermissionsByObjectMetadataId,
});
const updatedRecord = await apolloCoreClient
.mutate({
mutation: updateOneRecordMutation,
variables: {
idToUpdate,
input: sanitizedInput,
},
update: (cache, { data }) => {
const record = data?.[mutationResponseField];
if (!isDefined(record)) return;
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
currentRecord: computedOptimisticRecord,
updatedRecord: record,
objectMetadataItems,
});
},
})
.catch((error: Error) => {
if (!shouldHandleOptimisticCache) {
throw error;
}
const cachedRecordKeys = new Set(Object.keys(cachedRecord));
const recordKeysAddedByOptimisticCache = Object.keys(
optimisticRecordInput,
).filter((diffKey) => !cachedRecordKeys.has(diffKey));
const recordGqlFields = {
...computeDepthOneRecordGqlFieldsFromRecord({
objectMetadataItem,
record: cachedRecord,
}),
...buildRecordFromKeysWithSameValue(
recordKeysAddedByOptimisticCache,
true,
),
};
updateRecordFromCache({
objectMetadataItems,
objectMetadataItem,
cache: apolloCoreClient.cache,
record: {
...cachedRecord,
...buildRecordFromKeysWithSameValue(
recordKeysAddedByOptimisticCache,
null,
),
},
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
triggerUpdateRecordOptimisticEffect({
cache: apolloCoreClient.cache,
objectMetadataItem,
currentRecord: optimisticRecordWithConnection,
updatedRecord: cachedRecordWithConnection,
objectMetadataItems,
});
throw error;
});
return updatedRecord?.data?.[mutationResponseField] ?? null;
};
return {
updateOneRecord,
};
};