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:
+1
@@ -172,6 +172,7 @@ export const CommandMenuMessageThreadPage = () => {
|
||||
{canReply && !messageChannelLoading && (
|
||||
<StyledButtonContainer isMobile={isMobile}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleReplyClick}
|
||||
title={t`Reply`}
|
||||
Icon={IconArrowBackUp}
|
||||
|
||||
-16
@@ -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}
|
||||
|
||||
-26
@@ -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>
|
||||
);
|
||||
|
||||
-20
@@ -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`}
|
||||
|
||||
-18
@@ -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)}
|
||||
|
||||
+3
@@ -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]);
|
||||
|
||||
+10
@@ -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,
|
||||
});
|
||||
+1
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowCreateStepContent = () => {
|
||||
workflowVisualizerWorkflowId,
|
||||
createdStep.name,
|
||||
getIcon(getActionIcon(createdStep.type as WorkflowActionType)),
|
||||
createdStep.id,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowEditStepTypeContent = () => {
|
||||
workflowVisualizerWorkflowId,
|
||||
updatedStep.name,
|
||||
getIcon(getActionIcon(updatedStep.type as WorkflowActionType)),
|
||||
updatedStep.id,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = () => {
|
||||
workflowId,
|
||||
defaultLabel,
|
||||
getIcon(icon),
|
||||
TRIGGER_STEP_ID,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user