From a215d3cbdfdfa79051925d3f70b10e524a36a6e5 Mon Sep 17 00:00:00 2001 From: Weiko Date: Mon, 29 Jun 2026 14:14:42 +0200 Subject: [PATCH] fix: prevent crash when leaving dashboard edit mode with side panel open (#22292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem When creating or editing a dashboard and opening a page layout side panel (clicking the **"Add widget"** box, or editing a widget via grid edition), clicking another nav item like **"Opportunities"** crashes the whole app ("Sorry, something went wrong"). ### Steps to reproduce 1. Go to `/objects/dashboards` and click **"+ Add new"** to create a dashboard. 2. Click the **"Add widget"** box (or add a widget via grid edition and open its settings). 3. Click another nav item such as **"Opportunities"**. 4. The app crashes. ## Root cause The side panel stays mounted during its close animation, but the main context store has already switched to the new page (the record index has no single targeted record). The still-mounted page layout side panel page (`SidePanelPageLayoutDashboardWidgetTypeSelect`, chart settings, etc.) re-renders and calls `usePageLayoutIdFromContextStore`, which throws `Error: Only one record should be selected`. With no local error boundary, the throw propagates to the top-level boundary and crashes the app. ## Fix `SidePanelRouter` now skips rendering page layout side panel pages whenever the main context store has no single targeted record — the same condition `usePageLayoutIdFromContextStore` requires to not throw. During navigation the panel closes cleanly instead of crashing. This is safe because `usePageLayoutIdFromContextStore` unconditionally throws without a single-record selection, so the guard can only skip pages that would otherwise crash — it cannot break a currently-working flow. The guard uses the existing `isPageLayoutSidePanelPage` helper, so it covers all page layout side panel pages (widget type select, chart / iframe / record table settings, record page field settings, etc.). ## Testing - Reproduced the crash in the running app, applied the fix, and confirmed navigating to Opportunities from both the **widget type select** and the **chart settings** panels now lands on the Opportunities list with no console or page errors. - `oxlint --type-aware` and `nx typecheck twenty-front` both pass. --- .../side-panel/components/SidePanelRouter.tsx | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelRouter.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelRouter.tsx index 55faff84ac..885e23987d 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelRouter.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelRouter.tsx @@ -1,11 +1,16 @@ import { CommandMenuContextProvider } from '@/command-menu-item/contexts/CommandMenuContextProvider'; +import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; +import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { SidePanelContainer } from '@/side-panel/components/SidePanelContainer'; import { SidePanelSubPageRouter } from '@/side-panel/components/SidePanelSubPageRouter'; import { SidePanelTopBar } from '@/side-panel/components/SidePanelTopBar'; import { SIDE_PANEL_PAGES_CONFIG } from '@/side-panel/constants/SidePanelPagesConfig'; +import { isPageLayoutSidePanelPage } from '@/side-panel/pages/page-layout/utils/isPageLayoutSidePanelPage'; import { SidePanelPageComponentInstanceContext } from '@/side-panel/states/contexts/SidePanelPageComponentInstanceContext'; import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState'; import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState'; +import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { styled } from '@linaria/react'; import { motion } from 'framer-motion'; @@ -23,9 +28,29 @@ export const SidePanelRouter = () => { const sidePanelPage = useAtomStateValue(sidePanelPageState); const sidePanelPageInfo = useAtomStateValue(sidePanelPageInfoState); - const rawPageComponent = isDefined(sidePanelPage) - ? SIDE_PANEL_PAGES_CONFIG.get(sidePanelPage) - : null; + const contextStoreTargetedRecordsRule = useAtomComponentStateValue( + contextStoreTargetedRecordsRuleComponentState, + MAIN_CONTEXT_STORE_INSTANCE_ID, + ); + const contextStoreCurrentObjectMetadataItemId = useAtomComponentStateValue( + contextStoreCurrentObjectMetadataItemIdComponentState, + MAIN_CONTEXT_STORE_INSTANCE_ID, + ); + + const hasSingleTargetedRecord = + contextStoreTargetedRecordsRule.mode === 'selection' && + contextStoreTargetedRecordsRule.selectedRecordIds.length === 1; + + const shouldSkipPageLayoutPage = + isDefined(sidePanelPage) && + isPageLayoutSidePanelPage(sidePanelPage) && + (!isDefined(contextStoreCurrentObjectMetadataItemId) || + !hasSingleTargetedRecord); + + const rawPageComponent = + isDefined(sidePanelPage) && !shouldSkipPageLayoutPage + ? SIDE_PANEL_PAGES_CONFIG.get(sidePanelPage) + : null; const sidePanelPageComponent = isDefined(rawPageComponent) && React.isValidElement(rawPageComponent)