Files
twenty/packages/twenty-front/src/modules/side-panel/hooks/useSidePanelHistory.ts
T
BugIsGod 176e81cd76 fix: clear navigation stack immediately in goBackFromSidePanel (#19153)
Fixes: #19152 

## Summary
goBackFromSidePanel returned early without writing the new (empty) stack
to the store, leaving stale navigation state. And it caused
openRecordInSidePanel to think the record was already open and skip
reopening.
<img width="587" height="405" alt="image"
src="https://github.com/user-attachments/assets/eb36f4c6-43ca-4c08-891a-c592ff673837"
/>

## Before


https://github.com/user-attachments/assets/03d1e24a-6d1e-4efc-93c1-72be0bc31a89


## After
When close the side panel with Escape, now it can be opened by clicking
arrow button again.




https://github.com/user-attachments/assets/ae700d41-4f81-4d1a-af9a-f97f31f489a6

---------

Co-authored-by: Devessier <baptiste@devessier.fr>
2026-03-31 11:29:03 +00:00

178 lines
5.7 KiB
TypeScript

import { useCallback } from 'react';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { hasUserSelectedSidePanelListItemState } from '@/side-panel/states/hasUserSelectedSidePanelListItemState';
import { sidePanelNavigationMorphItemsByPageState } from '@/side-panel/states/sidePanelNavigationMorphItemsByPageState';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { sidePanelSubPageStackComponentState } from '@/side-panel/states/sidePanelSubPageStackComponentState';
import { getShowPageTabListComponentId } from '@/ui/layout/show-page/utils/getShowPageTabListComponentId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { isNonEmptyArray } from '@sniptt/guards';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
export const useSidePanelHistory = () => {
const store = useStore();
const { closeSidePanelMenu } = useSidePanelMenu();
const cleanupCurrentPage = useCallback(() => {
const currentNavigationStack = store.get(
sidePanelNavigationStackState.atom,
);
const currentMorphItems = store.get(
sidePanelNavigationMorphItemsByPageState.atom,
);
if (currentNavigationStack.length > 0) {
const removedItem = currentNavigationStack.at(-1);
if (isDefined(removedItem)) {
const newMorphItems = new Map(currentMorphItems);
newMorphItems.delete(removedItem.pageId);
store.set(sidePanelNavigationMorphItemsByPageState.atom, newMorphItems);
store.set(
sidePanelSubPageStackComponentState.atomFamily({
instanceId: removedItem.pageId,
}),
[],
);
const morphItems = currentMorphItems.get(removedItem.pageId);
if (isNonEmptyArray(morphItems)) {
store.set(
activeTabIdComponentState.atomFamily({
instanceId: getShowPageTabListComponentId({
pageId: removedItem.pageId,
targetObjectId: morphItems[0].recordId,
}),
}),
null,
);
}
}
}
}, [store]);
const goBackFromSidePanel = useCallback(() => {
cleanupCurrentPage();
const currentNavigationStack = store.get(
sidePanelNavigationStackState.atom,
);
const newNavigationStack = currentNavigationStack.slice(0, -1);
const lastNavigationStackItem = newNavigationStack.at(-1);
store.set(sidePanelNavigationStackState.atom, newNavigationStack);
if (!isDefined(lastNavigationStackItem)) {
closeSidePanelMenu();
return;
}
store.set(sidePanelPageState.atom, lastNavigationStackItem.page);
store.set(sidePanelPageInfoState.atom, {
title: lastNavigationStackItem.pageTitle,
Icon: lastNavigationStackItem.pageIcon,
instanceId: lastNavigationStackItem.pageId,
});
store.set(hasUserSelectedSidePanelListItemState.atom, false);
}, [cleanupCurrentPage, closeSidePanelMenu, store]);
const goBackOneSubPageOrMainPage = useCallback(() => {
const currentPageInfo = store.get(sidePanelPageInfoState.atom);
const subPageStack = store.get(
sidePanelSubPageStackComponentState.atomFamily({
instanceId: currentPageInfo.instanceId,
}),
);
if (isNonEmptyArray(subPageStack)) {
store.set(
sidePanelSubPageStackComponentState.atomFamily({
instanceId: currentPageInfo.instanceId,
}),
subPageStack.slice(0, -1),
);
return;
}
goBackFromSidePanel();
}, [goBackFromSidePanel, store]);
const navigateSidePanelHistory = useCallback(
(pageIndex: number) => {
const currentNavigationStack = store.get(
sidePanelNavigationStackState.atom,
);
const newNavigationStack = currentNavigationStack.slice(0, pageIndex + 1);
store.set(sidePanelNavigationStackState.atom, newNavigationStack);
const newNavigationStackItem = newNavigationStack.at(-1);
if (!isDefined(newNavigationStackItem)) {
throw new Error(
`No side panel navigation stack item found for index ${pageIndex}`,
);
}
store.set(sidePanelPageState.atom, newNavigationStackItem.page);
store.set(sidePanelPageInfoState.atom, {
title: newNavigationStackItem.pageTitle,
Icon: newNavigationStackItem.pageIcon,
instanceId: newNavigationStackItem.pageId,
});
const currentMorphItems = store.get(
sidePanelNavigationMorphItemsByPageState.atom,
);
for (const [pageId, morphItems] of currentMorphItems.entries()) {
if (!newNavigationStack.some((item) => item.pageId === pageId)) {
store.set(
sidePanelSubPageStackComponentState.atomFamily({
instanceId: pageId,
}),
[],
);
store.set(
activeTabIdComponentState.atomFamily({
instanceId: getShowPageTabListComponentId({
pageId,
targetObjectId: morphItems[0].recordId,
}),
}),
null,
);
}
}
const newMorphItems = new Map(
Array.from(currentMorphItems.entries()).filter(([pageId]) =>
newNavigationStack.some((item) => item.pageId === pageId),
),
);
store.set(sidePanelNavigationMorphItemsByPageState.atom, newMorphItems);
store.set(hasUserSelectedSidePanelListItemState.atom, false);
},
[store],
);
return {
goBackFromSidePanel,
goBackOneSubPageOrMainPage,
navigateSidePanelHistory,
};
};