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 <remi@labox-apps.com>
This commit is contained in:
Rémi Delassus
2026-02-11 10:55:19 +01:00
committed by GitHub
parent 41e413d7b1
commit 859241d237
2 changed files with 117 additions and 12 deletions
@@ -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[];
}) => (
<RecoilRoot
initializeState={({ set }) => {
set(
commandMenuNavigationMorphItemsByPageState,
new Map([
[
pageId,
initialRecordIds.map((recordId) => ({
objectMetadataId,
recordId,
})),
],
]),
);
}}
>
{children}
</RecoilRoot>
);
const renderHooks = (initialRecordIds: string[]) =>
renderHook(
() => {
const { updateCommandMenuNavigationMorphItemsByPage } =
useCommandMenuUpdateNavigationMorphItemsByPage();
const commandMenuNavigationMorphItemsByPage = useRecoilValue(
commandMenuNavigationMorphItemsByPageState,
);
return {
updateCommandMenuNavigationMorphItemsByPage,
commandMenuNavigationMorphItemsByPage,
};
},
{
wrapper: ({ children }) => (
<Wrapper initialRecordIds={initialRecordIds}>{children}</Wrapper>
),
},
);
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',
},
]);
});
});
@@ -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);