diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenMergeRecordsPageInCommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useOpenMergeRecordsPageInCommandMenu.tsx new file mode 100644 index 0000000000..2ef26c25d5 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenMergeRecordsPageInCommandMenu.tsx @@ -0,0 +1,43 @@ +import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useLoadSelectedRecordsInContextStore } from '@/object-record/hooks/useLoadSelectedRecordsInContextStore'; +import { msg, t } from '@lingui/core/macro'; +import { IconArrowMerge } from 'twenty-ui/display'; + +type UseOpenMergeRecordsPageInCommandMenuProps = { + objectNameSingular: string; + objectRecordIds: string[]; +}; + +export const useOpenMergeRecordsPageInCommandMenu = ({ + objectNameSingular, + objectRecordIds, +}: UseOpenMergeRecordsPageInCommandMenuProps) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { navigateCommandMenu } = useNavigateCommandMenu(); + + const { loadSelectedRecordsInContextStore } = + useLoadSelectedRecordsInContextStore({ + objectNameSingular, + objectRecordIds, + objectMetadataItemId: objectMetadataItem.id, + }); + + const openMergeRecordsPageInCommandMenu = async () => { + await loadSelectedRecordsInContextStore(); + + navigateCommandMenu({ + page: CommandMenuPages.MergeRecords, + pageTitle: t(msg`Merge records`), + pageIcon: IconArrowMerge, + }); + }; + + return { + openMergeRecordsPageInCommandMenu, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLoadSelectedRecordsInContextStore.tsx b/packages/twenty-front/src/modules/object-record/hooks/useLoadSelectedRecordsInContextStore.tsx new file mode 100644 index 0000000000..65dd4cfdf3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useLoadSelectedRecordsInContextStore.tsx @@ -0,0 +1,68 @@ +import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; +import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { useRecoilCallback } from 'recoil'; + +type UseLoadSelectedRecordsInContextStoreProps = { + objectNameSingular: string; + objectRecordIds: string[]; + objectMetadataItemId: string; +}; + +export const useLoadSelectedRecordsInContextStore = ({ + objectNameSingular, + objectRecordIds, + objectMetadataItemId, +}: UseLoadSelectedRecordsInContextStoreProps) => { + const { upsertRecords } = useUpsertRecordsInStore(); + + const { findManyRecordsLazy } = useLazyFindManyRecords({ + objectNameSingular, + filter: { + id: { + in: objectRecordIds, + }, + }, + }); + + const loadSelectedRecordsInContextStore = useRecoilCallback( + ({ set }) => { + return async () => { + set( + contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({ + instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, + }), + objectMetadataItemId, + ); + + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, + }), + { + mode: 'selection', + selectedRecordIds: objectRecordIds, + }, + ); + + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID, + }), + objectRecordIds.length, + ); + + const records = await findManyRecordsLazy(); + upsertRecords(records.records); + }; + }, + [objectRecordIds, objectMetadataItemId, findManyRecordsLazy, upsertRecords], + ); + + return { + loadSelectedRecordsInContextStore, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts index f5680ba773..ca267a8098 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useMergeManyRecords.ts @@ -4,10 +4,13 @@ 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 { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDuplicatesRecordsQuery'; +import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery'; 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'; +import { getOperationName } from '@apollo/client/utilities'; export type MergeManySettings = { conflictPriorityIndex: number; @@ -43,6 +46,15 @@ export const useMergeManyRecords = < objectMetadataNamePlural: objectMetadataItem.namePlural, }); + const { findOneRecordQuery } = useFindOneRecordQuery({ + objectNameSingular, + recordGqlFields: computedRecordGqlFields, + }); + + const { findDuplicateRecordsQuery } = useFindDuplicateRecordsQuery({ + objectNameSingular, + }); + type MergeManyRecordsProps = { recordIds: string[]; mergeSettings: MergeManySettings; @@ -74,6 +86,10 @@ export const useMergeManyRecords = < fetchPolicy: 'no-cache', errorPolicy: 'ignore', }), + refetchQueries: [ + getOperationName(findOneRecordQuery) ?? '', + getOperationName(findDuplicateRecordsQuery) ?? '', + ].filter(Boolean), }); setLoading(false); @@ -90,6 +106,8 @@ export const useMergeManyRecords = < }, [ apolloCoreClient, + findDuplicateRecordsQuery, + findOneRecordQuery, mergeManyRecordsMutation, objectMetadataItem.namePlural, refetchAggregateQueries, diff --git a/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx b/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx index 94bea90da8..c1e1860fc4 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx +++ b/packages/twenty-front/src/modules/object-record/record-merge/components/MergePreviewTab.tsx @@ -48,6 +48,7 @@ export const MergePreviewTab = ({ targetObjectNameSingular: objectNameSingular, id: mergePreviewRecord.id, }} + showDuplicatesSection={false} isInRightDrawer={true} /> diff --git a/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordTab.tsx b/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordTab.tsx index 4d0919e59a..a30f3b794c 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordTab.tsx +++ b/packages/twenty-front/src/modules/object-record/record-merge/components/MergeRecordTab.tsx @@ -26,6 +26,7 @@ export const MergeRecordTab = ({ id: recordId, }} isInRightDrawer={true} + showDuplicatesSection={false} /> ); diff --git a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordRelationships.ts b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordRelationships.ts index cb3f4c094f..10abf3fdad 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordRelationships.ts +++ b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordRelationships.ts @@ -1,9 +1,13 @@ +import { useLazyQuery } from '@apollo/client'; import { useEffect, useMemo, useState } from 'react'; import { useRecoilCallback } from 'recoil'; +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; +import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; import { mergeRecordRelationshipData } from '@/object-record/record-merge/utils/mergeRelationshipData'; import { buildFindOneRecordForShowPageOperationSignature } from '@/object-record/record-show/graphql/operations/factories/findOneRecordForShowPageOperationSignatureFactory'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; @@ -46,20 +50,23 @@ export const useMergeRecordRelationships = ({ }).fields; }, [objectMetadataItem, objectMetadataItems]); - const { findManyRecordsLazy } = useLazyFindManyRecords({ + const apolloCoreClient = useApolloCoreClient(); + + const { findManyRecordsQuery } = useFindManyRecordsQuery({ objectNameSingular, recordGqlFields, - filter: { - id: { - in: selectedRecords.map((record) => record.id), - }, - }, }); + const [findManyRecords] = useLazyQuery( + findManyRecordsQuery, + { + fetchPolicy: 'cache-first', + client: apolloCoreClient, + }, + ); + useEffect(() => { if (selectedRecords.length === 0 || !previewRecordId) { - setCompleteRecords([]); - setError(undefined); return; } @@ -68,7 +75,29 @@ export const useMergeRecordRelationships = ({ setError(undefined); try { - const { records } = await findManyRecordsLazy(); + const result = await findManyRecords({ + variables: { + filter: { + id: { + in: selectedRecords.map((record) => record.id), + }, + }, + }, + }); + + const records = getRecordsFromRecordConnection({ + recordConnection: { + edges: result?.data?.[objectMetadataItem.namePlural]?.edges ?? [], + pageInfo: result?.data?.[objectMetadataItem.namePlural] + ?.pageInfo ?? { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '', + endCursor: '', + }, + }, + }); + setCompleteRecords(records || []); } catch (fetchError) { const errorMessage = @@ -78,14 +107,18 @@ export const useMergeRecordRelationships = ({ setError( new Error(`Failed to merge record relationships: ${errorMessage}`), ); - setCompleteRecords([]); } finally { setIsLoading(false); } }; fetchCompleteRecords(); - }, [selectedRecords, findManyRecordsLazy, previewRecordId]); + }, [ + selectedRecords, + findManyRecords, + previewRecordId, + objectMetadataItem.namePlural, + ]); const mergedRelationshipData = useMemo((): MergedRelationshipData => { return mergeRecordRelationshipData( diff --git a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsActions.ts b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsActions.ts index 64cce40001..c4cec46fe7 100644 --- a/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-merge/hooks/useMergeRecordsActions.ts @@ -1,15 +1,12 @@ import { useLingui } from '@lingui/react/macro'; import { useRecoilValue } from 'recoil'; -import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu'; +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore'; -import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; -import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; import { useMergeManyRecords } from '@/object-record/hooks/useMergeManyRecords'; -import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; -import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; +import { AppPath } from '@/types/AppPath'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useNavigateApp } from '~/hooks/useNavigateApp'; import { mergeSettingsState } from '../states/mergeSettingsState'; type UseMergeRecordsActionsProps = { @@ -32,25 +29,9 @@ export const useMergeRecordsActions = ({ const { t } = useLingui(); const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar(); - const { openRecordInCommandMenu } = useOpenRecordInCommandMenu(); - const { objectNamePlural } = useObjectNamePluralFromSingular({ - objectNameSingular, - }); - const contextStoreCurrentViewId = useRecoilComponentValueV2( - contextStoreCurrentViewIdComponentState, - ); - - if (!contextStoreCurrentViewId) { - throw new Error('Current view ID is not defined'); - } - - const { resetTableRowSelection } = useRecordTable({ - recordTableId: getRecordIndexIdFromObjectNamePluralAndViewId( - objectNamePlural, - contextStoreCurrentViewId, - ), - }); + const { closeCommandMenu } = useCommandMenu(); + const navigate = useNavigateApp(); const handleMergeRecords = async () => { try { const mergedRecord = await mergeManyRecords({ @@ -68,11 +49,11 @@ export const useMergeRecordsActions = ({ enqueueSuccessSnackBar({ message: t`Successfully merged ${recordCount} records`, }); + closeCommandMenu(); - resetTableRowSelection(); - openRecordInCommandMenu({ + navigate(AppPath.RecordShowPage, { objectNameSingular, - recordId: mergedRecord.id, + objectRecordId: mergedRecord.id, }); } catch (error) { enqueueErrorSnackBar({ diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx index a67da45d10..1f3bc826c5 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/CardComponents.tsx @@ -51,7 +51,13 @@ type CardComponentProps = { isInRightDrawer?: boolean; }; -type CardComponentType = (props: CardComponentProps) => JSX.Element | null; +type CardComponentType = ( + props: CardComponentProps | FieldsCardComponentProps, +) => JSX.Element | null; + +type FieldsCardComponentProps = CardComponentProps & { + showDuplicatesSection?: boolean; +}; const LoadingSkeleton = () => { const theme = useTheme(); @@ -103,11 +109,16 @@ export const CardComponents: Record = { /> ), - [CardType.FieldCard]: ({ targetableObject, isInRightDrawer }) => ( + [CardType.FieldCard]: ({ + targetableObject, + isInRightDrawer, + showDuplicatesSection, + }: FieldsCardComponentProps) => ( ), diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx index 3b1f57cb0d..0e9763e62c 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx @@ -26,6 +26,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; type FieldsCardProps = { objectNameSingular: string; objectRecordId: string; + showDuplicatesSection?: boolean; }; const INPUT_ID_PREFIX = 'fields-card'; @@ -33,6 +34,7 @@ const INPUT_ID_PREFIX = 'fields-card'; export const FieldsCard = ({ objectNameSingular, objectRecordId, + showDuplicatesSection = true, }: FieldsCardProps) => { const { recordLoading, labelIdentifierFieldMetadataItem, isPrefetchLoading } = useRecordShowContainerData({ @@ -204,10 +206,12 @@ export const FieldsCard = ({ )} - + {showDuplicatesSection && ( + + )} {boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => ( record.id), + objectRecordId, + ]; + + const { openMergeRecordsPageInCommandMenu } = + useOpenMergeRecordsPageInCommandMenu({ + objectNameSingular, + objectRecordIds: duplicateRecordIds, + }); + if (!queryResults || !queryResults[0] || queryResults[0].length === 0) return null; return ( - + + } + /> {queryResults[0].slice(0, 5).map((duplicateRecord) => (