feat: merge records (#13436)

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
neo773
2025-07-31 15:36:36 +05:30
committed by GitHub
parent 00d12e854a
commit 06e0814ce8
45 changed files with 2047 additions and 29 deletions
@@ -0,0 +1,103 @@
import { useCallback, useState } from 'react';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useMergeManyRecordsMutation } from '@/object-record/hooks/useMergeManyRecordsMutation';
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { getMergeManyRecordsMutationResponseField } from '@/object-record/utils/getMergeManyRecordsMutationResponseField';
export type MergeManySettings = {
conflictPriorityIndex: number;
};
export type UseMergeManyRecordsProps = {
objectNameSingular: string;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
};
export const useMergeManyRecords = <
MergedObjectRecord extends ObjectRecord = ObjectRecord,
>({
objectNameSingular,
recordGqlFields,
}: UseMergeManyRecordsProps) => {
const apolloCoreClient = useApolloCoreClient();
const [loading, setLoading] = useState(false);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const computedRecordGqlFields =
recordGqlFields ?? generateDepthOneRecordGqlFields({ objectMetadataItem });
const { mergeManyRecordsMutation } = useMergeManyRecordsMutation({
objectNameSingular,
recordGqlFields: computedRecordGqlFields,
});
const { refetchAggregateQueries } = useRefetchAggregateQueries({
objectMetadataNamePlural: objectMetadataItem.namePlural,
});
type MergeManyRecordsProps = {
recordIds: string[];
mergeSettings: MergeManySettings;
preview?: boolean;
};
const mergeManyRecords = useCallback(
async ({
recordIds,
mergeSettings,
preview = false,
}: MergeManyRecordsProps): Promise<MergedObjectRecord | null> => {
setLoading(true);
const mutationResponseField = getMergeManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);
try {
const mergedObject = await apolloCoreClient.mutate({
mutation: mergeManyRecordsMutation,
variables: {
ids: recordIds,
conflictPriorityIndex: mergeSettings.conflictPriorityIndex,
dryRun: preview,
},
// Prevent cache updates during dry run to avoid overwriting original record data
...(preview && {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore',
}),
});
setLoading(false);
if (!preview) {
await refetchAggregateQueries();
}
return mergedObject.data?.[mutationResponseField] ?? null;
} catch (error) {
setLoading(false);
throw error;
}
},
[
apolloCoreClient,
mergeManyRecordsMutation,
objectMetadataItem.namePlural,
refetchAggregateQueries,
],
);
return {
mergeManyRecords,
loading,
};
};
@@ -0,0 +1,62 @@
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 { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields';
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getMergeManyRecordsMutationResponseField } from '@/object-record/utils/getMergeManyRecordsMutationResponseField';
import { capitalize } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useMergeManyRecordsMutation = ({
objectNameSingular,
recordGqlFields,
}: {
objectNameSingular: string;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
}) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const appliedRecordGqlFields =
recordGqlFields ??
generateDepthOneRecordGqlFields({
objectMetadataItem,
});
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
if (isUndefinedOrNull(objectMetadataItem)) {
return { mergeManyRecordsMutation: EMPTY_MUTATION };
}
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
const mutationResponseField = getMergeManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);
const mergeManyRecordsMutation = gql`
mutation merge${capitalizedObjectName}s($ids: [UUID!]!, $conflictPriorityIndex: Int!, $dryRun: Boolean) {
${mutationResponseField}(ids: $ids, conflictPriorityIndex: $conflictPriorityIndex, dryRun: $dryRun) ${mapObjectMetadataToGraphQLQuery(
{
objectMetadataItems,
objectMetadataItem,
recordGqlFields: appliedRecordGqlFields,
objectPermissionsByObjectMetadataId,
},
)}
}
`;
return {
mergeManyRecordsMutation,
};
};