Compare commits

...

1 Commits

Author SHA1 Message Date
Félix Malfait b59b67ae03 fix(twenty-front): keep the AI side panel mounted across navigation
The AI chat side panel lived inside each per-page layout
(SettingsPageLayout and the record body container), so React unmounted
and remounted it on every navigation — the chat reloaded and lost its
state whenever you opened another record or settings page.

Hoist the side panel into a new persistent layout route,
MainAppLayoutWithSidePanel, that wraps every main-app route (records,
page layouts, settings). The panel is now a stable sibling of the routed
Outlet, so it stays mounted — and the AI chat keeps its state — while
only the page content reloads. PageChangeEffect already exempts the AI
chat from its close-on-navigation logic, so the panel now genuinely
persists instead of just re-opening.

- MainAppLayoutWithSidePanel owns the side panel + command-menu hotkeys;
  SettingsPageLayout and the record body container no longer render their
  own panel.
- MainContainerLayoutWithSidePanel renamed to MainContainerLayout since
  it no longer owns a panel.
- SidePanelForDesktop carries its own margin so it floats correctly as a
  top-level sibling; the margin collapses with the panel when closed, so
  pages render unchanged while it is closed.
- On a full reload the settings route shows the rounded-card skeleton
  (matching in-app navigation) instead of the legacy page skeleton.
