From 859241d2372058957d1c8c88565c0f884b71df2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Delassus?= Date: Wed, 11 Feb 2026 10:55:19 +0100 Subject: [PATCH] Fix merge records page accumulating duplicate morph items (#17705) ## Why When opening **Merge records** repeatedly, morph items for the same command-menu page were appended instead of replaced. This could produce duplicated IDs (e.g. `[A,B,B,A]`) in the merge flow and extra duplicate tabs in the UI. ## What - Update `useCommandMenuUpdateNavigationMorphItemsByPage` to replace page morph items instead of appending existing ones. - Add regression tests covering: - replacing existing morph items for the same page - keeping only the latest payload when called twice for the same page ## Notes I could not run the full workspace tests locally in this environment because of existing test/build setup issues unrelated to this change (missing `packages/twenty-front/tsconfig.spec.json` and `temporal-polyfill` resolution in dependent tasks). Co-authored-by: remi --- ...uUpdateNavigationMorphItemsByPage.test.tsx | 113 ++++++++++++++++++ ...ndMenuUpdateNavigationMorphItemsByPage.tsx | 16 +-- 2 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuUpdateNavigationMorphItemsByPage.test.tsx diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuUpdateNavigationMorphItemsByPage.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuUpdateNavigationMorphItemsByPage.test.tsx new file mode 100644 index 0000000000..c75feaaddd --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuUpdateNavigationMorphItemsByPage.test.tsx @@ -0,0 +1,113 @@ +import { useCommandMenuUpdateNavigationMorphItemsByPage } from '@/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage'; +import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { RecoilRoot, useRecoilValue } from 'recoil'; + +const pageId = 'merge-page-id'; +const objectMetadataId = 'company-metadata-id'; + +const Wrapper = ({ + children, + initialRecordIds, +}: { + children: React.ReactNode; + initialRecordIds: string[]; +}) => ( + { + set( + commandMenuNavigationMorphItemsByPageState, + new Map([ + [ + pageId, + initialRecordIds.map((recordId) => ({ + objectMetadataId, + recordId, + })), + ], + ]), + ); + }} + > + {children} + +); + +const renderHooks = (initialRecordIds: string[]) => + renderHook( + () => { + const { updateCommandMenuNavigationMorphItemsByPage } = + useCommandMenuUpdateNavigationMorphItemsByPage(); + const commandMenuNavigationMorphItemsByPage = useRecoilValue( + commandMenuNavigationMorphItemsByPageState, + ); + + return { + updateCommandMenuNavigationMorphItemsByPage, + commandMenuNavigationMorphItemsByPage, + }; + }, + { + wrapper: ({ children }) => ( + {children} + ), + }, + ); + +describe('useCommandMenuUpdateNavigationMorphItemsByPage', () => { + it('should replace existing items for a page instead of appending', async () => { + const { result } = renderHooks(['record-1', 'record-2']); + + await act(async () => { + await result.current.updateCommandMenuNavigationMorphItemsByPage({ + pageId, + objectMetadataId, + objectRecordIds: ['record-2', 'record-1'], + }); + }); + + expect( + result.current.commandMenuNavigationMorphItemsByPage.get(pageId), + ).toEqual([ + { + objectMetadataId, + recordId: 'record-2', + }, + { + objectMetadataId, + recordId: 'record-1', + }, + ]); + }); + + it('should keep only the latest payload when called twice for the same page', async () => { + const { result } = renderHooks([]); + + await act(async () => { + await result.current.updateCommandMenuNavigationMorphItemsByPage({ + pageId, + objectMetadataId, + objectRecordIds: ['record-1', 'record-2'], + }); + await result.current.updateCommandMenuNavigationMorphItemsByPage({ + pageId, + objectMetadataId, + objectRecordIds: ['record-2', 'record-1'], + }); + }); + + expect( + result.current.commandMenuNavigationMorphItemsByPage.get(pageId), + ).toEqual([ + { + objectMetadataId, + recordId: 'record-2', + }, + { + objectMetadataId, + recordId: 'record-1', + }, + ]); + }); +}); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage.tsx index bc1b248ea7..faf0b0f226 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuUpdateNavigationMorphItemsByPage.tsx @@ -1,5 +1,4 @@ import { commandMenuNavigationMorphItemsByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsByPageState'; -import { isNonEmptyArray } from '@sniptt/guards'; import { useRecoilCallback } from 'recoil'; type UpdateNavigationMorphItemsByPageParams = { @@ -20,17 +19,10 @@ export const useCommandMenuUpdateNavigationMorphItemsByPage = () => { .getLoadable(commandMenuNavigationMorphItemsByPageState) .getValue(); - const currentMorphItemsForPage = currentMorphItems.get(pageId); - - const newMorphItems = [ - ...(isNonEmptyArray(currentMorphItemsForPage) - ? currentMorphItemsForPage - : []), - ...objectRecordIds.map((recordId) => ({ - objectMetadataId, - recordId, - })), - ]; + const newMorphItems = objectRecordIds.map((recordId) => ({ + objectMetadataId, + recordId, + })); const newMorphItemsMap = new Map(currentMorphItems); newMorphItemsMap.set(pageId, newMorphItems);