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:
-187
@@ -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 };
|
||||
};
|
||||
-118
@@ -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 };
|
||||
};
|
||||
-170
@@ -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 };
|
||||
};
|
||||
-89
@@ -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 };
|
||||
};
|
||||
+111
@@ -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 };
|
||||
};
|
||||
+92
@@ -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 };
|
||||
};
|
||||
-284
@@ -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,
|
||||
};
|
||||
};
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
||||
import { gql } from '@apollo/client';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
export const generateUpdateOneRecordMutation = ({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordGqlFields,
|
||||
computeReferences,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
computeReferences: boolean;
|
||||
objectPermissionsByObjectMetadataId: Record<
|
||||
string,
|
||||
ObjectPermissions & { objectMetadataId: string }
|
||||
>;
|
||||
}) => {
|
||||
const appliedRecordGqlFields =
|
||||
recordGqlFields ??
|
||||
generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||
|
||||
const mutationResponseField = getUpdateOneRecordMutationResponseField(
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
const updateOneRecordMutation = gql`
|
||||
mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!){
|
||||
${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery(
|
||||
{
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
computeReferences,
|
||||
recordGqlFields: appliedRecordGqlFields,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
},
|
||||
)}
|
||||
}
|
||||
`;
|
||||
|
||||
return updateOneRecordMutation;
|
||||
};
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import {
|
||||
type FieldMorphRelationMetadata,
|
||||
type FieldRelationMetadata,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
|
||||
export const getTargetFieldMetadataName = ({
|
||||
fieldDefinition,
|
||||
objectNameSingular,
|
||||
}: {
|
||||
fieldDefinition:
|
||||
| FieldDefinition<FieldMorphRelationMetadata>
|
||||
| FieldDefinition<FieldRelationMetadata>;
|
||||
objectNameSingular: string;
|
||||
}) => {
|
||||
if (isFieldRelation(fieldDefinition)) {
|
||||
return fieldDefinition.metadata.targetFieldMetadataName;
|
||||
} else {
|
||||
return fieldDefinition.metadata.morphRelations.find(
|
||||
(morphRelation) =>
|
||||
morphRelation.targetObjectMetadata.nameSingular === objectNameSingular,
|
||||
)?.targetFieldMetadata.name;
|
||||
}
|
||||
};
|
||||
+2
-2
@@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||
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';
|
||||
@@ -22,6 +21,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
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);
|
||||
@@ -83,7 +83,7 @@ export const RecordDetailRelationSectionDropdownToMany = () => {
|
||||
setMultipleRecordPickerSearchFilter('');
|
||||
}, [setMultipleRecordPickerSearchFilter]);
|
||||
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput();
|
||||
const { updateRelation } = useUpdateRelationOneToManyFieldInput();
|
||||
|
||||
const { createNewRecordAndOpenRightDrawer } =
|
||||
useAddNewRecordAndOpenRightDrawer({
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ export const RecordDetailRelationSectionDropdownToOne = () => {
|
||||
componentInstanceId={dropdownId}
|
||||
EmptyIcon={IconForbid}
|
||||
onRecordSelected={handleRelationPickerEntitySelected}
|
||||
objectNameSingular={relationObjectMetadataNameSingular}
|
||||
objectNameSingulars={[relationObjectMetadataNameSingular]}
|
||||
recordPickerInstanceId={dropdownId}
|
||||
onCancel={() => closeDropdown(dropdownId)}
|
||||
onCreate={shouldAllowCreateNew ? handleCreateNew : undefined}
|
||||
|
||||
+13
-5
@@ -9,7 +9,7 @@ import { visibleRecordFieldsComponentSelector } from '@/object-record/record-fie
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRecordFieldGqlFields = ({
|
||||
objectMetadataItem,
|
||||
@@ -73,10 +73,18 @@ export const useRecordFieldGqlFields = ({
|
||||
);
|
||||
}
|
||||
|
||||
return fieldMetadataItem.morphRelations.map((morphRelation) => [
|
||||
morphRelation.sourceFieldMetadata.name,
|
||||
true,
|
||||
]);
|
||||
return fieldMetadataItem.morphRelations.map((morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldMetadataItem.name,
|
||||
relationType: fieldMetadataItem.settings?.relationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
return [computedFieldName, true];
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+4
-4
@@ -19,8 +19,6 @@ import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFi
|
||||
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
|
||||
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
|
||||
import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFieldRating';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
|
||||
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
|
||||
|
||||
@@ -28,6 +26,8 @@ import { MorphRelationManyToOneFieldDisplay } from '@/object-record/record-field
|
||||
import { MorphRelationOneToManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/display/components/MorphRelationOneToManyFieldDisplay';
|
||||
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
|
||||
import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { AddressFieldDisplay } from '../meta-types/display/components/AddressFieldDisplay';
|
||||
@@ -74,9 +74,9 @@ export const FieldDisplay = () => {
|
||||
|
||||
return isChipDisplay ? (
|
||||
<ChipFieldDisplay />
|
||||
) : isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
) : isFieldRelationManyToOne(fieldDefinition) ? (
|
||||
<RelationToOneFieldDisplay />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
) : isFieldRelationOneToMany(fieldDefinition) ? (
|
||||
<RelationFromManyFieldDisplay />
|
||||
) : isFieldMorphRelationManyToOne(fieldDefinition) ? (
|
||||
<MorphRelationManyToOneFieldDisplay />
|
||||
|
||||
+12
-8
@@ -8,13 +8,14 @@ import { LinksFieldInput } from '@/object-record/record-field/ui/meta-types/inpu
|
||||
import { MultiSelectFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MultiSelectFieldInput';
|
||||
import { PhonesFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/PhonesFieldInput';
|
||||
import { RawJsonFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RawJsonFieldInput';
|
||||
import { RelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationFromManyFieldInput';
|
||||
import { SelectFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/SelectFieldInput';
|
||||
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects';
|
||||
|
||||
import { ArrayFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/ArrayFieldInput';
|
||||
import { MorphRelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput';
|
||||
import { MorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/MorphRelationOneToManyFieldInput';
|
||||
import { RelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput';
|
||||
import { RelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput';
|
||||
import { RichTextFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RichTextFieldInput';
|
||||
import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress';
|
||||
import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray';
|
||||
@@ -25,12 +26,14 @@ import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/is
|
||||
import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails';
|
||||
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
|
||||
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
|
||||
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
|
||||
import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany';
|
||||
import { isFieldMultiSelect } from '@/object-record/record-field/ui/types/guards/isFieldMultiSelect';
|
||||
import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber';
|
||||
import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFieldRating';
|
||||
import { isFieldRawJson } from '@/object-record/record-field/ui/types/guards/isFieldRawJson';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
|
||||
import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFieldSelect';
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
@@ -39,7 +42,6 @@ import { CurrencyFieldInput } from '../meta-types/input/components/CurrencyField
|
||||
import { DateTimeFieldInput } from '../meta-types/input/components/DateTimeFieldInput';
|
||||
import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput';
|
||||
import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput';
|
||||
import { RelationToOneFieldInput } from '../meta-types/input/components/RelationToOneFieldInput';
|
||||
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
|
||||
@@ -48,10 +50,12 @@ export const FieldInput = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
<RelationToOneFieldInput />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldInput />
|
||||
{isFieldRelationManyToOne(fieldDefinition) ? (
|
||||
<RelationManyToOneFieldInput />
|
||||
) : isFieldRelationOneToMany(fieldDefinition) ? (
|
||||
<RelationOneToManyFieldInput />
|
||||
) : isFieldMorphRelationManyToOne(fieldDefinition) ? (
|
||||
<MorphRelationManyToOneFieldInput />
|
||||
) : isFieldMorphRelationOneToMany(fieldDefinition) ? (
|
||||
<MorphRelationOneToManyFieldInput />
|
||||
) : isFieldPhones(fieldDefinition) ? (
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ import { isFieldMultiSelect } from '@/object-record/record-field/ui/types/guards
|
||||
import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber';
|
||||
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
|
||||
import { isFieldRawJson } from '@/object-record/record-field/ui/types/guards/isFieldRawJson';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
|
||||
import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFieldSelect';
|
||||
import { isFieldText } from '@/object-record/record-field/ui/types/guards/isFieldText';
|
||||
@@ -210,7 +210,7 @@ export const FormFieldInput = ({
|
||||
readonly={readonly}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
) : isFieldRelationToOneObject(field) ? (
|
||||
) : isFieldRelationManyToOne(field) ? (
|
||||
<FormRelationToOneFieldInput
|
||||
label={field.label}
|
||||
objectNameSingular={field.metadata.relationObjectMetadataNameSingular}
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const FormRelationToOneFieldInput = ({
|
||||
: defaultValue
|
||||
}
|
||||
onChange={onChange}
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
disabled={readonly}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
|
||||
+15
-11
@@ -16,10 +16,9 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useCallback, useId } from 'react';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { CustomError, isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { IconChevronDown, IconForbid } from 'twenty-ui/display';
|
||||
|
||||
const StyledFormSelectContainer = styled(FormFieldInputInnerContainer)<{
|
||||
@@ -63,7 +62,7 @@ export type FormSingleRecordPickerProps = {
|
||||
label?: string;
|
||||
defaultValue?: RecordId | Variable;
|
||||
onChange: (value: RecordId | Variable | null) => void;
|
||||
objectNameSingular: string;
|
||||
objectNameSingulars: string[];
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
@@ -72,14 +71,12 @@ export type FormSingleRecordPickerProps = {
|
||||
export const FormSingleRecordPicker = ({
|
||||
label,
|
||||
defaultValue,
|
||||
objectNameSingular,
|
||||
objectNameSingulars,
|
||||
onChange,
|
||||
disabled,
|
||||
testId,
|
||||
VariablePicker,
|
||||
}: FormSingleRecordPickerProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const theme = useTheme();
|
||||
const draftValue: FormSingleRecordPickerValue = isStandaloneVariableString(
|
||||
defaultValue,
|
||||
@@ -93,12 +90,19 @@ export const FormSingleRecordPicker = ({
|
||||
value: defaultValue || '',
|
||||
};
|
||||
|
||||
if (objectNameSingulars.length === 0) {
|
||||
throw new CustomError(
|
||||
'Object is missing while building form single record picker',
|
||||
'FORM_SINGLE_RECORD_PICKER_OBJECT_NAME_SINGULAR_IS_MISSING',
|
||||
);
|
||||
}
|
||||
|
||||
const { record: selectedRecord } = useFindOneRecord({
|
||||
objectRecordId:
|
||||
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
|
||||
? defaultValue
|
||||
: '',
|
||||
objectNameSingular,
|
||||
objectNameSingular: objectNameSingulars[0],
|
||||
withSoftDeleted: true,
|
||||
skip: !isDefined(defaultValue) || !isValidUuid(defaultValue),
|
||||
});
|
||||
@@ -171,7 +175,7 @@ export const FormSingleRecordPicker = ({
|
||||
<FormSingleRecordFieldChip
|
||||
draftValue={draftValue}
|
||||
selectedRecord={selectedRecord}
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectNameSingular={objectNameSingulars[0]}
|
||||
onRemove={handleUnlinkVariable}
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -193,7 +197,7 @@ export const FormSingleRecordPicker = ({
|
||||
<FormSingleRecordFieldChip
|
||||
draftValue={draftValue}
|
||||
selectedRecord={selectedRecord}
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectNameSingular={objectNameSingulars[0]}
|
||||
onRemove={handleUnlinkVariable}
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -210,10 +214,10 @@ export const FormSingleRecordPicker = ({
|
||||
focusId={dropdownId}
|
||||
componentInstanceId={dropdownId}
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={t`No ${objectNameSingular}`}
|
||||
emptyLabel={'No ' + objectNameSingulars.join(' or ')}
|
||||
onCancel={() => closeDropdown(dropdownId)}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
objectNameSingular={objectNameSingular}
|
||||
objectNameSingulars={objectNameSingulars}
|
||||
recordPickerInstanceId={dropdownId}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ export const Default: Story = {
|
||||
args: {
|
||||
label: 'Company',
|
||||
defaultValue: '123e4567-e89b-12d3-a456-426614174000',
|
||||
objectNameSingular: 'company',
|
||||
objectNameSingulars: ['company'],
|
||||
onChange: fn(),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
@@ -56,7 +56,7 @@ export const WithVariables: Story = {
|
||||
args: {
|
||||
label: 'Company',
|
||||
defaultValue: `{{${MOCKED_STEP_ID}.company.id}}`,
|
||||
objectNameSingular: 'company',
|
||||
objectNameSingulars: ['company'],
|
||||
onChange: fn(),
|
||||
VariablePicker: () => <div>VariablePicker</div>,
|
||||
},
|
||||
@@ -80,7 +80,7 @@ export const Disabled: Story = {
|
||||
args: {
|
||||
label: 'Company',
|
||||
defaultValue: '123e4567-e89b-12d3-a456-426614174000',
|
||||
objectNameSingular: 'company',
|
||||
objectNameSingulars: ['company'],
|
||||
onChange: fn(),
|
||||
disabled: true,
|
||||
VariablePicker: () => <div>VariablePicker</div>,
|
||||
|
||||
+25
-10
@@ -5,7 +5,8 @@ import { type Task } from '@/activities/types/Task';
|
||||
import { type TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useOpenMorphRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationFromManyFieldInput';
|
||||
import { useOpenMorphRelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationManyToOneFieldInput';
|
||||
import { useOpenMorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput';
|
||||
import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
|
||||
import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenRelationToOneFieldInput';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
@@ -15,9 +16,10 @@ import {
|
||||
type FieldRelationValue,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
|
||||
import { isFieldMorphRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationOneToMany';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -32,12 +34,15 @@ export const useOpenFieldInputEditMode = () => {
|
||||
const { openRelationFromManyFieldInput } =
|
||||
useOpenRelationFromManyFieldInput();
|
||||
|
||||
const { openMorphRelationFromManyFieldInput } =
|
||||
useOpenMorphRelationFromManyFieldInput();
|
||||
const { openMorphRelationOneToManyFieldInput } =
|
||||
useOpenMorphRelationOneToManyFieldInput();
|
||||
|
||||
const { openActivityTargetCellEditMode } =
|
||||
useOpenActivityTargetCellEditMode();
|
||||
|
||||
const { openMorphRelationManyToOneFieldInput } =
|
||||
useOpenMorphRelationManyToOneFieldInput();
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const openFieldInput = useRecoilCallback(
|
||||
@@ -52,7 +57,7 @@ export const useOpenFieldInputEditMode = () => {
|
||||
prefix?: string;
|
||||
}) => {
|
||||
if (
|
||||
isFieldRelationFromManyObjects(fieldDefinition) &&
|
||||
isFieldRelationOneToMany(fieldDefinition) &&
|
||||
['taskTarget', 'noteTarget'].includes(
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
)
|
||||
@@ -91,7 +96,7 @@ export const useOpenFieldInputEditMode = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFieldRelationToOneObject(fieldDefinition)) {
|
||||
if (isFieldRelationManyToOne(fieldDefinition)) {
|
||||
openRelationToOneFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
recordId,
|
||||
@@ -106,7 +111,7 @@ export const useOpenFieldInputEditMode = () => {
|
||||
throw new Error('Field is not a morph relation one to many');
|
||||
}
|
||||
|
||||
openMorphRelationFromManyFieldInput({
|
||||
openMorphRelationOneToManyFieldInput({
|
||||
recordId,
|
||||
prefix,
|
||||
fieldDefinition,
|
||||
@@ -114,7 +119,7 @@ export const useOpenFieldInputEditMode = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFieldRelationFromManyObjects(fieldDefinition)) {
|
||||
if (isFieldRelationOneToMany(fieldDefinition)) {
|
||||
if (
|
||||
isDefined(
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
@@ -131,6 +136,15 @@ export const useOpenFieldInputEditMode = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isFieldMorphRelationManyToOne(fieldDefinition)) {
|
||||
openMorphRelationManyToOneFieldInput({
|
||||
recordId,
|
||||
prefix,
|
||||
fieldDefinition,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: getRecordFieldInputInstanceId({
|
||||
recordId,
|
||||
@@ -152,7 +166,8 @@ export const useOpenFieldInputEditMode = () => {
|
||||
},
|
||||
[
|
||||
openActivityTargetCellEditMode,
|
||||
openMorphRelationFromManyFieldInput,
|
||||
openMorphRelationManyToOneFieldInput,
|
||||
openMorphRelationOneToManyFieldInput,
|
||||
openRelationFromManyFieldInput,
|
||||
openRelationToOneFieldInput,
|
||||
pushFocusItemToFocusStack,
|
||||
|
||||
+113
-23
@@ -3,6 +3,7 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import {
|
||||
type FieldMetadata,
|
||||
type FieldMorphRelationMetadata,
|
||||
type FieldRelationMetadata,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress';
|
||||
@@ -21,21 +22,28 @@ import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFi
|
||||
import { isFieldPhonesValue } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue';
|
||||
import { isFieldRawJson } from '@/object-record/record-field/ui/types/guards/isFieldRawJson';
|
||||
import { isFieldRawJsonValue } from '@/object-record/record-field/ui/types/guards/isFieldRawJsonValue';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
|
||||
import { isFieldRelationToOneValue } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneValue';
|
||||
import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFieldSelect';
|
||||
import { isFieldSelectValue } from '@/object-record/record-field/ui/types/guards/isFieldSelectValue';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray';
|
||||
import { isFieldArrayValue } from '@/object-record/record-field/ui/types/guards/isFieldArrayValue';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRelationManyToOneValue } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOneValue';
|
||||
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
|
||||
import { isFieldRichTextV2 } from '@/object-record/record-field/ui/types/guards/isFieldRichTextV2';
|
||||
import { isFieldRichTextValue } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValue';
|
||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValueV2';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
|
||||
@@ -55,13 +63,16 @@ export const usePersistField = ({
|
||||
}: {
|
||||
objectMetadataItemId: string;
|
||||
}) => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: objectMetadataItem?.nameSingular ?? '',
|
||||
});
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const persistField = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
@@ -74,10 +85,15 @@ export const usePersistField = ({
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
valueToPersist: unknown;
|
||||
}) => {
|
||||
const fieldIsRelationToOneObject =
|
||||
isFieldRelationToOneObject(
|
||||
const fieldIsRelationManyToOne =
|
||||
isFieldRelationManyToOne(
|
||||
fieldDefinition as FieldDefinition<FieldRelationMetadata>,
|
||||
) && isFieldRelationToOneValue(valueToPersist);
|
||||
) && isFieldRelationManyToOneValue(valueToPersist);
|
||||
|
||||
const fieldIsMorphRelationManyToOne =
|
||||
isFieldMorphRelationManyToOne(
|
||||
fieldDefinition as FieldDefinition<FieldMorphRelationMetadata>,
|
||||
) && isFieldRelationManyToOneValue(valueToPersist);
|
||||
|
||||
const fieldIsText =
|
||||
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
|
||||
@@ -150,7 +166,8 @@ export const usePersistField = ({
|
||||
}
|
||||
|
||||
const isValuePersistable =
|
||||
fieldIsRelationToOneObject ||
|
||||
fieldIsMorphRelationManyToOne ||
|
||||
fieldIsRelationManyToOne ||
|
||||
fieldIsText ||
|
||||
fieldIsBoolean ||
|
||||
fieldIsEmails ||
|
||||
@@ -177,23 +194,14 @@ export const usePersistField = ({
|
||||
.getLoadable(recordStoreFamilySelector({ recordId, fieldName }))
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
fieldIsRelationToOneObject &&
|
||||
valueToPersist?.id === currentValue?.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (fieldIsRelationManyToOne) {
|
||||
if (
|
||||
fieldIsRelationManyToOne &&
|
||||
valueToPersist?.id === currentValue?.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDeeplyEqual(valueToPersist, currentValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
recordStoreFamilySelector({ recordId, fieldName }),
|
||||
valueToPersist,
|
||||
);
|
||||
|
||||
if (fieldIsRelationToOneObject) {
|
||||
updateOneRecord?.({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
@@ -201,6 +209,79 @@ export const usePersistField = ({
|
||||
valueToPersist?.id ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
set(
|
||||
recordStoreFamilySelector({ recordId, fieldName }),
|
||||
valueToPersist,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldIsMorphRelationManyToOne) {
|
||||
if (isFieldMorphRelation(fieldDefinition)) {
|
||||
// TODO: this is hack, we should refactor the SingleRecordPicker to pass the updated object
|
||||
const objectNameSingulars =
|
||||
fieldDefinition.metadata.morphRelations.map(
|
||||
(morphRelation) =>
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(valueToPersist?.id)) {
|
||||
// Handle detach
|
||||
return;
|
||||
}
|
||||
|
||||
for (const objectNameSingular of objectNameSingulars) {
|
||||
const candidateObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(candidateObjectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for ${objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const cachedRecord = getRecordFromCache({
|
||||
cache: apolloCoreClient.cache,
|
||||
objectMetadataItem: candidateObjectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordId: valueToPersist.id,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
|
||||
if (!isDefined(cachedRecord)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName,
|
||||
relationType: fieldDefinition.metadata.relationType,
|
||||
targetObjectMetadataNameSingular: objectNameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
candidateObjectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
updateOneRecord?.({
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[`${computedFieldName}Id`]: valueToPersist.id,
|
||||
},
|
||||
});
|
||||
set(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: computedFieldName,
|
||||
}),
|
||||
valueToPersist,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDeeplyEqual(valueToPersist, currentValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,6 +291,10 @@ export const usePersistField = ({
|
||||
[fieldName]: valueToPersist,
|
||||
},
|
||||
});
|
||||
set(
|
||||
recordStoreFamilySelector({ recordId, fieldName }),
|
||||
valueToPersist,
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Invalid value to persist: ${JSON.stringify(
|
||||
@@ -220,7 +305,12 @@ export const usePersistField = ({
|
||||
);
|
||||
}
|
||||
},
|
||||
[updateOneRecord],
|
||||
[
|
||||
apolloCoreClient.cache,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
updateOneRecord,
|
||||
],
|
||||
);
|
||||
|
||||
return persistField;
|
||||
|
||||
-1
@@ -36,7 +36,6 @@ export const useMorphRelationFromManyFieldDisplay = () => {
|
||||
recordStoreMorphOneToManyValueWithObjectNameFamilySelector({
|
||||
recordId,
|
||||
morphRelations: fieldDefinition.metadata.morphRelations,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
-4
@@ -17,7 +17,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useMorphRelationToOneFieldDisplay = () => {
|
||||
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
|
||||
const { chipGeneratorPerObjectPerField } = useContext(
|
||||
PreComputedChipGeneratorsContext,
|
||||
);
|
||||
@@ -34,13 +33,10 @@ export const useMorphRelationToOneFieldDisplay = () => {
|
||||
|
||||
const button = fieldDefinition.editButtonIcon;
|
||||
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
|
||||
const morphFieldValueWithObjectName = useRecoilValue(
|
||||
recordStoreMorphManyToOneValueWithObjectNameFamilySelector({
|
||||
recordId,
|
||||
morphRelations: fieldDefinition.metadata.morphRelations,
|
||||
fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionLoadingComponentState';
|
||||
import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne';
|
||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||
import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord';
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useContext } from 'react';
|
||||
import { IconForbid } from 'twenty-ui/display';
|
||||
|
||||
export const MorphRelationManyToOneFieldInput = () => {
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const { onCancel, onSubmit } = useContext(FieldInputEventContext);
|
||||
const { onCloseTableCell } = useRecordTableBodyContextOrThrow();
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
const handleRecordSelected = (
|
||||
selectedRecord: SingleRecordPickerRecord | null | undefined,
|
||||
) => {
|
||||
onSubmit?.({ newValue: selectedRecord?.record ?? null });
|
||||
onCloseTableCell();
|
||||
};
|
||||
|
||||
const layoutDirection = useRecoilComponentValue(
|
||||
recordFieldInputLayoutDirectionComponentState,
|
||||
);
|
||||
|
||||
const isLoading = useRecoilComponentValue(
|
||||
recordFieldInputLayoutDirectionLoadingComponentState,
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (!isFieldMorphRelationManyToOne(fieldDefinition)) {
|
||||
return null;
|
||||
}
|
||||
const objectNameSingulars = fieldDefinition.metadata.morphRelations.map(
|
||||
(morphRelation) => morphRelation.targetObjectMetadata.nameSingular,
|
||||
);
|
||||
|
||||
return (
|
||||
<SingleRecordPicker
|
||||
focusId={instanceId}
|
||||
componentInstanceId={instanceId}
|
||||
EmptyIcon={IconForbid}
|
||||
emptyLabel={'No ' + fieldDefinition.label}
|
||||
onCancel={onCancel}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
objectNameSingulars={objectNameSingulars}
|
||||
recordPickerInstanceId={instanceId}
|
||||
layoutDirection={
|
||||
layoutDirection === 'downward'
|
||||
? 'search-bar-on-top'
|
||||
: 'search-bar-on-bottom'
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
||||
import { useUpdateMorphRelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationManyToOneFieldInput';
|
||||
import { useUpdateMorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateMorphRelationOneToManyFieldInput';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
|
||||
@@ -15,8 +15,8 @@ export const MorphRelationOneToManyFieldInput = () => {
|
||||
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const { updateMorphRelationManyToOne } =
|
||||
useUpdateMorphRelationManyToOneFieldInput();
|
||||
const { updateMorphRelationOneToMany } =
|
||||
useUpdateMorphRelationOneToManyFieldInput();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.({ skipPersist: true });
|
||||
@@ -32,7 +32,7 @@ export const MorphRelationOneToManyFieldInput = () => {
|
||||
componentInstanceId={instanceId}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={(morphItem) => {
|
||||
updateMorphRelationManyToOne(morphItem);
|
||||
updateMorphRelationOneToMany(morphItem);
|
||||
}}
|
||||
onClickOutside={handleSubmit}
|
||||
layoutDirection={
|
||||
|
||||
+4
-4
@@ -18,7 +18,7 @@ import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconForbid } from 'twenty-ui/display';
|
||||
|
||||
export const RelationToOneFieldInput = () => {
|
||||
export const RelationManyToOneFieldInput = () => {
|
||||
const { t } = useLingui();
|
||||
const { fieldDefinition, recordId } = useRelationField<ObjectRecord>();
|
||||
|
||||
@@ -86,9 +86,9 @@ export const RelationToOneFieldInput = () => {
|
||||
onCancel={onCancel}
|
||||
onCreate={handleCreateNew}
|
||||
onRecordSelected={handleRecordSelected}
|
||||
objectNameSingular={
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular
|
||||
}
|
||||
objectNameSingulars={[
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
]}
|
||||
recordPickerInstanceId={instanceId}
|
||||
layoutDirection={
|
||||
layoutDirection === 'downward'
|
||||
+3
-3
@@ -10,7 +10,6 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont
|
||||
import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
||||
import { useRelationField } from '@/object-record/record-field/ui/meta-types/hooks/useRelationField';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
@@ -23,8 +22,9 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput';
|
||||
|
||||
export const RelationFromManyFieldInput = () => {
|
||||
export const RelationOneToManyFieldInput = () => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
RecordFieldComponentInstanceContext,
|
||||
@@ -32,7 +32,7 @@ export const RelationFromManyFieldInput = () => {
|
||||
|
||||
const { onSubmit } = useContext(FieldInputEventContext);
|
||||
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput();
|
||||
const { updateRelation } = useUpdateRelationOneToManyFieldInput();
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
const objectMetadataNameSingular =
|
||||
fieldDefinition.metadata.objectMetadataNameSingular;
|
||||
+8
-8
@@ -22,8 +22,8 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { getCanvasElementForDropdownTesting } from 'twenty-ui/testing';
|
||||
import { RelationToOneFieldInput } from '../RelationToOneFieldInput';
|
||||
|
||||
import { RelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { getFieldInputEventContextProviderWithJestMocks } from './utils/getFieldInputEventContextProviderWithJestMocks';
|
||||
|
||||
@@ -56,14 +56,14 @@ const {
|
||||
handleCancelMocked,
|
||||
} = getFieldInputEventContextProviderWithJestMocks();
|
||||
|
||||
type RelationToOneFieldInputWithContextProps = {
|
||||
type RelationManyToOneFieldInputWithContextProps = {
|
||||
value: number;
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
const RelationToOneFieldInputWithContext = ({
|
||||
const RelationManyToOneFieldInputWithContext = ({
|
||||
recordId,
|
||||
}: RelationToOneFieldInputWithContextProps) => {
|
||||
}: RelationManyToOneFieldInputWithContextProps) => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -108,7 +108,7 @@ const RelationToOneFieldInputWithContext = ({
|
||||
<FieldInputEventContextProviderWithJestMocks>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
|
||||
<RelationToOneFieldInput />
|
||||
<RelationManyToOneFieldInput />
|
||||
</FieldInputEventContextProviderWithJestMocks>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
</FieldContext.Provider>
|
||||
@@ -126,8 +126,8 @@ const clearMocksDecorator: Decorator = (Story, context) => {
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Input/RelationToOneFieldInput',
|
||||
component: RelationToOneFieldInputWithContext,
|
||||
title: 'UI/Data/Field/Input/RelationManyToOneFieldInput',
|
||||
component: RelationManyToOneFieldInputWithContext,
|
||||
args: {
|
||||
useEditButton: true,
|
||||
onSubmit: handleSubmitMocked,
|
||||
@@ -151,7 +151,7 @@ const meta: Meta = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RelationToOneFieldInputWithContext>;
|
||||
type Story = StoryObj<typeof RelationManyToOneFieldInputWithContext>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
+6
-6
@@ -4,7 +4,6 @@ import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { RelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationFromManyFieldInput';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useOpenFieldInputEditMode } from '@/object-record/record-field/ui/hooks/useOpenFieldInputEditMode';
|
||||
import { RelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
@@ -37,7 +37,7 @@ const RelationWorkspaceSetterEffect = () => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const RelationManyFieldInputWithContext = () => {
|
||||
const RelationOneToManyFieldInputWithContext = () => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const fieldDefinition = useMemo(
|
||||
@@ -104,7 +104,7 @@ const RelationManyFieldInputWithContext = () => {
|
||||
}}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationFromManyFieldInput />
|
||||
<RelationOneToManyFieldInput />
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldComponentInstanceContext.Provider>
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
@@ -112,8 +112,8 @@ const RelationManyFieldInputWithContext = () => {
|
||||
);
|
||||
};
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Input/RelationManyFieldInput',
|
||||
component: RelationManyFieldInputWithContext,
|
||||
title: 'UI/Data/Field/Input/RelationOneToManyFieldInput',
|
||||
component: RelationOneToManyFieldInputWithContext,
|
||||
args: {},
|
||||
decorators: [ObjectMetadataItemsDecorator, SnackBarDecorator],
|
||||
parameters: {
|
||||
@@ -124,6 +124,6 @@ const meta: Meta = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RelationManyFieldInputWithContext>;
|
||||
type Story = StoryObj<typeof RelationOneToManyFieldInputWithContext>;
|
||||
|
||||
export const Default: Story = {};
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import {
|
||||
type FieldMorphRelationMetadata,
|
||||
type FieldRelationToOneValue,
|
||||
type FieldRelationValue,
|
||||
} from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen';
|
||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useOpenMorphRelationManyToOneFieldInput = () => {
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { openSingleRecordPicker } = useSingleRecordPickerOpen();
|
||||
|
||||
const openMorphRelationManyToOneFieldInput = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
prefix,
|
||||
}: {
|
||||
fieldDefinition: FieldDefinition<FieldMorphRelationMetadata>;
|
||||
recordId: string;
|
||||
prefix?: string;
|
||||
}) => {
|
||||
const potentielFieldNames = fieldDefinition.metadata.morphRelations.map(
|
||||
(morphRelation) => {
|
||||
return computeMorphRelationFieldName({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
relationType: fieldDefinition.metadata.relationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const fieldValue = potentielFieldNames
|
||||
.map((fieldName) => {
|
||||
const fieldValue = snapshot
|
||||
.getLoadable<FieldRelationValue<FieldRelationToOneValue>>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
return fieldValue;
|
||||
})
|
||||
.find((fieldValue) => isDefined(fieldValue));
|
||||
|
||||
const recordPickerInstanceId = getRecordFieldInputInstanceId({
|
||||
recordId,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
prefix,
|
||||
});
|
||||
|
||||
if (isDefined(fieldValue)) {
|
||||
set(
|
||||
singleRecordPickerSelectedIdComponentState.atomFamily({
|
||||
instanceId: recordPickerInstanceId,
|
||||
}),
|
||||
fieldValue.id,
|
||||
);
|
||||
}
|
||||
|
||||
openSingleRecordPicker(recordPickerInstanceId);
|
||||
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: recordPickerInstanceId,
|
||||
component: {
|
||||
type: FocusComponentType.OPENED_FIELD_INPUT,
|
||||
instanceId: recordPickerInstanceId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
[openSingleRecordPicker, pushFocusItemToFocusStack],
|
||||
);
|
||||
|
||||
return { openMorphRelationManyToOneFieldInput };
|
||||
};
|
||||
+5
-4
@@ -16,13 +16,13 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useOpenMorphRelationFromManyFieldInput = () => {
|
||||
export const useOpenMorphRelationOneToManyFieldInput = () => {
|
||||
const { performSearch } = useMultipleRecordPickerPerformSearch();
|
||||
const { openMultipleRecordPicker } = useMultipleRecordPickerOpen();
|
||||
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
const openMorphRelationFromManyFieldInput = useRecoilCallback(
|
||||
const openMorphRelationOneToManyFieldInput = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({
|
||||
recordId,
|
||||
@@ -48,7 +48,6 @@ export const useOpenMorphRelationFromManyFieldInput = () => {
|
||||
recordStoreMorphOneToManyValueWithObjectNameFamilySelector({
|
||||
recordId,
|
||||
morphRelations: fieldDefinition.metadata.morphRelations,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
@@ -136,5 +135,7 @@ export const useOpenMorphRelationFromManyFieldInput = () => {
|
||||
[openMultipleRecordPicker, performSearch, pushFocusItemToFocusStack],
|
||||
);
|
||||
|
||||
return { openMorphRelationFromManyFieldInput };
|
||||
return {
|
||||
openMorphRelationOneToManyFieldInput,
|
||||
};
|
||||
};
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useAttachMorphRelatedRecordFromRecord } from '@/object-record/hooks/useAttachMorphRelatedRecordFromRecord';
|
||||
import { useDetachMorphRelatedRecordFromRecord } from '@/object-record/hooks/useDetachMorphRelatedRecordFromRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUpdateMorphRelationManyToOneFieldInput = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
isFieldMorphRelation,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
if (!fieldDefinition.metadata.objectMetadataNameSingular) {
|
||||
throw new Error('ObjectMetadataNameSingular is required');
|
||||
}
|
||||
|
||||
const { updateOneRecordAndDetachMorphRelations } =
|
||||
useDetachMorphRelatedRecordFromRecord();
|
||||
|
||||
const { updateOneRecordAndAttachMorphRelations } =
|
||||
useAttachMorphRelatedRecordFromRecord();
|
||||
|
||||
const updateMorphRelationManyToOne = useRecoilCallback(
|
||||
() =>
|
||||
async (
|
||||
morphItem: Pick<
|
||||
RecordPickerPickableMorphItem,
|
||||
'recordId' | 'isSelected'
|
||||
>,
|
||||
) => {
|
||||
const recordObjectNameSingulars =
|
||||
fieldDefinition.metadata.morphRelations.map(
|
||||
(morphRelation) => morphRelation.targetObjectMetadata.nameSingular,
|
||||
);
|
||||
if (morphItem.isSelected) {
|
||||
await updateOneRecordAndAttachMorphRelations({
|
||||
recordId,
|
||||
relatedRecordId: morphItem.recordId,
|
||||
objectNameSingulars: recordObjectNameSingulars,
|
||||
});
|
||||
} else {
|
||||
await updateOneRecordAndDetachMorphRelations({
|
||||
recordId,
|
||||
relatedRecordId: morphItem.recordId,
|
||||
objectNameSingulars: recordObjectNameSingulars,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
fieldDefinition.metadata.morphRelations,
|
||||
recordId,
|
||||
updateOneRecordAndAttachMorphRelations,
|
||||
updateOneRecordAndDetachMorphRelations,
|
||||
],
|
||||
);
|
||||
|
||||
return { updateMorphRelationManyToOne };
|
||||
};
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useRecordOneToManyFieldAttachTargetRecord } from '@/object-record/hooks/useRecordOneToManyFieldAttachTargetRecord';
|
||||
import { useRecordOneToManyFieldDetachTargetRecord } from '@/object-record/hooks/useRecordOneToManyFieldDetachTargetRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUpdateMorphRelationOneToManyFieldInput = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
isFieldMorphRelation,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
if (!fieldDefinition.metadata.objectMetadataNameSingular) {
|
||||
throw new Error('ObjectMetadataNameSingular is required');
|
||||
}
|
||||
|
||||
const { recordOneToManyFieldDetachTargetRecord } =
|
||||
useRecordOneToManyFieldDetachTargetRecord();
|
||||
|
||||
const { recordOneToManyFieldAttachTargetRecord } =
|
||||
useRecordOneToManyFieldAttachTargetRecord();
|
||||
|
||||
const updateMorphRelationOneToMany = useRecoilCallback(
|
||||
() =>
|
||||
async (
|
||||
morphItem: Pick<
|
||||
RecordPickerPickableMorphItem,
|
||||
'recordId' | 'isSelected' | 'objectMetadataId'
|
||||
>,
|
||||
) => {
|
||||
if (!fieldDefinition.metadata.objectMetadataNameSingular) {
|
||||
throw new Error('ObjectMetadataNameSingular is required');
|
||||
}
|
||||
|
||||
const targetMorphRelation =
|
||||
fieldDefinition.metadata.morphRelations.find(
|
||||
(morphRelation) =>
|
||||
morphRelation.targetObjectMetadata.id ===
|
||||
morphItem.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(targetMorphRelation)) {
|
||||
throw new Error('TargetMorphRelation is required');
|
||||
}
|
||||
|
||||
if (morphItem.isSelected) {
|
||||
await recordOneToManyFieldAttachTargetRecord({
|
||||
sourceObjectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
targetObjectNameSingular:
|
||||
targetMorphRelation.targetObjectMetadata.nameSingular,
|
||||
targetGQLFieldName: targetMorphRelation.targetFieldMetadata.name,
|
||||
sourceRecordId: recordId,
|
||||
targetRecordId: morphItem.recordId,
|
||||
});
|
||||
} else {
|
||||
await recordOneToManyFieldDetachTargetRecord({
|
||||
sourceObjectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
targetObjectNameSingular:
|
||||
targetMorphRelation.targetObjectMetadata.nameSingular,
|
||||
targetGQLFieldName: targetMorphRelation.targetFieldMetadata.name,
|
||||
sourceRecordId: recordId,
|
||||
targetRecordId: morphItem.recordId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
fieldDefinition.metadata.morphRelations,
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
recordId,
|
||||
recordOneToManyFieldAttachTargetRecord,
|
||||
recordOneToManyFieldDetachTargetRecord,
|
||||
],
|
||||
);
|
||||
|
||||
return { updateMorphRelationOneToMany };
|
||||
};
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useAttachRelatedRecordFromRecord } from '@/object-record/hooks/useAttachRelatedRecordFromRecord';
|
||||
import { useDetachRelatedRecordFromRecord } from '@/object-record/hooks/useDetachRelatedRecordFromRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUpdateRelationFromManyFieldInput = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.RELATION,
|
||||
isFieldRelation,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
if (!fieldDefinition.metadata.objectMetadataNameSingular) {
|
||||
throw new Error('ObjectMetadataNameSingular is required');
|
||||
}
|
||||
|
||||
const { updateOneRecordAndDetachRelations } =
|
||||
useDetachRelatedRecordFromRecord({
|
||||
recordObjectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
fieldNameOnRecordObject: fieldDefinition.metadata.fieldName,
|
||||
});
|
||||
|
||||
const { updateOneRecordAndAttachRelations } =
|
||||
useAttachRelatedRecordFromRecord({
|
||||
recordObjectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
fieldNameOnRecordObject: fieldDefinition.metadata.fieldName,
|
||||
});
|
||||
|
||||
const updateRelation = useRecoilCallback(
|
||||
() => async (morphItem: RecordPickerPickableMorphItem) => {
|
||||
if (morphItem.isSelected) {
|
||||
await updateOneRecordAndAttachRelations({
|
||||
recordId,
|
||||
relatedRecordId: morphItem.recordId,
|
||||
});
|
||||
} else {
|
||||
await updateOneRecordAndDetachRelations({
|
||||
recordId,
|
||||
relatedRecordId: morphItem.recordId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
recordId,
|
||||
updateOneRecordAndAttachRelations,
|
||||
updateOneRecordAndDetachRelations,
|
||||
],
|
||||
);
|
||||
|
||||
return { updateRelation };
|
||||
};
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useRecordOneToManyFieldAttachTargetRecord } from '@/object-record/hooks/useRecordOneToManyFieldAttachTargetRecord';
|
||||
import { useRecordOneToManyFieldDetachTargetRecord } from '@/object-record/hooks/useRecordOneToManyFieldDetachTargetRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useUpdateRelationOneToManyFieldInput = () => {
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
assertFieldMetadata(
|
||||
FieldMetadataType.RELATION,
|
||||
isFieldRelation,
|
||||
fieldDefinition,
|
||||
);
|
||||
|
||||
const { objectMetadataItem: targetObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
if (!isDefined(fieldDefinition.metadata.objectMetadataNameSingular)) {
|
||||
throw new Error('ObjectMetadataNameSingular is required');
|
||||
}
|
||||
|
||||
const { objectMetadataItem: sourceObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
});
|
||||
|
||||
if (!fieldDefinition.metadata.objectMetadataNameSingular) {
|
||||
throw new Error('ObjectMetadataNameSingular is required');
|
||||
}
|
||||
|
||||
if (!isDefined(targetObjectMetadataItem)) {
|
||||
throw new Error('TargetObjectMetadataItem is required');
|
||||
}
|
||||
|
||||
const { recordOneToManyFieldDetachTargetRecord } =
|
||||
useRecordOneToManyFieldDetachTargetRecord();
|
||||
|
||||
const { recordOneToManyFieldAttachTargetRecord } =
|
||||
useRecordOneToManyFieldAttachTargetRecord();
|
||||
|
||||
const updateRelation = useRecoilCallback(
|
||||
() => async (morphItem: RecordPickerPickableMorphItem) => {
|
||||
if (
|
||||
!fieldDefinition.metadata?.relationObjectMetadataNameSingular ||
|
||||
!fieldDefinition.metadata?.targetFieldMetadataName
|
||||
) {
|
||||
throw new Error('RelationObjectMetadataNameSingular is required');
|
||||
}
|
||||
|
||||
if (!fieldDefinition.metadata?.objectMetadataNameSingular) {
|
||||
throw new Error('RelationFieldMetadata is required');
|
||||
}
|
||||
|
||||
const targetFieldMetadata = targetObjectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.name === fieldDefinition.metadata.targetFieldMetadataName,
|
||||
);
|
||||
|
||||
if (!isDefined(targetFieldMetadata)) {
|
||||
throw new Error('TargetFieldMetadata is required');
|
||||
}
|
||||
|
||||
let targetGQLFieldName: string;
|
||||
if (targetFieldMetadata.type === FieldMetadataType.MORPH_RELATION) {
|
||||
targetGQLFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldDefinition.metadata.targetFieldMetadataName,
|
||||
relationType: targetFieldMetadata.settings?.relationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
sourceObjectMetadataItem.nameSingular,
|
||||
targetObjectMetadataNamePlural: sourceObjectMetadataItem.namePlural,
|
||||
});
|
||||
} else {
|
||||
targetGQLFieldName = fieldDefinition.metadata.targetFieldMetadataName;
|
||||
}
|
||||
|
||||
if (morphItem.isSelected) {
|
||||
await recordOneToManyFieldAttachTargetRecord({
|
||||
sourceObjectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
targetObjectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
targetGQLFieldName,
|
||||
sourceRecordId: recordId,
|
||||
targetRecordId: morphItem.recordId,
|
||||
});
|
||||
} else {
|
||||
await recordOneToManyFieldDetachTargetRecord({
|
||||
sourceObjectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
targetObjectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
targetGQLFieldName,
|
||||
sourceRecordId: recordId,
|
||||
targetRecordId: morphItem.recordId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
fieldDefinition.metadata.targetFieldMetadataName,
|
||||
recordId,
|
||||
recordOneToManyFieldAttachTargetRecord,
|
||||
recordOneToManyFieldDetachTargetRecord,
|
||||
sourceObjectMetadataItem.namePlural,
|
||||
sourceObjectMetadataItem.nameSingular,
|
||||
targetObjectMetadataItem.fields,
|
||||
],
|
||||
);
|
||||
|
||||
return { updateRelation };
|
||||
};
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
type FieldRelationMetadata,
|
||||
} from '../FieldMetadata';
|
||||
|
||||
export const isFieldRelationToOneObject = (
|
||||
export const isFieldRelationManyToOne = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||
): field is FieldDefinition<FieldRelationMetadata> =>
|
||||
isFieldRelation(field) &&
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
||||
import { type FieldRelationToOneValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
// TODO: add zod
|
||||
export const isFieldRelationToOneValue = (
|
||||
export const isFieldRelationManyToOneValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldRelationToOneValue =>
|
||||
!isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue));
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
type FieldRelationMetadata,
|
||||
} from '../FieldMetadata';
|
||||
|
||||
export const isFieldRelationFromManyObjects = (
|
||||
export const isFieldRelationOneToMany = (
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||
): field is FieldDefinition<FieldRelationMetadata> =>
|
||||
isFieldRelation(field) &&
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
||||
import { type FieldRelationFromManyValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
// TODO: add zod
|
||||
export const isFieldRelationFromManyValue = (
|
||||
export const isFieldRelationOneToManyValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldRelationFromManyValue =>
|
||||
!isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue));
|
||||
+2
-2
@@ -25,7 +25,7 @@ export const SingleRecordPicker = ({
|
||||
onCancel,
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
objectNameSingulars,
|
||||
componentInstanceId,
|
||||
layoutDirection,
|
||||
dropdownWidth,
|
||||
@@ -84,7 +84,7 @@ export const SingleRecordPicker = ({
|
||||
onCancel: handleCancel,
|
||||
onCreate,
|
||||
onRecordSelected: handleRecordSelected,
|
||||
objectNameSingular,
|
||||
objectNameSingulars,
|
||||
layoutDirection,
|
||||
}}
|
||||
/>
|
||||
|
||||
+19
-14
@@ -1,5 +1,5 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { SingleRecordPickerLoadingEffect } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerLoadingEffect';
|
||||
import {
|
||||
SingleRecordPickerMenuItems,
|
||||
@@ -22,7 +22,7 @@ import { IconPlus } from 'twenty-ui/display';
|
||||
export type SingleRecordPickerMenuItemsWithSearchProps = {
|
||||
excludedRecordIds?: string[];
|
||||
onCreate?: ((searchInput?: string) => void) | (() => void);
|
||||
objectNameSingular: string;
|
||||
objectNameSingulars: string[];
|
||||
recordPickerInstanceId?: string;
|
||||
layoutDirection?: RecordPickerLayoutDirection;
|
||||
focusId: string;
|
||||
@@ -42,7 +42,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
onCancel,
|
||||
onCreate,
|
||||
onRecordSelected,
|
||||
objectNameSingular,
|
||||
objectNameSingulars,
|
||||
layoutDirection = 'search-bar-on-top',
|
||||
focusId,
|
||||
}: SingleRecordPickerMenuItemsWithSearchProps) => {
|
||||
@@ -58,19 +58,24 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
);
|
||||
|
||||
const { records } = useSingleRecordPickerRecords({
|
||||
objectNameSingulars: [objectNameSingular],
|
||||
objectNameSingulars,
|
||||
excludedRecordIds,
|
||||
});
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
const { objectMetadataItems: allObjectMetadataItems } =
|
||||
useObjectMetadataItems();
|
||||
const objectMetadataItems = allObjectMetadataItems.filter(
|
||||
(objectMetadataItem) =>
|
||||
objectNameSingulars.includes(objectMetadataItem.nameSingular),
|
||||
);
|
||||
|
||||
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const hasUpdatePermissions = objectMetadataItems.every(
|
||||
(objectMetadataItem) =>
|
||||
objectPermissionsByObjectMetadataId[objectMetadataItem.id]
|
||||
?.canUpdateObjectRecords,
|
||||
);
|
||||
|
||||
const handleCreateNew = () => {
|
||||
onCreate?.(recordPickerSearchFilter);
|
||||
@@ -81,7 +86,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
<SingleRecordPickerLoadingEffect loading={records.loading} />
|
||||
{layoutDirection === 'search-bar-on-bottom' && (
|
||||
<>
|
||||
{isDefined(onCreate) && hasObjectUpdatePermissions && (
|
||||
{isDefined(onCreate) && hasUpdatePermissions && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<CreateNewButton
|
||||
@@ -133,7 +138,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
{isDefined(onCreate) && hasObjectUpdatePermissions && (
|
||||
{isDefined(onCreate) && hasUpdatePermissions && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ const meta: Meta<typeof SingleRecordPicker> = {
|
||||
I18nFrontDecorator,
|
||||
],
|
||||
args: {
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
objectNameSingulars: [CoreObjectNameSingular.WorkspaceMember],
|
||||
componentInstanceId: 'single-record-picker',
|
||||
},
|
||||
argTypes: {
|
||||
|
||||
+9
-3
@@ -3,24 +3,30 @@ import { selectorFamily } from 'recoil';
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const recordStoreMorphManyToOneValueWithObjectNameFamilySelector =
|
||||
selectorFamily({
|
||||
key: 'recordStoreMorphManyToOneValueWithObjectNameFamilySelector',
|
||||
get:
|
||||
({
|
||||
fieldName,
|
||||
recordId,
|
||||
morphRelations,
|
||||
}: {
|
||||
fieldName: string;
|
||||
recordId: string;
|
||||
morphRelations: FieldMetadataItemRelation[];
|
||||
}) =>
|
||||
({ get }) => {
|
||||
const morphValuesWithObjectName = morphRelations.map(
|
||||
(morphRelation) => {
|
||||
const fieldName = computeMorphRelationFieldName({
|
||||
fieldName: morphRelation.sourceFieldMetadata.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return {
|
||||
objectNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
|
||||
+10
-2
@@ -3,23 +3,31 @@ import { selectorFamily } from 'recoil';
|
||||
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type RelationType } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName } from 'twenty-shared/utils';
|
||||
|
||||
export const recordStoreMorphOneToManyValueWithObjectNameFamilySelector =
|
||||
selectorFamily({
|
||||
key: 'recordStoreMorphOneToManyValueWithObjectNameFamilySelector',
|
||||
get:
|
||||
({
|
||||
fieldName,
|
||||
recordId,
|
||||
morphRelations,
|
||||
}: {
|
||||
fieldName: string;
|
||||
recordId: string;
|
||||
morphRelations: FieldMetadataItemRelation[];
|
||||
}) =>
|
||||
({ get }) => {
|
||||
const morphValuesWithObjectName = morphRelations.map(
|
||||
(morphRelation) => {
|
||||
const fieldName = computeMorphRelationFieldName({
|
||||
fieldName: morphRelation.sourceFieldMetadata.name,
|
||||
relationType: morphRelation.type as RelationType,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return {
|
||||
objectNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
|
||||
+4
-4
@@ -3,8 +3,8 @@ import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifie
|
||||
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/ui/types/guards/isFieldRelationFromManyObjects';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/ui/types/guards/isFieldRelationToOneObject';
|
||||
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
|
||||
import { isFieldRelationOneToMany } from '@/object-record/record-field/ui/types/guards/isFieldRelationOneToMany';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
@@ -39,8 +39,8 @@ export const RecordTableCellFieldContextGeneric = ({
|
||||
|
||||
// todo @guillim : adjust this to handle morph relations permissions display
|
||||
if (
|
||||
isFieldRelationToOneObject(fieldDefinition) ||
|
||||
isFieldRelationFromManyObjects(fieldDefinition)
|
||||
isFieldRelationManyToOne(fieldDefinition) ||
|
||||
isFieldRelationOneToMany(fieldDefinition)
|
||||
) {
|
||||
const relationObjectMetadataId =
|
||||
fieldDefinition.metadata.relationObjectMetadataId;
|
||||
|
||||
+22
-3
@@ -9,12 +9,13 @@ import {
|
||||
import { GRAPHQL_TYPENAME_KEY } from '@/object-record/constants/GraphqlTypenameKey';
|
||||
import { type FieldActorValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldActor } from '@/object-record/record-field/ui/types/guards/isFieldActor';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { isFieldUuid } from '@/object-record/record-field/ui/types/guards/isFieldUuid';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { buildOptimisticActorFieldValueFromCurrentWorkspaceMember } from '@/object-record/utils/buildOptimisticActorFieldValueFromCurrentWorkspaceMember';
|
||||
import { getForeignKeyNameFromRelationFieldName } from '@/object-record/utils/getForeignKeyNameFromRelationFieldName';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type ComputeOptimisticCacheRecordInputArgs = {
|
||||
@@ -39,16 +40,34 @@ export const computeOptimisticRecordFromInput = ({
|
||||
(field) => field.name === recordKey,
|
||||
);
|
||||
|
||||
const potentialJoinColumnNameFieldMetadataItem =
|
||||
const potentialRelationJoinColumnNameFieldMetadataItem =
|
||||
objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.settings?.joinColumnName === recordKey,
|
||||
);
|
||||
|
||||
const potentialMorphRelationJoinColumnNameFieldMetadataItem =
|
||||
objectMetadataItem.fields.find((field) => {
|
||||
if (!isFieldMorphRelation(field)) return false;
|
||||
|
||||
return field.morphRelations?.some((morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName: field.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return computedFieldName === recordKey.replace('Id', '');
|
||||
});
|
||||
});
|
||||
|
||||
const isUnknownField =
|
||||
!isDefined(correspondingFieldMetadataItem) &&
|
||||
!isDefined(potentialJoinColumnNameFieldMetadataItem);
|
||||
!isDefined(potentialRelationJoinColumnNameFieldMetadataItem) &&
|
||||
!isDefined(potentialMorphRelationJoinColumnNameFieldMetadataItem);
|
||||
|
||||
const isTypenameField = recordKey === GRAPHQL_TYPENAME_KEY;
|
||||
return isUnknownField && !isTypenameField;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isSystemSearchVectorField } from '@/object-record/utils/isSystemSearchVectorField';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const sanitizeRecordInput = ({
|
||||
@@ -27,10 +28,26 @@ export const sanitizeRecordInput = ({
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.settings?.joinColumnName === fieldName,
|
||||
);
|
||||
const potentialMorphRelationJoinColumnNameFieldMetadataItem =
|
||||
objectMetadataItem.fields.find((field) => {
|
||||
if (!isFieldMorphRelation(field)) return false;
|
||||
return field.morphRelations?.some((morphRelation) => {
|
||||
const computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName: field.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
return computedFieldName === fieldName.replace('Id', '');
|
||||
});
|
||||
});
|
||||
|
||||
if (
|
||||
!isDefined(fieldMetadataItem) &&
|
||||
!isDefined(potentialJoinColumnNameFieldMetadataItem)
|
||||
!isDefined(potentialJoinColumnNameFieldMetadataItem) &&
|
||||
!isDefined(potentialMorphRelationJoinColumnNameFieldMetadataItem)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user