Fixed command menu and main container layout (#16665)

This PR fixes https://github.com/twentyhq/twenty/issues/16645

It solves two problems : 
- Command menu for mobile was outside of its proper place, it should
have been portaled instead of lifted that high in the hierarchy, which
is done here, thus avoiding context issues.
- CSS was odd due to a code path removing main container styling for
mobile display, everything has been cleaned with explicit and durable
naming. I used "main container layout" instead of "page layout" to
disambiguate from page layout feature.

## Before 

<img width="567" height="907" alt="image"
src="https://github.com/user-attachments/assets/73a335f6-d5b6-4e8a-a33f-73aa624c7ca5"
/>


## After 

<img width="566" height="907" alt="image"
src="https://github.com/user-attachments/assets/0433b7d8-c8de-4b2b-b3fd-0d8d45b92d75"
/>
This commit is contained in:
Lucas Bordeau
2025-12-18 01:48:31 -10:00
committed by GitHub
parent 52604e1c52
commit ce7f0c03f7
9 changed files with 106 additions and 73 deletions
@@ -0,0 +1,34 @@
import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import styled from '@emotion/styled';
import { AnimatePresence } from 'framer-motion';
import { createPortal } from 'react-dom';
import { useRecoilValue } from 'recoil';
const StyledCommandMenuMobileFullScreenContainer = styled.div`
height: 100%;
width: 100%;
`;
export const CommandMenuForMobile = () => {
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
return (
<AnimatePresence>
{isCommandMenuOpened && (
<>
{createPortal(
<StyledCommandMenuMobileFullScreenContainer>
<CommandMenuOpenContainer>
<CommandMenuRouter />
</CommandMenuOpenContainer>
</StyledCommandMenuMobileFullScreenContainer>,
document.body,
)}
</>
)}
</AnimatePresence>
);
};
@@ -53,7 +53,7 @@ const StyledModalContainer = styled.div`
const GAP_WIDTH = 8;
export const CommandMenuSidePanel = () => {
export const CommandMenuSidePanelForDesktop = () => {
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
const isCommandMenuClosing = useRecoilValue(isCommandMenuClosingState);
const [commandMenuWidth, setCommandMenuWidth] = useRecoilState(
@@ -1,45 +0,0 @@
import { CommandMenuSidePanel } from '@/command-menu/components/CommandMenuSidePanel';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { useIsMobile } from 'twenty-ui/utilities';
type CommandMenuPageLayoutProps = {
children: ReactNode;
};
const StyledLayout = styled.div`
display: flex;
flex: 1;
min-height: 0;
padding-bottom: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(3)};
`;
const StyledPageBody = styled(PageBody)`
flex: 1 1 0;
min-width: 0;
width: 0;
padding-bottom: 0;
padding-right: 0;
`;
export const CommandMenuPageLayout = ({
children,
}: CommandMenuPageLayoutProps) => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
if (isMobile) {
return <>{children}</>;
}
return (
<StyledLayout>
<StyledPageBody>{children}</StyledPageBody>
<CommandMenuSidePanel />
</StyledLayout>
);
};
@@ -0,0 +1,65 @@
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
import { CommandMenuSidePanelForDesktop } from '@/command-menu/components/CommandMenuSidePanelForDesktop';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { useIsMobile } from 'twenty-ui/utilities';
type MainContainerLayoutWithCommandMenuProps = {
children: ReactNode;
};
const StyledMainContainerLayoutForDesktop = styled.div`
display: flex;
flex: 1;
min-height: 0;
padding-bottom: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(3)};
`;
const StyledPageBodyForDesktop = styled(PageBody)`
flex: 1 1 0;
min-width: 0;
width: 0;
padding-bottom: 0;
padding-right: 0;
`;
const StyledMainContainerLayoutForMobile = styled.div`
display: flex;
flex: 1;
min-height: 0;
padding: 0;
`;
const StyledPageBodyForMobile = styled(PageBody)`
padding-bottom: 0;
padding-left: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(1.5)};
`;
export const MainContainerLayoutWithCommandMenu = ({
children,
}: MainContainerLayoutWithCommandMenuProps) => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
if (isMobile) {
return (
<StyledMainContainerLayoutForMobile>
<StyledPageBodyForMobile>{children}</StyledPageBodyForMobile>
<CommandMenuForMobile />
</StyledMainContainerLayoutForMobile>
);
}
return (
<StyledMainContainerLayoutForDesktop>
<StyledPageBodyForDesktop>{children}</StyledPageBodyForDesktop>
<CommandMenuSidePanelForDesktop />
</StyledMainContainerLayoutForDesktop>
);
};
@@ -3,7 +3,7 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { CommandMenuPageLayout } from '@/object-record/components/CommandMenuPageLayout';
import { MainContainerLayoutWithCommandMenu } from '@/object-record/components/MainContainerLayoutWithCommandMenu';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
@@ -98,14 +98,14 @@ export const RecordIndexContainerGater = () => {
>
<PageTitle title={objectMetadataItem.labelPlural} />
<RecordIndexPageHeader />
<CommandMenuPageLayout>
<MainContainerLayoutWithCommandMenu>
<StyledIndexContainer
className={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
>
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<RecordIndexContainer />
</StyledIndexContainer>
</CommandMenuPageLayout>
</MainContainerLayoutWithCommandMenu>
</ActionMenuComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
<RecordIndexLoadBaseOnContextStoreEffect />
@@ -1,7 +1,4 @@
import { AuthModal } from '@/auth/components/AuthModal';
import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
import { AppPageErrorFallback } from '@/error-handler/components/AppPageErrorFallback';
@@ -21,7 +18,6 @@ import { Global, css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
import { Outlet } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useScreenSize } from 'twenty-ui/utilities';
const StyledLayout = styled.div`
@@ -67,7 +63,6 @@ export const DefaultLayout = () => {
const windowsWidth = useScreenSize().width;
const showAuthModal = useShowAuthModal();
const useShowFullScreen = useShowFullscreen();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
return (
<>
@@ -124,15 +119,6 @@ export const DefaultLayout = () => {
)}
</StyledPageContainer>
{isMobile && !showAuthModal && <MobileNavigationBar />}
{isMobile && (
<AnimatePresence>
{isCommandMenuOpened && (
<CommandMenuOpenContainer>
<CommandMenuRouter />
</CommandMenuOpenContainer>
)}
</AnimatePresence>
)}
</AppErrorBoundary>
</StyledLayout>
</>
@@ -1,7 +1,6 @@
import styled from '@emotion/styled';
import { type ReactNode } from 'react';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { PagePanel } from './PagePanel';
type PageBodyProps = {
@@ -21,11 +20,6 @@ const StyledMainContainer = styled.div`
padding-right: ${({ theme }) => theme.spacing(3)};
padding-left: 0;
width: 100%;
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${({ theme }) => theme.spacing(3)};
padding-bottom: 0;
}
`;
type LeftContainerProps = {
@@ -133,7 +133,6 @@ export const PageHeader = ({
)}
</StyledTopBarIconStyledTitleContainer>
</StyledLeftContainer>
<StyledPageActionContainer
data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}
>
@@ -5,7 +5,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { CommandMenuPageLayout } from '@/object-record/components/CommandMenuPageLayout';
import { MainContainerLayoutWithCommandMenu } from '@/object-record/components/MainContainerLayoutWithCommandMenu';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { PageLayoutDispatcher } from '@/object-record/record-show/components/PageLayoutDispatcher';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
@@ -51,7 +51,7 @@ export const RecordShowPage = () => {
<RecordShowActionMenu />
<PageHeaderToggleCommandMenuButton />
</RecordShowPageHeader>
<CommandMenuPageLayout>
<MainContainerLayoutWithCommandMenu>
<TimelineActivityContext.Provider
value={{
recordId: objectRecordId,
@@ -64,7 +64,7 @@ export const RecordShowPage = () => {
}}
/>
</TimelineActivityContext.Provider>
</CommandMenuPageLayout>
</MainContainerLayoutWithCommandMenu>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>