2026-06-06 14:40:33 +02:00
9 changed files with 118 additions and 75 deletions
@@ -1,5 +1,9 @@
import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { styled } from '@linaria/react';
import { useLocation } from 'react-router-dom';
import { AppPath } from 'twenty-shared/types';
import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
const StyledRightPanelContainer = styled.div`
display: flex;
@@ -7,8 +11,17 @@ const StyledRightPanelContainer = styled.div`
width: 100%;
`;
export const RightPanelSkeletonLoader = () => (
<StyledRightPanelContainer>
<PageContentSkeletonLoader />
</StyledRightPanelContainer>
);
export const RightPanelSkeletonLoader = () => {
const location = useLocation();
const isSettingsPage = isMatchingLocation(location, AppPath.SettingsCatchAll);
return (
<StyledRightPanelContainer>
{isSettingsPage ? (
<SettingsSkeletonLoader />
) : (
<PageContentSkeletonLoader />
)}
</StyledRightPanelContainer>
);
};
@@ -7,6 +7,7 @@ import { VerifyEmailEffect } from '@/auth/components/VerifyEmailEffect';
import indexAppPath from '@/navigation/utils/indexAppPath';
import { BlankLayout } from '@/ui/layout/page/components/BlankLayout';
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
import { MainAppLayoutWithSidePanel } from '@/ui/layout/page/components/MainAppLayoutWithSidePanel';
import { AppPath } from 'twenty-shared/types';
import { lazy } from 'react';
@@ -209,48 +210,50 @@ export const useCreateAppRouter = (
</LazyRoute>
}
/>
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
<Route
path={AppPath.RecordIndexPage}
element={
<LazyRoute>
<RecordIndexPage />
</LazyRoute>
}
/>
<Route
path={AppPath.RecordShowPage}
element={
<LazyRoute>
<RecordShowPage />
</LazyRoute>
}
/>
<Route
path={AppPath.PageLayoutPage}
element={
<LazyRoute>
<StandalonePageLayoutPage />
</LazyRoute>
}
/>
<Route
path={AppPath.SettingsCatchAll}
element={
<SettingsRoutes
isFunctionSettingsEnabled={isFunctionSettingsEnabled}
isAdminPageEnabled={isAdminPageEnabled}
/>
}
/>
<Route
path={AppPath.NotFoundWildcard}
element={
<LazyRoute>
<NotFound />
</LazyRoute>
}
/>
<Route element={<MainAppLayoutWithSidePanel />}>
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
<Route
path={AppPath.RecordIndexPage}
element={
<LazyRoute>
<RecordIndexPage />
</LazyRoute>
}
/>
<Route
path={AppPath.RecordShowPage}
element={
<LazyRoute>
<RecordShowPage />
</LazyRoute>
}
/>
<Route
path={AppPath.PageLayoutPage}
element={
<LazyRoute>
<StandalonePageLayoutPage />
</LazyRoute>
}
/>
<Route
path={AppPath.SettingsCatchAll}
element={
<SettingsRoutes
isFunctionSettingsEnabled={isFunctionSettingsEnabled}
isAdminPageEnabled={isAdminPageEnabled}
/>
}
/>
<Route
path={AppPath.NotFoundWildcard}
element={
<LazyRoute>
<NotFound />
</LazyRoute>
}
/>
</Route>
</Route>
<Route element={<BlankLayout />}>
<Route
@@ -1,13 +1,10 @@
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { useIsMobile } from 'twenty-ui/utilities';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type MainContainerLayoutWithSidePanelProps = {
type MainContainerLayoutProps = {
children: ReactNode;
};
@@ -54,20 +51,15 @@ const StyledPageBodyForMobileContainer = styled.div`
}
`;
export const MainContainerLayoutWithSidePanel = ({
children,
}: MainContainerLayoutWithSidePanelProps) => {
export const MainContainerLayout = ({ children }: MainContainerLayoutProps) => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
if (isMobile) {
return (
<StyledMainContainerLayoutForMobile>
<StyledPageBodyForMobileContainer>
<PageBody>{children}</PageBody>
</StyledPageBodyForMobileContainer>
<CommandMenuForMobile />
</StyledMainContainerLayoutForMobile>
);
}
@@ -77,7 +69,6 @@ export const MainContainerLayoutWithSidePanel = ({
<StyledPageBodyForDesktopContainer>
<PageBody>{children}</PageBody>
</StyledPageBodyForDesktopContainer>
<SidePanelForDesktop />
</StyledMainContainerLayoutForDesktop>
);
};
@@ -3,7 +3,7 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
import { getCommandMenuIdFromRecordIndexId } from '@/command-menu-item/utils/getCommandMenuIdFromRecordIndexId';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { MainContainerLayout } from '@/object-record/components/MainContainerLayout';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
@@ -92,7 +92,7 @@ export const RecordIndexContainerGater = () => {
>
<PageTitle title={objectMetadataItem.labelPlural} />
<RecordIndexPageHeader />
<MainContainerLayoutWithSidePanel>
<MainContainerLayout>
<StyledIndexContainer
className={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
>
@@ -105,7 +105,7 @@ export const RecordIndexContainerGater = () => {
<RecordIndexEmptyStateNotShared />
)}
</StyledIndexContainer>
</MainContainerLayoutWithSidePanel>
</MainContainerLayout>
</CommandMenuComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
<RecordIndexLoadBaseOnContextStoreEffect />
@@ -1,9 +1,6 @@
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { SettingsPageHeader } from '@/settings/components/layout/SettingsPageHeader';
import { SettingsSecondaryBar } from '@/settings/components/layout/SettingsSecondaryBar';
import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop';
import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { styled } from '@linaria/react';
@@ -68,8 +65,6 @@ export const SettingsPageLayout = ({
}: SettingsPageLayoutProps) => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
return (
<StyledRoot isMobile={isMobile}>
<StyledMainCardWrapper>
@@ -89,7 +84,6 @@ export const SettingsPageLayout = ({
</StyledBodyContent>
</StyledCard>
</StyledMainCardWrapper>
{isMobile ? <CommandMenuForMobile /> : <SidePanelForDesktop />}
</StyledRoot>
);
};
@@ -25,13 +25,18 @@ const StyledSidePanelWrapper = styled.div<{
isOpen: boolean;
isResizing: boolean;
}>`
box-sizing: border-box;
flex-shrink: 0;
min-width: 0;
overflow: hidden;
padding: ${({ isOpen }) =>
isOpen
? `${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[2]} 0`
: '0'};
transition: ${({ isResizing }) =>
isResizing
? 'none'
: `width calc(${themeCssVariables.animation.duration.normal} * 1s)`};
: `width calc(${themeCssVariables.animation.duration.normal} * 1s), padding calc(${themeCssVariables.animation.duration.normal} * 1s)`};
width: ${({ isOpen }) => (isOpen ? `var(${SIDE_PANEL_WIDTH_VAR})` : '0px')};
`;
@@ -0,0 +1,37 @@
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { styled } from '@linaria/react';
import { Outlet } from 'react-router-dom';
const StyledRow = styled.div`
display: flex;
flex: 1;
flex-direction: row;
min-height: 0;
min-width: 0;
`;
const StyledContent = styled.div`
display: flex;
flex: 1 1 0;
min-height: 0;
min-width: 0;
overflow: hidden;
`;
export const MainAppLayoutWithSidePanel = () => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
return (
<StyledRow>
<StyledContent>
<Outlet />
</StyledContent>
{isMobile ? <CommandMenuForMobile /> : <SidePanelForDesktop />}
</StyledRow>
);
};
@@ -7,7 +7,7 @@ import { TimelineActivityContext } from '@/activities/timeline-activities/contex
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { MainContainerLayout } from '@/object-record/components/MainContainerLayout';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { PageLayoutRecordPageRenderer } from '@/object-record/record-show/components/PageLayoutRecordPageRenderer';
import { RecordShowPageSSESubscribeEffect } from '@/object-record/record-show/components/RecordShowPageSSESubscribeEffect';
@@ -58,7 +58,7 @@ export const RecordShowPage = () => {
<RecordShowCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</RecordShowPageHeader>
<MainContainerLayoutWithSidePanel>
<MainContainerLayout>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
@@ -76,7 +76,7 @@ export const RecordShowPage = () => {
recordId={objectRecordId}
/>
</TimelineActivityContext.Provider>
</MainContainerLayoutWithSidePanel>
</MainContainerLayout>
</PageContainer>
</CommandMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { MainContainerLayout } from '@/object-record/components/MainContainerLayout';
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
@@ -34,9 +34,9 @@ export const StandalonePageLayoutPage = () => {
isInSidePanel: false,
}}
>
<MainContainerLayoutWithSidePanel>
<MainContainerLayout>
<PageLayoutRenderer pageLayoutId={pageLayoutId} />
</MainContainerLayoutWithSidePanel>
</MainContainerLayout>
</LayoutRenderingProvider>
</CommandMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>