Morph-picker-MultiSelect-picker (#14113)
# 🚀 Multi-Select Morph Relations / One to Many Adds **multi-select picker support** for morph relations on the **One To Many** side, allowing users to select multiple related records from different object types. ### 🔑 Main Changes (TL;DR) - **New Component**: `MorphRelationOneToManyFieldInput` for multi-selection - **Enhanced Hooks**: `useAttachMorphRelatedRecordFromRecord` & `useDetachMorphRelatedRecordFromRecord` - **Better UX**: Optimistic updates, improved search, loading states - **24 files changed**: Mostly additions (1,277 lines added, 56 removed) **Summary**: This adds multi-select capability to morph relations with proper state management and optimistic updates. The changes are mostly additive and follow existing patterns. **TODO** : - optimistic rendering not working currently - single select picker (many to one)
This commit is contained in:
+193
@@ -0,0 +1,193 @@
|
||||
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 { computeMorphRelationFieldName, 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 computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
relationDirection: fieldDefinition.metadata.relationType,
|
||||
nameSingular: relatedObjectMetadataItem.nameSingular,
|
||||
namePlural: relatedObjectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const currentMorphFieldValue = parentRecord[computedFieldName];
|
||||
|
||||
const objectRecordFromCache = getRecordFromCache({
|
||||
objectMetadataItem: relatedObjectMetadataItem,
|
||||
recordId: relatedRecordId,
|
||||
cache: apolloCoreClient.cache,
|
||||
objectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
|
||||
if (!isDefined(objectRecordFromCache)) {
|
||||
return;
|
||||
}
|
||||
|
||||
set(recordStoreFamilyState(recordId), {
|
||||
...parentRecord,
|
||||
[computedFieldName]: [
|
||||
...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 };
|
||||
};
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
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 { computeMorphRelationFieldName, 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 computedFieldName = computeMorphRelationFieldName({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
relationDirection: fieldDefinition.metadata.relationType,
|
||||
nameSingular: relatedObjectMetadataItem.nameSingular,
|
||||
namePlural: relatedObjectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const currentMorphFieldValue = parentRecord[
|
||||
computedFieldName
|
||||
] 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,
|
||||
[computedFieldName]: 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 };
|
||||
};
|
||||
+9
-14
@@ -1,8 +1,7 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier';
|
||||
import { useDoObjectMetadataItemsExist } from '@/object-metadata/hooks/useDoObjectMetadataItemsExist';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { type WatchQueryFetchPolicy } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
@@ -14,7 +13,9 @@ import {
|
||||
} from '~/generated/graphql';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & {
|
||||
// maybe we should look at ObjectMetadataItemIdentifier to update the API even though there are many location to update
|
||||
export type UseSearchRecordsParams = {
|
||||
objectNameSingulars: string[];
|
||||
limit?: number;
|
||||
onError?: (error?: Error) => void;
|
||||
skip?: boolean;
|
||||
@@ -24,7 +25,7 @@ export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & {
|
||||
};
|
||||
|
||||
export const useObjectRecordSearchRecords = ({
|
||||
objectNameSingular,
|
||||
objectNameSingulars,
|
||||
searchInput,
|
||||
limit,
|
||||
skip,
|
||||
@@ -32,30 +33,25 @@ export const useObjectRecordSearchRecords = ({
|
||||
fetchPolicy,
|
||||
}: UseSearchRecordsParams) => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
const areDefined = useDoObjectMetadataItemsExist(objectNameSingulars);
|
||||
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const { data, loading, error, previousData } = useSearchQuery({
|
||||
skip:
|
||||
skip ||
|
||||
!objectMetadataItem ||
|
||||
!currentWorkspaceMember ||
|
||||
!isDefined(searchInput),
|
||||
skip || !areDefined || !currentWorkspaceMember || !isDefined(searchInput),
|
||||
variables: {
|
||||
searchInput: searchInput ?? '',
|
||||
limit: limit ?? MAX_SEARCH_RESULTS,
|
||||
filter: filter ?? {},
|
||||
includedObjectNameSingulars: [objectNameSingular],
|
||||
includedObjectNameSingulars: objectNameSingulars,
|
||||
},
|
||||
fetchPolicy: fetchPolicy,
|
||||
client: apolloCoreClient,
|
||||
onError: (error) => {
|
||||
logError(
|
||||
`useSearchRecords for "${objectMetadataItem.namePlural}" error : ` +
|
||||
`useSearchRecords for "${objectNameSingulars.join(', ')}" error : ` +
|
||||
error,
|
||||
);
|
||||
enqueueErrorSnackBar({
|
||||
@@ -72,7 +68,6 @@ export const useObjectRecordSearchRecords = ({
|
||||
);
|
||||
|
||||
return {
|
||||
objectMetadataItem,
|
||||
searchRecords,
|
||||
loading,
|
||||
error,
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CustomError } from '@/error-handler/CustomError';
|
||||
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 { 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,
|
||||
};
|
||||
};
|
||||
+8
-30
@@ -1,15 +1,11 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
|
||||
import { type RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { getUpdateOneRecordMutationResponseField } from '@/object-record/utils/getUpdateOneRecordMutationResponseField';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { generateUpdateOneRecordMutation } from '@/object-record/multiple-objects/utils/generateUpdateOneRecordMutation';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const useUpdateOneRecordMutation = ({
|
||||
@@ -33,31 +29,13 @@ export const useUpdateOneRecordMutation = ({
|
||||
return { updateOneRecordMutation: EMPTY_MUTATION };
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
)}
|
||||
}
|
||||
`;
|
||||
const updateOneRecordMutation = generateUpdateOneRecordMutation({
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordGqlFields,
|
||||
computeReferences,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
});
|
||||
|
||||
return {
|
||||
updateOneRecordMutation,
|
||||
|
||||
Reference in New Issue
Block a user