feat: fix Command Menu Side Panel Layout (#15883)

[Figma
Design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=81380-344641&t=FpjWNOK2gZuDQQfr-0)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds a side panel layout for the Command Menu, routes modals into a
local container, updates the top bar and context chips, and standardizes
small button sizes.
> 
> - **Command Menu**:
> - **Side Panel Layout**: Introduces `CommandMenuSidePanelLayout` with
animated width, hosts `CommandMenuRouter`, and provides a modal
container via `ModalContainerContext`.
> - **Top Bar**: Redesign (`CommandMenuTopBar`) with back icon, optional
AI sparkles action, compact height
(`COMMAND_MENU_SEARCH_BAR_HEIGHT=40`), and updated placeholder.
> - **Context Chips**: Adds `CommandMenuLastContextChip` and
`CommandMenuRecordInfo`; extends `CommandMenuContextChip` with `page`
prop; updates `CommandMenuContextChipGroups` to render last chip as
record info when applicable.
> - **Container Simplification**: `CommandMenuContainer` simplified to
just provide contexts and `AgentChatProvider`.
> - **Modal System**:
> - Adds `ModalContainerContext` and updates `Modal` to portal into
provided container; `Modal.Backdrop` supports `isInContainer`.
> - Updates usages (e.g., `UserOrMetadataLoader`, `ActionModal`) to
align with new modal behavior.
> - **Page Integration**:
> - Replaces `PageBody` with `CommandMenuSidePanelLayout` in
`RecordShowPage` and `RecordIndexContainerGater`.
> - Removes global `CommandMenuRouter` from `DefaultLayout` (keeps
keyboard shortcuts).
> - **UI/Styling**:
> - Standardizes several buttons to `size="small"` (e.g., command
actions, open record, options, reply, workflow footer).
> - Adjusts `ShowPageSubContainer` styling when rendered inside command
menu.
>   - Storybook tests updated for new placeholder text.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
81fcaa145618a2fa1c3e11e2dc88fe832c51374c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-11-21 20:42:41 +05:30
committed by GitHub
parent 28b8a4f7ec
commit 04b0a65e73
102 changed files with 1913 additions and 1548 deletions
@@ -172,6 +172,7 @@ export const CommandMenuMessageThreadPage = () => {
{canReply && !messageChannelLoading && (
<StyledButtonContainer isMobile={isMobile}>
<Button
size="small"
onClick={handleReplyClick}
title={t`Reply`}
Icon={IconArrowBackUp}
@@ -1,19 +1,13 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { ChartFiltersSettings } from '@/command-menu/pages/page-layout/components/ChartFiltersSettings';
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { isChartWidget } from '@/command-menu/pages/page-layout/utils/isChartWidget';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
export const CommandMenuPageLayoutGraphFilter = () => {
const theme = useTheme();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const draftPageLayout = useRecoilComponentValue(
@@ -52,18 +46,8 @@ export const CommandMenuPageLayoutGraphFilter = () => {
return null;
}
const currentGraphType = widgetInEditMode.configuration.graphType;
const graphTypeLabel = t(GRAPH_TYPE_INFORMATION[currentGraphType].label);
return (
<>
<SidePanelHeader
Icon={GRAPH_TYPE_INFORMATION[currentGraphType].icon}
iconColor={theme.font.color.tertiary}
initialTitle={widgetInEditMode.title}
headerType={t`${graphTypeLabel} Chart`}
disabled
/>
<ChartFiltersSettings
widget={widgetInEditMode}
objectMetadataItem={objectMetadataItem}
@@ -1,15 +1,9 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { ChartSettings } from '@/command-menu/pages/page-layout/components/ChartSettings';
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
export const CommandMenuPageLayoutGraphTypeSelect = () => {
@@ -25,8 +19,6 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
pageLayoutId,
);
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
if (!isDefined(pageLayoutEditingWidgetId)) {
throw new Error('Widget ID must be present while editing the widget');
}
@@ -41,8 +33,6 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
);
}
const theme = useTheme();
if (
!isDefined(widgetInEditMode.configuration) ||
!('graphType' in widgetInEditMode.configuration)
@@ -50,26 +40,10 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
return null;
}
const currentGraphType = widgetInEditMode.configuration.graphType;
const graphTypeLabel = t(GRAPH_TYPE_INFORMATION[currentGraphType].label);
return (
<GraphWidgetComponentInstanceContext.Provider
value={{ instanceId: widgetInEditMode.id }}
>
<SidePanelHeader
Icon={GRAPH_TYPE_INFORMATION[currentGraphType].icon}
iconColor={theme.font.color.tertiary}
initialTitle={widgetInEditMode.title}
headerType={t`${graphTypeLabel} Chart`}
onTitleChange={(newTitle) => {
if (isNonEmptyString(newTitle)) {
updatePageLayoutWidget(widgetInEditMode.id, {
title: newTitle,
});
}
}}
/>
<ChartSettings widget={widgetInEditMode} />
</GraphWidgetComponentInstanceContext.Provider>
);
@@ -1,15 +1,12 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isNonEmptyString, isString } from '@sniptt/guards';
import { useState } from 'react';
import { isDefined, isValidUrl } from 'twenty-shared/utils';
import { IconFrame } from 'twenty-ui/display';
const StyledContainer = styled.div`
display: flex;
@@ -21,8 +18,6 @@ const StyledContainer = styled.div`
export const CommandMenuPageLayoutIframeSettings = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const theme = useTheme();
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
@@ -74,21 +69,6 @@ export const CommandMenuPageLayoutIframeSettings = () => {
return (
<>
<SidePanelHeader
Icon={IconFrame}
iconColor={theme.font.color.tertiary}
initialTitle={widgetInEditMode.title}
headerType={t`iFrame Widget`}
onTitleChange={(newTitle) => {
if (!isNonEmptyString(newTitle)) {
return;
}
updatePageLayoutWidget(widgetInEditMode.id, {
title: newTitle,
});
}}
/>
<StyledContainer>
<FormTextFieldInput
label={t`URL to Embed`}
@@ -1,26 +1,21 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/command-menu/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useDeletePageLayoutTab } from '@/page-layout/hooks/useDeletePageLayoutTab';
import { useDuplicatePageLayoutTab } from '@/page-layout/hooks/useDuplicatePageLayoutTab';
import { useMovePageLayoutTab } from '@/page-layout/hooks/useMovePageLayoutTab';
import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import {
IconAppWindow,
IconChevronLeft,
IconChevronRight,
IconCopyPlus,
@@ -28,7 +23,6 @@ import {
} from 'twenty-ui/display';
export const CommandMenuPageLayoutTabSettings = () => {
const theme = useTheme();
const { closeCommandMenu } = useCommandMenu();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
@@ -44,7 +38,6 @@ export const CommandMenuPageLayoutTabSettings = () => {
const { moveLeft, moveRight } = useMovePageLayoutTab(pageLayoutId);
const { deleteTab } = useDeletePageLayoutTab(pageLayoutId);
const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId);
const { duplicateTab } = useDuplicatePageLayoutTab(pageLayoutId);
if (!isDefined(openTabId)) {
@@ -69,17 +62,6 @@ export const CommandMenuPageLayoutTabSettings = () => {
return (
<>
<SidePanelHeader
Icon={IconAppWindow}
iconColor={theme.font.color.tertiary}
initialTitle={tab.title}
headerType={t`Tab`}
onTitleChange={(newTitle) => {
if (isDefined(newTitle) && isNonEmptyString(newTitle)) {
updatePageLayoutTab(tab.id, { title: newTitle });
}
}}
/>
<CommandMenuList
commandGroups={[]}
selectableItemIds={Object.values(TAB_SETTINGS_SELECTABLE_ITEM_IDS)}
@@ -11,6 +11,7 @@ type NavigatePageLayoutCommandMenuProps = {
pageTitle?: string;
pageIcon?: IconComponent;
focusTitleInput?: boolean;
resetNavigationStack?: boolean;
};
export const useNavigatePageLayoutCommandMenu = () => {
@@ -22,6 +23,7 @@ export const useNavigatePageLayoutCommandMenu = () => {
pageTitle,
pageIcon,
focusTitleInput = false,
resetNavigationStack = false,
}: NavigatePageLayoutCommandMenuProps) => {
navigateCommandMenu({
page: commandMenuPage,
@@ -32,6 +34,7 @@ export const useNavigatePageLayoutCommandMenu = () => {
? pageIcon
: getPageLayoutIcon(commandMenuPage),
focusTitleInput,
resetNavigationStack,
});
};
}, [navigateCommandMenu]);
@@ -0,0 +1,10 @@
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const commandMenuWorkflowStepIdComponentState = createComponentState<
string | undefined
>({
key: 'command-menu/workflow-step-id',
defaultValue: undefined,
componentInstanceContext: CommandMenuPageComponentInstanceContext,
});
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowCreateStepContent = () => {
workflowVisualizerWorkflowId,
createdStep.name,
getIcon(getActionIcon(createdStep.type as WorkflowActionType)),
createdStep.id,
);
};
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowEditStepTypeContent = () => {
workflowVisualizerWorkflowId,
updatedStep.name,
getIcon(getActionIcon(updatedStep.type as WorkflowActionType)),
updatedStep.id,
);
};
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = () => {
workflowId,
defaultLabel,
getIcon(icon),
TRIGGER_STEP_ID,
);
};
};