From 04b0a65e73d953ef390f112675c39d53ec478fb0 Mon Sep 17 00:00:00 2001
From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com>
Date: Fri, 21 Nov 2025 20:42:41 +0530
Subject: [PATCH] feat: fix Command Menu Side Panel Layout (#15883)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[Figma
Design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=81380-344641&t=FpjWNOK2gZuDQQfr-0)
---
> [!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.
>
> 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).
---------
Co-authored-by: Félix Malfait
Co-authored-by: Lucas Bordeau
Co-authored-by: Devessier
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>
---
.../actions/components/ActionModal.tsx | 25 +-
.../components/ActionOpenSidePanelPage.tsx | 6 +-
.../DeleteMultipleRecordsAction.tsx | 10 +-
.../components/DeleteSingleRecordAction.tsx | 12 +-
.../components/CmdEnterActionButton.tsx | 2 +-
.../CommandMenuActionMenuDropdown.tsx | 6 +-
.../RecordShowRightDrawerOpenRecordButton.tsx | 2 +-
.../ai/components/AIChatThreadGroup.tsx | 5 +-
.../ai/hooks/useCreateNewAIChatThread.ts | 2 +-
.../components/CommandMenuBackButton.tsx | 82 +++++++
.../components/CommandMenuContainer.tsx | 31 +--
.../components/CommandMenuContextChip.tsx | 5 +
.../CommandMenuContextChipGroups.tsx | 89 -------
...nuContextChipGroupsWithRecordSelection.tsx | 64 -----
.../components/CommandMenuPageInfo.tsx | 68 ++++++
.../components/CommandMenuPageLayoutInfo.tsx | 160 ++++++++++++
.../components/CommandMenuRecordInfo.tsx | 162 ++++++++++++
.../components/CommandMenuTopBar.tsx | 123 +++++-----
.../CommandMenuTopBarRightCornerIcon.tsx | 63 +++++
.../CommandMenuWorkflowStepInfo.tsx | 230 ++++++++++++++++++
.../__stories__/CommandMenu.stories.tsx | 9 +-
.../CommandMenuContextChipGroups.stories.tsx | 97 --------
.../hooks/usePageLayoutHeaderInfo.ts | 151 ++++++++++++
.../CommandMenuNavigationHistoryDropdownId.ts | 2 +
.../constants/CommandMenuSearchBarHeight.ts | 2 +-
.../CommandMenuSearchBarHeightMobile.ts | 1 +
.../constants/CommandMenuSidePanelWidth.ts | 1 +
.../hooks/__tests__/useCommandMenu.test.tsx | 43 ++++
...ilterActionsWithCommandMenuSearch.test.tsx | 102 ++++++++
.../hooks/useCommandMenuHotKeys.ts | 2 +-
.../hooks/useOpenAskAIPageInCommandMenu.ts | 17 +-
.../useOpenRecordsSearchPageInCommandMenu.ts | 4 +
.../hooks/useWorkflowCommandMenu.ts | 57 ++++-
.../CommandMenuMessageThreadPage.tsx | 1 +
.../CommandMenuPageLayoutGraphFilter.tsx | 16 --
.../CommandMenuPageLayoutGraphTypeSelect.tsx | 26 --
.../CommandMenuPageLayoutIframeSettings.tsx | 20 --
.../CommandMenuPageLayoutTabSettings.tsx | 18 --
.../hooks/useNavigatePageLayoutCommandMenu.ts | 3 +
...commandMenuWorkflowStepIdComponentState.ts | 10 +
.../CommandMenuWorkflowCreateStepContent.tsx | 1 +
...CommandMenuWorkflowEditStepTypeContent.tsx | 1 +
...ndMenuWorkflowSelectTriggerTypeContent.tsx | 1 +
.../MainNavigationDrawerFixedItems.tsx | 2 +-
.../components/CommandMenuLayout.tsx | 151 ++++++++++++
.../components/CommandMenuPageLayout.tsx | 144 +++++++++++
.../components/RecordIndexContainerGater.tsx | 6 +-
.../components/RecordTableWidthEffect.tsx | 45 ++--
.../states/tableWidthResizeIsActivedState.ts | 6 +
.../components/RecordTitleCell.tsx | 2 +-
.../components/RecordTitleCellFieldInput.tsx | 2 +-
.../RecordTitleCellTextFieldInput.tsx | 2 +-
.../components/RecordTitleDoubleTextInput.tsx | 2 +-
.../RecordTitleFullNameFieldInput.tsx | 2 +-
.../hooks/useEditPageLayoutWidget.ts | 2 +
.../input/editor/components/BlockEditor.tsx | 2 +-
.../ui/layout/modal/components/Modal.tsx | 31 ++-
.../modal/contexts/ModalContainerContext.tsx | 13 +
.../layout/page/components/DefaultLayout.tsx | 20 +-
.../ui/layout/page/components/PageBody.tsx | 5 +-
.../components/SelectableListItem.tsx | 20 +-
.../components/ShowPageSubContainer.tsx | 24 +-
.../workflow/utils/getAgentIdFromStep.ts | 39 +++
.../components/WorkflowDiagramCanvasBase.tsx | 98 +++-----
.../WorkflowDiagramEmptyTriggerReadonly.tsx | 1 +
.../WorkflowDiagramStepNodeEditable.tsx | 1 +
.../WorkflowDiagramStepNodeReadonly.tsx | 1 +
.../components/WorkflowRunStepInputDetail.tsx | 27 +-
.../WorkflowRunStepOutputDetail.tsx | 44 +---
.../components/WorkflowStepFooter.tsx | 7 +-
.../__tests__/useUpdateAgentLabel.test.tsx | 94 +++++++
.../hooks/useUpdateAgentLabel.ts | 34 +++
.../components/WorkflowEditActionAiAgent.tsx | 39 ---
.../WorkflowEditActionServerlessFunction.tsx | 27 +-
...rkflowReadonlyActionServerlessFunction.tsx | 21 --
.../WorkflowEditActionCreateRecord.tsx | 30 ---
.../WorkflowEditActionDeleteRecord.tsx | 27 --
.../WorkflowEditActionSendEmail.tsx | 32 +--
.../WorkflowEditActionUpdateRecord.tsx | 27 --
.../WorkflowEditActionUpsertRecord.tsx | 31 ---
...WorkflowEditActionCreateRecord.stories.tsx | 9 -
...WorkflowEditActionDeleteRecord.stories.tsx | 27 --
.../WorkflowEditActionFindRecords.stories.tsx | 9 -
.../WorkflowEditActionSendEmail.stories.tsx | 3 -
...WorkflowEditActionUpdateRecord.stories.tsx | 27 --
.../components/WorkflowEditActionDelay.tsx | 29 ---
.../components/WorkflowEditActionFilter.tsx | 30 ---
.../WorkflowEditActionFindRecords.tsx | 27 --
.../WorkflowEditActionFormBuilder.tsx | 29 ---
.../WorkflowEditActionFormFiller.tsx | 19 --
.../WorkflowEditActionFormBuilder.stories.tsx | 9 -
.../useWorkflowActionHeader.test.tsx | 223 -----------------
.../hooks/useWorkflowActionHeader.ts | 38 ---
.../WorkflowEditActionHttpRequest.tsx | 25 +-
.../WorkflowEditActionHttpRequest.stories.tsx | 4 -
.../components/WorkflowEditActionIterator.tsx | 29 ---
.../WorkflowEditTriggerCronForm.tsx | 34 ---
.../WorkflowEditTriggerDatabaseEventForm.tsx | 27 --
.../components/WorkflowEditTriggerManual.tsx | 29 ---
.../WorkflowEditTriggerWebhookForm.tsx | 34 +--
.../hooks/useVariableDropdown.ts | 1 +
.../pages/object-record/RecordShowPage.tsx | 6 +-
102 files changed, 1913 insertions(+), 1548 deletions(-)
create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuBackButton.tsx
delete mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroups.tsx
delete mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroupsWithRecordSelection.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordInfo.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBarRightCornerIcon.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuWorkflowStepInfo.tsx
delete mode 100644 packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextChipGroups.stories.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts
create mode 100644 packages/twenty-front/src/modules/command-menu/constants/CommandMenuNavigationHistoryDropdownId.ts
create mode 100644 packages/twenty-front/src/modules/command-menu/constants/CommandMenuSearchBarHeightMobile.ts
create mode 100644 packages/twenty-front/src/modules/command-menu/constants/CommandMenuSidePanelWidth.ts
create mode 100644 packages/twenty-front/src/modules/command-menu/hooks/__tests__/useFilterActionsWithCommandMenuSearch.test.tsx
create mode 100644 packages/twenty-front/src/modules/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState.ts
create mode 100644 packages/twenty-front/src/modules/object-record/components/CommandMenuLayout.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/components/CommandMenuPageLayout.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/tableWidthResizeIsActivedState.ts
create mode 100644 packages/twenty-front/src/modules/ui/layout/modal/contexts/ModalContainerContext.tsx
create mode 100644 packages/twenty-front/src/modules/workflow/utils/getAgentIdFromStep.ts
create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateAgentLabel.test.tsx
create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateAgentLabel.ts
delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/__tests__/useWorkflowActionHeader.test.tsx
delete mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader.ts
diff --git a/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx b/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx
index 2ba6e1e718..d42265c52e 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx
@@ -1,5 +1,4 @@
import { type ReactNode, useContext } from 'react';
-import { createPortal } from 'react-dom';
import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDisplay';
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
@@ -63,19 +62,17 @@ export const ActionModal = ({
return (
<>
- {isModalOpened &&
- createPortal(
- ,
- document.body,
- )}
+ {isModalOpened && (
+
+ )}
>
);
};
diff --git a/packages/twenty-front/src/modules/action-menu/actions/components/ActionOpenSidePanelPage.tsx b/packages/twenty-front/src/modules/action-menu/actions/components/ActionOpenSidePanelPage.tsx
index 6482d421f4..4a54d8b732 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/components/ActionOpenSidePanelPage.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/components/ActionOpenSidePanelPage.tsx
@@ -2,11 +2,12 @@ import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDi
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
+import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type MessageDescriptor } from '@lingui/core';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
-import { useSetRecoilState } from 'recoil';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
import { type IconComponent } from 'twenty-ui/display';
export const ActionOpenSidePanelPage = ({
@@ -28,6 +29,8 @@ export const ActionOpenSidePanelPage = ({
const setCommandMenuSearchState = useSetRecoilState(commandMenuSearchState);
+ const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
+
if (!actionConfig) {
return null;
}
@@ -39,6 +42,7 @@ export const ActionOpenSidePanelPage = ({
page,
pageTitle: t(pageTitle),
pageIcon,
+ resetNavigationStack: isCommandMenuOpened,
});
if (shouldResetSearchState) {
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx
index 6400d11453..0f7deb15ac 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/DeleteMultipleRecordsAction.tsx
@@ -1,4 +1,4 @@
-import { ActionModal } from '@/action-menu/actions/components/ActionModal';
+import { Action } from '@/action-menu/actions/components/Action';
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
import { computeProgressText } from '@/action-menu/utils/computeProgressText';
import { getActionLabel } from '@/action-menu/utils/getActionLabel';
@@ -14,7 +14,6 @@ import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
-import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -92,12 +91,7 @@ export const DeleteMultipleRecordsAction = () => {
return (
-
+
);
};
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DeleteSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DeleteSingleRecordAction.tsx
index a29d602a73..99597b27bc 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DeleteSingleRecordAction.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/DeleteSingleRecordAction.tsx
@@ -1,11 +1,10 @@
-import { ActionModal } from '@/action-menu/actions/components/ActionModal';
+import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
-import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
export const DeleteSingleRecordAction = () => {
@@ -37,12 +36,5 @@ export const DeleteSingleRecordAction = () => {
await deleteOneRecord(recordId);
};
- return (
-
- );
+ return ;
};
diff --git a/packages/twenty-front/src/modules/action-menu/components/CmdEnterActionButton.tsx b/packages/twenty-front/src/modules/action-menu/components/CmdEnterActionButton.tsx
index 2be7c8699d..b4a8ddaddd 100644
--- a/packages/twenty-front/src/modules/action-menu/components/CmdEnterActionButton.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/CmdEnterActionButton.tsx
@@ -25,7 +25,7 @@ export const CmdEnterActionButton = ({
title={title}
variant={disabled ? 'secondary' : 'primary'}
accent="blue"
- size="medium"
+ size="small"
onClick={onClick}
disabled={disabled}
hotkeys={[getOsControlSymbol(), '⏎']}
diff --git a/packages/twenty-front/src/modules/action-menu/components/CommandMenuActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/CommandMenuActionMenuDropdown.tsx
index d9ec84428a..90c6c2c5b9 100644
--- a/packages/twenty-front/src/modules/action-menu/components/CommandMenuActionMenuDropdown.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/CommandMenuActionMenuDropdown.tsx
@@ -66,7 +66,11 @@ export const CommandMenuActionMenuDropdown = () => {
dropdownId={dropdownId}
data-select-disable
clickableComponent={
-
+
}
dropdownPlacement="top-end"
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx
index 89ce7d29a2..45a6cf2a78 100644
--- a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerOpenRecordButton.tsx
@@ -135,7 +135,7 @@ export const RecordShowRightDrawerOpenRecordButton = ({
title={t`Open`}
variant="primary"
accent="blue"
- size="medium"
+ size="small"
Icon={IconBrowserMaximize}
hotkeys={[getOsControlSymbol(), '⏎']}
onClick={handleOpenRecord}
diff --git a/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx b/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx
index 0b1a7a0b60..efede6ea68 100644
--- a/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx
+++ b/packages/twenty-front/src/modules/ai/components/AIChatThreadGroup.tsx
@@ -90,7 +90,10 @@ export const AIChatThreadGroup = ({
{
setCurrentAIChatThread(thread.id);
- openAskAIPage(thread.title);
+ openAskAIPage({
+ pageTitle: thread.title,
+ resetNavigationStack: false,
+ });
}}
key={thread.id}
>
diff --git a/packages/twenty-front/src/modules/ai/hooks/useCreateNewAIChatThread.ts b/packages/twenty-front/src/modules/ai/hooks/useCreateNewAIChatThread.ts
index 4c158fc6f9..0d6c5965a7 100644
--- a/packages/twenty-front/src/modules/ai/hooks/useCreateNewAIChatThread.ts
+++ b/packages/twenty-front/src/modules/ai/hooks/useCreateNewAIChatThread.ts
@@ -10,7 +10,7 @@ export const useCreateNewAIChatThread = () => {
const [createChatThread] = useCreateChatThreadMutation({
onCompleted: (data) => {
setCurrentAIChatThread(data.createChatThread.id);
- openAskAIPage();
+ openAskAIPage({ resetNavigationStack: false });
},
});
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuBackButton.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuBackButton.tsx
new file mode 100644
index 0000000000..70bd56c4dd
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuBackButton.tsx
@@ -0,0 +1,82 @@
+import { COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuNavigationHistoryDropdownId';
+import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
+import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
+import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
+import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
+import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
+import styled from '@emotion/styled';
+import { IconChevronLeft } from 'twenty-ui/display';
+import { IconButton } from 'twenty-ui/input';
+import { MenuItem } from 'twenty-ui/navigation';
+
+const StyledNavigationIcon = styled.div`
+ align-items: center;
+ color: ${({ theme }) => theme.font.color.secondary};
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ margin-right: ${({ theme }) => theme.spacing(1)};
+`;
+
+export const CommandMenuBackButton = () => {
+ const { goBackFromCommandMenu } = useCommandMenuHistory();
+
+ const { contextChips } = useCommandMenuContextChips();
+
+ const { openDropdown } = useOpenDropdown();
+
+ const { closeDropdown } = useCloseDropdown();
+
+ const handleBackButtonContextMenu = (
+ event: React.MouseEvent,
+ ) => {
+ if (contextChips.length === 0) {
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ openDropdown({
+ dropdownComponentInstanceIdFromProps:
+ COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID,
+ });
+ };
+
+ return (
+
+
+
+ }
+ dropdownComponents={
+
+
+ {contextChips.slice(0, -1).map((chip, index) => (
+
+
+ }
+ dropdownId={COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID}
+ dropdownPlacement="bottom-start"
+ disableClickForClickableComponent={true}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx
index 6f269de2f9..671897d659 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx
@@ -1,10 +1,6 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
-import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
-import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
-import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
-import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
@@ -12,19 +8,15 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
-import { AnimatePresence } from 'framer-motion';
import { useRecoilValue } from 'recoil';
+type CommandMenuContainerProps = {
+ children: React.ReactNode;
+};
+
export const CommandMenuContainer = ({
children,
-}: {
- children: React.ReactNode;
-}) => {
- const { commandMenuCloseAnimationCompleteCleanup } =
- useCommandMenuCloseAnimationCompleteCleanup();
-
- const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
-
+}: CommandMenuContainerProps) => {
const objectMetadataItemId = useRecoilComponentValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
COMMAND_MENU_COMPONENT_INSTANCE_ID,
@@ -46,8 +38,6 @@ export const CommandMenuContainer = ({
currentViewId ?? '',
);
- useCommandMenuHotKeys();
-
return (
-
-
- {isCommandMenuOpened && (
- {children}
- )}
-
-
+ {children}
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx
index bbb422fecd..8b08c591de 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx
@@ -1,3 +1,4 @@
+import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { Fragment } from 'react/jsx-runtime';
@@ -54,6 +55,10 @@ export type CommandMenuContextChipProps = {
testId?: string;
maxWidth?: string;
forceEmptyText?: boolean;
+ page?: {
+ page: CommandMenuPages;
+ pageId: string;
+ };
};
export const CommandMenuContextChip = ({
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroups.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroups.tsx
deleted file mode 100644
index 87290f4e18..0000000000
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroups.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
-import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
-import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
-import { isDefined } from 'twenty-shared/utils';
-import { MenuItem } from 'twenty-ui/navigation';
-import {
- CommandMenuContextChip,
- type CommandMenuContextChipProps,
-} from './CommandMenuContextChip';
-
-export const CommandMenuContextChipGroups = ({
- contextChips,
-}: {
- contextChips: CommandMenuContextChipProps[];
-}) => {
- const { closeDropdown } = useCloseDropdown();
-
- if (contextChips.length === 0) {
- return null;
- }
-
- if (contextChips.length < 3) {
- return (
- <>
- {contextChips.map((chip, index) => (
-
- ))}
- >
- );
- }
-
- const firstChips = contextChips.slice(0, -1);
- const firstThreeChips = firstChips.slice(0, 3);
- const lastChip = contextChips.at(-1);
-
- return (
- <>
- {firstChips.length > 0 && (
- chip.Icons?.[0])}
- onClick={() => {}}
- text={`${firstChips.length}`}
- />
- }
- dropdownComponents={
-
-
- {firstChips.map((chip, index) => (
-
-
- }
- dropdownId={COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID}
- dropdownPlacement="bottom-start"
- >
- )}
-
- {isDefined(lastChip) && (
-
- )}
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroupsWithRecordSelection.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroupsWithRecordSelection.tsx
deleted file mode 100644
index 7de8410f97..0000000000
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChipGroupsWithRecordSelection.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { CommandMenuContextChipGroups } from '@/command-menu/components/CommandMenuContextChipGroups';
-import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
-import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
-import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
-import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
-import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
-import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
-import { isDefined } from 'twenty-shared/utils';
-
-export const CommandMenuContextChipGroupsWithRecordSelection = ({
- contextChips,
- objectMetadataItemId,
-}: {
- contextChips: CommandMenuContextChipProps[];
- objectMetadataItemId: string;
-}) => {
- const { objectMetadataItem } = useObjectMetadataItemById({
- objectId: objectMetadataItemId,
- });
-
- const { records, loading, totalCount } =
- useFindManyRecordsSelectedInContextStore({
- limit: 3,
- });
-
- const { openCommandMenu } = useCommandMenu();
-
- if (loading) {
- return null;
- }
-
- const Avatars = records.map((record) => (
-
- ));
-
- const recordSelectionContextChip =
- totalCount && records.length > 0
- ? {
- text: getSelectedRecordsContextText(
- objectMetadataItem,
- records,
- totalCount,
- ),
- Icons: Avatars,
- onClick: contextChips.length > 0 ? openCommandMenu : undefined,
- withIconBackground: false,
- }
- : undefined;
-
- const contextChipsWithRecordSelection = [
- recordSelectionContextChip,
- ...contextChips,
- ].filter(isDefined);
-
- return (
-
- );
-};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx
new file mode 100644
index 0000000000..f1aee1bd5d
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx
@@ -0,0 +1,68 @@
+import { CommandMenuPageLayoutInfo } from '@/command-menu/components/CommandMenuPageLayoutInfo';
+import { CommandMenuRecordInfo } from '@/command-menu/components/CommandMenuRecordInfo';
+import { CommandMenuWorkflowStepInfo } from '@/command-menu/components/CommandMenuWorkflowStepInfo';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import styled from '@emotion/styled';
+import { isDefined } from 'twenty-shared/utils';
+import { OverflowingTextWithTooltip } from 'twenty-ui/display';
+import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
+
+const StyledPageTitle = styled.div`
+ color: ${({ theme }) => theme.font.color.primary};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ font-weight: ${({ theme }) => theme.font.weight.semiBold};
+`;
+
+type CommandMenuPageInfoProps = {
+ pageChip: CommandMenuContextChipProps | undefined;
+};
+
+export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
+ if (!isDefined(pageChip)) {
+ return null;
+ }
+
+ const isRecordPage = pageChip.page?.page === CommandMenuPages.ViewRecord;
+
+ if (isRecordPage && isDefined(pageChip.page?.pageId)) {
+ return (
+
+ );
+ }
+
+ const isWorkflowStepPage = pageChip.page?.page
+ ? [
+ CommandMenuPages.WorkflowStepEdit,
+ CommandMenuPages.WorkflowStepView,
+ CommandMenuPages.WorkflowRunStepView,
+ ].includes(pageChip.page?.page)
+ : false;
+
+ if (isWorkflowStepPage && isDefined(pageChip.page?.pageId)) {
+ return (
+
+ );
+ }
+
+ const isPageLayoutPage = pageChip.page?.page
+ ? [
+ CommandMenuPages.PageLayoutWidgetTypeSelect,
+ CommandMenuPages.PageLayoutGraphTypeSelect,
+ CommandMenuPages.PageLayoutGraphFilter,
+ CommandMenuPages.PageLayoutIframeSettings,
+ CommandMenuPages.PageLayoutTabSettings,
+ ].includes(pageChip.page?.page)
+ : false;
+
+ if (isPageLayoutPage) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx
new file mode 100644
index 0000000000..d9f9b1e5cb
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx
@@ -0,0 +1,160 @@
+import { usePageLayoutHeaderInfo } from '@/command-menu/components/hooks/usePageLayoutHeaderInfo';
+import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
+import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
+import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab';
+import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
+import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
+import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
+import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
+import { TitleInput } from '@/ui/input/components/TitleInput';
+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 styled from '@emotion/styled';
+import { isNonEmptyString } from '@sniptt/guards';
+import { useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+import { useIcons } from 'twenty-ui/display';
+
+const StyledPageLayoutInfoContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(0.5)};
+`;
+
+const StyledPageLayoutIcon = styled.div<{ iconColor: string }>`
+ align-items: center;
+ background: ${({ theme }) => theme.background.transparent.light};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ color: ${({ iconColor }) => iconColor};
+ display: flex;
+ flex-shrink: 0;
+ justify-content: center;
+ padding: ${({ theme }) => theme.spacing(1)};
+`;
+
+const StyledPageLayoutTitleContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 1;
+ font-size: ${({ theme }) => theme.font.size.sm};
+ font-weight: ${({ theme }) => theme.font.weight.semiBold};
+ min-width: 0;
+`;
+
+const StyledPageLayoutType = styled.div`
+ color: ${({ theme }) => theme.font.color.tertiary};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ white-space: nowrap;
+`;
+
+export const CommandMenuPageLayoutInfo = () => {
+ const theme = useTheme();
+ const { getIcon } = useIcons();
+ const commandMenuPage = useRecoilValue(commandMenuPageState);
+ const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
+
+ const draftPageLayout = useRecoilComponentValue(
+ pageLayoutDraftComponentState,
+ pageLayoutId,
+ );
+
+ const pageLayoutEditingWidgetId = useRecoilComponentValue(
+ pageLayoutEditingWidgetIdComponentState,
+ pageLayoutId,
+ );
+
+ const [openTabId] = useRecoilComponentState(
+ pageLayoutTabSettingsOpenTabIdComponentState,
+ pageLayoutId,
+ );
+
+ const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
+ const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
+ const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId);
+
+ const [editedTitle, setEditedTitle] = useState(null);
+
+ const headerInfo = usePageLayoutHeaderInfo({
+ commandMenuPage,
+ draftPageLayout,
+ pageLayoutEditingWidgetId,
+ openTabId,
+ editedTitle,
+ });
+
+ if (!headerInfo) {
+ return null;
+ }
+
+ const {
+ headerIcon,
+ headerIconColor,
+ headerType,
+ title,
+ isReadonly,
+ tab,
+ widgetInEditMode,
+ } = headerInfo;
+
+ const Icon = headerIcon ?? getIcon('IconDefault');
+
+ const handleTitleChange = (newTitle: string) => {
+ setEditedTitle(newTitle);
+ };
+
+ const saveTitle = async () => {
+ const finalTitle = editedTitle ?? title;
+
+ if (!isNonEmptyString(finalTitle)) {
+ return;
+ }
+
+ updateCommandMenuPageInfo({
+ pageTitle: finalTitle,
+ pageIcon: Icon,
+ });
+
+ if (
+ commandMenuPage === CommandMenuPages.PageLayoutTabSettings &&
+ isDefined(tab)
+ ) {
+ updatePageLayoutTab(tab.id, { title: finalTitle });
+ } else if (isDefined(widgetInEditMode)) {
+ updatePageLayoutWidget(widgetInEditMode.id, {
+ title: finalTitle,
+ });
+ }
+
+ setEditedTitle(null);
+ };
+
+ return (
+
+ {isDefined(headerIcon) && (
+
+
+
+ )}
+
+ setEditedTitle(null)}
+ onClickOutside={saveTitle}
+ onTab={saveTitle}
+ onShiftTab={saveTitle}
+ />
+
+ {headerType && {headerType}}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordInfo.tsx
new file mode 100644
index 0000000000..faea9a5ef3
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordInfo.tsx
@@ -0,0 +1,162 @@
+import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
+import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
+import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
+import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
+import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
+import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
+import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
+import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
+import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
+import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import styled from '@emotion/styled';
+import { Trans } from '@lingui/react/macro';
+import { isNonEmptyString } from '@sniptt/guards';
+import { useRecoilValue } from 'recoil';
+import { Avatar } from 'twenty-ui/display';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
+import { dateLocaleState } from '~/localization/states/dateLocaleState';
+import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
+
+const StyledRecordInfoContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(0.5)};
+`;
+
+const StyledRecordAvatar = styled.div`
+ align-items: center;
+ background: ${({ theme }) => theme.background.transparent.light};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ display: flex;
+ flex-shrink: 0;
+ justify-content: center;
+ padding: ${({ theme }) => theme.spacing(1)};
+`;
+
+const StyledRecordTitleContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 1;
+ font-size: ${({ theme }) => theme.font.size.sm};
+ font-weight: ${({ theme }) => theme.font.weight.semiBold};
+ min-width: 0;
+`;
+
+const StyledRecordDate = styled.div`
+ color: ${({ theme }) => theme.font.color.tertiary};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ white-space: nowrap;
+`;
+
+export const CommandMenuRecordInfo = ({
+ commandMenuPageInstanceId,
+}: {
+ commandMenuPageInstanceId: string;
+}) => {
+ const viewableRecordNameSingular = useRecoilComponentValue(
+ viewableRecordNameSingularComponentState,
+ commandMenuPageInstanceId,
+ );
+
+ const viewableRecordId = useRecoilComponentValue(
+ viewableRecordIdComponentState,
+ commandMenuPageInstanceId,
+ );
+
+ const { objectNameSingular, objectRecordId } = useRecordShowPage(
+ viewableRecordNameSingular!,
+ viewableRecordId!,
+ );
+
+ const recordCreatedAt = useRecoilValue(
+ recordStoreFamilySelector({
+ recordId: objectRecordId,
+ fieldName: 'createdAt',
+ }),
+ );
+
+ const recordIdentifier = useRecoilValue(
+ recordStoreIdentifierFamilySelector({
+ recordId: objectRecordId,
+ }),
+ );
+
+ const { localeCatalog } = useRecoilValue(dateLocaleState);
+ const beautifiedCreatedAt = isNonEmptyString(recordCreatedAt)
+ ? beautifyPastDateRelativeToNow(recordCreatedAt, localeCatalog)
+ : '';
+
+ const { objectMetadataItem } = useObjectMetadataItem({
+ objectNameSingular,
+ });
+
+ const { labelIdentifierFieldMetadataItem } =
+ useLabelIdentifierFieldMetadataItem({
+ objectNameSingular,
+ });
+
+ const isTitleReadOnly = useIsRecordFieldReadOnly({
+ recordId: objectRecordId,
+ fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
+ objectMetadataId: objectMetadataItem.id,
+ });
+
+ const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
+ objectNameSingular,
+ objectRecordId,
+ });
+
+ const fieldDefinition = {
+ type: labelIdentifierFieldMetadataItem?.type ?? FieldMetadataType.TEXT,
+ iconName: '',
+ fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
+ label: labelIdentifierFieldMetadataItem?.label ?? '',
+ metadata: {
+ fieldName: labelIdentifierFieldMetadataItem?.name ?? '',
+ objectMetadataNameSingular: objectNameSingular,
+ },
+ defaultValue: labelIdentifierFieldMetadataItem?.defaultValue,
+ };
+
+ return (
+
+ {recordIdentifier && (
+
+
+
+ )}
+
+
+
+
+
+ {beautifiedCreatedAt && (
+
+ Created {beautifiedCreatedAt}
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx
index d1147f5ee6..586106b1d1 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx
@@ -1,49 +1,50 @@
-import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
-import { CommandMenuContextChipGroups } from '@/command-menu/components/CommandMenuContextChipGroups';
-import { CommandMenuContextChipGroupsWithRecordSelection } from '@/command-menu/components/CommandMenuContextChipGroupsWithRecordSelection';
+import { CommandMenuBackButton } from '@/command-menu/components/CommandMenuBackButton';
+import { CommandMenuPageInfo } from '@/command-menu/components/CommandMenuPageInfo';
import { CommandMenuTopBarInputFocusEffect } from '@/command-menu/components/CommandMenuTopBarInputFocusEffect';
-import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
+import { CommandMenuTopBarRightCornerIcon } from '@/command-menu/components/CommandMenuTopBarRightCornerIcon';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
+import { COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE } from '@/command-menu/constants/CommandMenuSearchBarHeightMobile';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
-import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
+import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
-import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
-import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { useRef } from 'react';
-import { useLocation } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
-import { AppBasePath } from 'twenty-shared/types';
-import { isDefined } from 'twenty-shared/utils';
-import { IconChevronLeft, IconX } from 'twenty-ui/display';
-import { Button } from 'twenty-ui/input';
-import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
+import { IconX } from 'twenty-ui/display';
+import { IconButton } from 'twenty-ui/input';
+import { useIsMobile } from 'twenty-ui/utilities';
-const StyledInputContainer = styled.div`
+const StyledInputContainer = styled.div<{ isMobile: boolean }>`
align-items: center;
- background-color: ${({ theme }) => theme.background.transparent.lighter};
+ background-color: ${({ theme }) => theme.background.secondary};
border: none;
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 0;
+ box-sizing: border-box;
display: flex;
justify-content: space-between;
font-size: ${({ theme }) => theme.font.size.lg};
- height: ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px;
+ height: ${({ isMobile }) =>
+ isMobile
+ ? COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE
+ : COMMAND_MENU_SEARCH_BAR_HEIGHT}px;
margin: 0;
outline: none;
position: relative;
+ overflow: hidden;
padding: 0 ${({ theme }) => theme.spacing(COMMAND_MENU_SEARCH_BAR_PADDING)};
- gap: ${({ theme }) => theme.spacing(1)};
+ gap: ${({ theme }) => theme.spacing(4)};
flex-shrink: 0;
+ justify-content: space-between;
`;
const StyledInput = styled.input`
@@ -69,10 +70,8 @@ const StyledContentContainer = styled.div`
display: flex;
flex: 1;
gap: ${({ theme }) => theme.spacing(1)};
-`;
-
-const StyledCloseButtonWrapper = styled.div<{ isVisible: boolean }>`
- visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
+ min-width: 0;
+ overflow: hidden;
`;
export const CommandMenuTopBar = () => {
@@ -91,83 +90,75 @@ export const CommandMenuTopBar = () => {
const { closeCommandMenu } = useCommandMenu();
- const { goBackFromCommandMenu } = useCommandMenuHistory();
-
- const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValue(
- contextStoreCurrentObjectMetadataItemIdComponentState,
- COMMAND_MENU_COMPONENT_INSTANCE_ID,
- );
-
const commandMenuPage = useRecoilValue(commandMenuPageState);
+ const commandMenuNavigationStack = useRecoilValue(
+ commandMenuNavigationStackState,
+ );
+
const theme = useTheme();
const { contextChips } = useCommandMenuContextChips();
- const location = useLocation();
- const isButtonVisible =
- !location.pathname.startsWith(`${AppBasePath.Root}objects/`) &&
- !location.pathname.startsWith(`${AppBasePath.Root}object/`);
+ const canGoBack = commandMenuNavigationStack.length > 1;
- const backButtonAnimationDuration =
- contextChips.length > 0 ? theme.animation.duration.instant : 0;
+ const shouldShowCloseButton =
+ !isMobile && commandMenuNavigationStack.length === 1;
+
+ const shouldShowBackButton = canGoBack;
+
+ const lastChip = contextChips.at(-1);
return (
-
+
- {commandMenuPage !== CommandMenuPages.Root && (
+ {shouldShowBackButton && (
- ]}
- onClick={goBackFromCommandMenu}
- testId="command-menu-go-back-button"
- forceEmptyText={true}
+
+
+ )}
+ {shouldShowCloseButton && (
+
+
)}
- {isDefined(contextStoreCurrentObjectMetadataItemId) &&
- commandMenuPage !== CommandMenuPages.SearchRecords ? (
-
- ) : (
-
- )}
+ {lastChip &&
+ commandMenuPage !== CommandMenuPages.Root &&
+ commandMenuPage !== CommandMenuPages.SearchRecords && (
+
+ )}
{(commandMenuPage === CommandMenuPages.Root ||
commandMenuPage === CommandMenuPages.SearchRecords) && (
<>
>
)}
- {!isMobile && (
-
-
-
- )}
+
);
};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBarRightCornerIcon.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBarRightCornerIcon.tsx
new file mode 100644
index 0000000000..1be8d042e2
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBarRightCornerIcon.tsx
@@ -0,0 +1,63 @@
+import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
+import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
+import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
+import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
+import styled from '@emotion/styled';
+import { useRecoilValue } from 'recoil';
+import { IconHandMove, IconSparkles } from 'twenty-ui/display';
+import { IconButton } from 'twenty-ui/input';
+import { useIsMobile } from 'twenty-ui/utilities';
+import { FeatureFlagKey } from '~/generated/graphql';
+
+const StyledIconButton = styled(IconButton)`
+ color: ${({ theme }) => theme.font.color.secondary};
+`;
+
+export const CommandMenuTopBarRightCornerIcon = () => {
+ const isMobile = useIsMobile();
+ const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
+ const commandMenuPage = useRecoilValue(commandMenuPageState);
+ const commandMenuNavigationStack = useRecoilValue(
+ commandMenuNavigationStackState,
+ );
+
+ const { goBackFromCommandMenu } = useCommandMenuHistory();
+ const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
+
+ if (isMobile || !isAiEnabled) {
+ return null;
+ }
+
+ const isOnAskAIPage = [
+ CommandMenuPages.AskAI,
+ CommandMenuPages.ViewPreviousAIChats,
+ ].includes(commandMenuPage);
+
+ const canGoBack = commandMenuNavigationStack.length > 1;
+
+ if (!isOnAskAIPage) {
+ return (
+ openAskAIPage({ resetNavigationStack: false })}
+ Icon={IconSparkles}
+ variant="tertiary"
+ size="small"
+ />
+ );
+ }
+
+ if (!canGoBack) {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuWorkflowStepInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuWorkflowStepInfo.tsx
new file mode 100644
index 0000000000..e67b2eff37
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuWorkflowStepInfo.tsx
@@ -0,0 +1,230 @@
+import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
+import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState';
+import { commandMenuWorkflowStepIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState';
+import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
+import { TitleInput } from '@/ui/input/components/TitleInput';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { type WorkflowVersion } from '@/workflow/types/Workflow';
+import { getAgentIdFromStep } from '@/workflow/utils/getAgentIdFromStep';
+import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
+import { useUpdateAgentLabel } from '@/workflow/workflow-steps/hooks/useUpdateAgentLabel';
+import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep';
+import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
+import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
+import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
+import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
+import { useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
+import { useIcons } from 'twenty-ui/display';
+
+const StyledWorkflowStepInfoContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(0.5)};
+`;
+
+const StyledWorkflowStepIcon = styled.div<{ iconColor: string }>`
+ align-items: center;
+ background: ${({ theme }) => theme.background.transparent.light};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ color: ${({ iconColor }) => iconColor};
+ display: flex;
+ flex-shrink: 0;
+ justify-content: center;
+ padding: ${({ theme }) => theme.spacing(1)};
+`;
+
+const StyledWorkflowStepTitleContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 1;
+ font-size: ${({ theme }) => theme.font.size.sm};
+ font-weight: ${({ theme }) => theme.font.weight.semiBold};
+ min-width: 0;
+`;
+
+const StyledWorkflowStepType = styled.div`
+ color: ${({ theme }) => theme.font.color.tertiary};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ white-space: nowrap;
+`;
+
+export const CommandMenuWorkflowStepInfo = ({
+ commandMenuPageInstanceId,
+}: {
+ commandMenuPageInstanceId: string;
+}) => {
+ const theme = useTheme();
+ const { getIcon } = useIcons();
+
+ const commandMenuPage = useRecoilValue(commandMenuPageState);
+
+ const workflowId = useRecoilComponentValue(
+ commandMenuWorkflowIdComponentState,
+ commandMenuPageInstanceId,
+ );
+
+ const workflowStepId = useRecoilComponentValue(
+ commandMenuWorkflowStepIdComponentState,
+ commandMenuPageInstanceId,
+ );
+
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
+
+ const isReadonly =
+ commandMenuPage === CommandMenuPages.WorkflowStepView ||
+ commandMenuPage === CommandMenuPages.WorkflowRunStepView;
+
+ const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
+ const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
+ const { updateOneRecord: updateOneWorkflowVersion } =
+ useUpdateOneRecord({
+ objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
+ });
+
+ const {
+ trigger,
+ steps,
+ id: workflowVersionId,
+ } = workflowWithCurrentVersion?.currentVersion ?? {
+ trigger: null,
+ steps: null,
+ id: undefined,
+ };
+
+ const isTriggerStep = workflowStepId === TRIGGER_STEP_ID;
+
+ const stepDefinition =
+ isDefined(workflowStepId) && isDefined(trigger)
+ ? isTriggerStep || isDefined(steps)
+ ? getStepDefinitionOrThrow({
+ stepId: workflowStepId,
+ trigger,
+ steps,
+ })
+ : undefined
+ : undefined;
+
+ const isTrigger = stepDefinition?.type === 'trigger';
+
+ const agentId = getAgentIdFromStep(stepDefinition);
+ const { updateAgentLabel } = useUpdateAgentLabel(agentId);
+ const stepName =
+ isDefined(stepDefinition) && isDefined(stepDefinition.definition)
+ ? isTrigger
+ ? (stepDefinition.definition.name ??
+ (stepDefinition.definition.type === 'MANUAL'
+ ? 'Launch manually'
+ : 'Trigger'))
+ : (stepDefinition.definition.name ?? 'Action')
+ : '';
+
+ const [editedTitle, setEditedTitle] = useState(null);
+
+ const title =
+ editedTitle ?? (isDefined(stepName) && stepName !== '' ? stepName : '');
+
+ const handleTitleChange = (newTitle: string) => {
+ setEditedTitle(newTitle);
+ };
+
+ if (
+ !isDefined(workflowId) ||
+ !isDefined(workflowStepId) ||
+ !isDefined(workflowWithCurrentVersion?.currentVersion) ||
+ !isDefined(stepDefinition) ||
+ !isDefined(stepDefinition.definition)
+ ) {
+ return null;
+ }
+
+ const headerIcon = isTrigger
+ ? getTriggerIcon(stepDefinition.definition)
+ : getActionIcon(stepDefinition.definition.type);
+
+ const headerIconColor = isTrigger
+ ? getTriggerIconColor({
+ theme,
+ triggerType: stepDefinition.definition.type,
+ })
+ : getActionIconColorOrThrow({
+ theme,
+ actionType: stepDefinition.definition.type,
+ });
+
+ const headerType = isTrigger ? 'Trigger' : 'Action';
+
+ const Icon = getIcon(headerIcon ?? 'IconDefault');
+
+ const saveTitle = async () => {
+ if (!isDefined(workflowVersionId)) {
+ return;
+ }
+
+ updateCommandMenuPageInfo({
+ pageTitle: title,
+ pageIcon: Icon,
+ });
+
+ if (isTrigger) {
+ await updateOneWorkflowVersion({
+ idToUpdate: workflowVersionId,
+ updateOneRecordInput: {
+ trigger: {
+ ...stepDefinition.definition,
+ name: title,
+ } as typeof stepDefinition.definition,
+ },
+ });
+ } else {
+ await updateWorkflowVersionStep({
+ workflowVersionId,
+ step: {
+ ...stepDefinition.definition,
+ name: title,
+ },
+ });
+
+ if (isDefined(agentId)) {
+ await updateAgentLabel(title);
+ }
+ }
+ };
+
+ return (
+
+ {headerIcon && (
+
+
+
+ )}
+
+ setEditedTitle(null)}
+ onClickOutside={saveTitle}
+ onTab={saveTitle}
+ onShiftTab={saveTitle}
+ />
+
+
+ {isTrigger ? t`Trigger` : t`Action`}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
index d9239fbb78..53bff5bf01 100644
--- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx
@@ -146,7 +146,7 @@ export const LimitedPermissions: Story = {
export const MatchingNavigate: Story = {
play: async () => {
const canvas = within(document.body);
- const searchInput = await canvas.findByPlaceholderText('Type anything');
+ const searchInput = await canvas.findByTestId('command-menu-search-input');
await sleep(openTimeout);
await userEvent.type(searchInput, 'ta');
expect(await canvas.findByText('Go to Tasks')).toBeVisible();
@@ -156,7 +156,7 @@ export const MatchingNavigate: Story = {
export const MatchingNavigateShortcuts: Story = {
play: async () => {
const canvas = within(document.body);
- const searchInput = await canvas.findByPlaceholderText('Type anything');
+ const searchInput = await canvas.findByTestId('command-menu-search-input');
await sleep(openTimeout);
await userEvent.type(searchInput, 'gp');
expect(await canvas.findByText('Go to People')).toBeVisible();
@@ -169,7 +169,7 @@ export const MatchingNavigateShortcuts: Story = {
// const canvas = within(document.body);
// const searchRecordsButton = await canvas.findByText('Search records');
// await userEvent.click(searchRecordsButton);
-// const searchInput = await canvas.findByPlaceholderText('Type anything');
+// const searchInput = await canvas.findByPlaceholderText('Type anything...');
// await sleep(openTimeout);
// await userEvent.type(searchInput, 'n');
// expect(await canvas.findByText('Linkedin')).toBeVisible();
@@ -181,7 +181,7 @@ export const MatchingNavigateShortcuts: Story = {
export const NoResultsSearchFallback: Story = {
play: async () => {
const canvas = within(document.body);
- const searchInput = await canvas.findByPlaceholderText('Type anything');
+ const searchInput = await canvas.findByTestId('command-menu-search-input');
await sleep(openTimeout);
await userEvent.type(searchInput, 'input without results');
expect(await canvas.findByText('No results found')).toBeVisible();
@@ -191,6 +191,7 @@ export const NoResultsSearchFallback: Story = {
parameters: {
msw: {
handlers: [
+ ...graphqlMocks.handlers,
graphql.query('Search', () => {
return HttpResponse.json({
data: {
diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextChipGroups.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextChipGroups.stories.tsx
deleted file mode 100644
index 3ad69153c6..0000000000
--- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenuContextChipGroups.stories.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { type Meta, type StoryObj } from '@storybook/react';
-
-import styled from '@emotion/styled';
-import { CommandMenuContextChipGroups } from '../CommandMenuContextChipGroups';
-import { ComponentDecorator } from 'twenty-ui/testing';
-import {
- IconBuildingSkyscraper,
- IconSearch,
- IconSettingsAutomation,
- IconUser,
-} from 'twenty-ui/display';
-
-const StyledContainer = styled.div`
- display: flex;
- flex-direction: row;
- gap: ${({ theme }) => theme.spacing(1)};
-`;
-
-const meta: Meta = {
- title: 'Modules/CommandMenu/CommandMenuContextChipGroups',
- component: CommandMenuContextChipGroups,
- decorators: [
- (Story) => {Story()},
- ComponentDecorator,
- ],
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {
- args: {
- contextChips: [
- {
- Icons: [],
- text: 'Company',
- },
- {
- Icons: [],
- text: 'Person',
- },
- ],
- },
-};
-
-export const SingleChip: Story = {
- args: {
- contextChips: [
- {
- Icons: [],
- text: 'Person',
- },
- ],
- },
-};
-
-export const ThreeChipsWithIcons: Story = {
- args: {
- contextChips: [
- {
- Icons: [],
- text: 'Company',
- },
- {
- Icons: [],
- text: 'Person',
- },
- {
- Icons: [],
- text: 'Settings',
- },
- ],
- },
-};
-
-export const FourChipsWithIcons: Story = {
- args: {
- contextChips: [
- {
- Icons: [],
- text: 'Company',
- },
- {
- Icons: [],
- text: 'Person',
- },
- {
- Icons: [],
- text: 'Settings',
- },
- {
- Icons: [],
- text: 'Search',
- },
- ],
- },
-};
diff --git a/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts b/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts
new file mode 100644
index 0000000000..cbc15dc30b
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts
@@ -0,0 +1,151 @@
+import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
+import { isChartWidget } from '@/command-menu/pages/page-layout/utils/isChartWidget';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
+import { useTheme } from '@emotion/react';
+import { t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+import {
+ IconAppWindow,
+ IconFrame,
+ type IconComponent,
+} from 'twenty-ui/display';
+import { type PageLayoutWidget } from '~/generated/graphql';
+
+type PageLayoutHeaderInfo = {
+ headerIcon: IconComponent | undefined;
+ headerIconColor: string;
+ headerType: string;
+ title: string;
+ isReadonly: boolean;
+ tab: PageLayoutTab | undefined;
+ widgetInEditMode: PageLayoutWidget | undefined;
+};
+
+type UsePageLayoutHeaderInfoParams = {
+ commandMenuPage: CommandMenuPages;
+ draftPageLayout: {
+ tabs: PageLayoutTab[];
+ };
+ pageLayoutEditingWidgetId: string | null | undefined;
+ openTabId: string | null | undefined;
+ editedTitle: string | null | undefined;
+};
+
+export const usePageLayoutHeaderInfo = ({
+ commandMenuPage,
+ draftPageLayout,
+ pageLayoutEditingWidgetId,
+ openTabId,
+ editedTitle,
+}: UsePageLayoutHeaderInfoParams): PageLayoutHeaderInfo | null => {
+ const theme = useTheme();
+ const iconColor = theme.font.color.tertiary;
+
+ switch (commandMenuPage) {
+ case CommandMenuPages.PageLayoutTabSettings: {
+ if (!isDefined(openTabId)) {
+ return null;
+ }
+
+ const tab = draftPageLayout.tabs.find((t) => t.id === openTabId);
+
+ if (!isDefined(tab)) {
+ return null;
+ }
+
+ const title = isDefined(editedTitle)
+ ? editedTitle
+ : isDefined(tab.title) && tab.title !== ''
+ ? tab.title
+ : '';
+
+ return {
+ headerIcon: IconAppWindow,
+ headerIconColor: iconColor,
+ headerType: t`Tab`,
+ title,
+ isReadonly: false,
+ tab,
+ widgetInEditMode: undefined,
+ };
+ }
+
+ case CommandMenuPages.PageLayoutIframeSettings: {
+ if (!isDefined(pageLayoutEditingWidgetId)) {
+ return null;
+ }
+
+ const widgetInEditMode = draftPageLayout.tabs
+ .flatMap((tab) => tab.widgets)
+ .find((widget) => widget.id === pageLayoutEditingWidgetId);
+
+ if (!isDefined(widgetInEditMode)) {
+ return null;
+ }
+
+ const title = isDefined(editedTitle)
+ ? editedTitle
+ : isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
+ ? widgetInEditMode.title
+ : '';
+
+ return {
+ headerIcon: IconFrame,
+ headerIconColor: iconColor,
+ headerType: t`iFrame Widget`,
+ title,
+ isReadonly: false,
+ tab: undefined,
+ widgetInEditMode,
+ };
+ }
+
+ case CommandMenuPages.PageLayoutGraphTypeSelect:
+ case CommandMenuPages.PageLayoutGraphFilter: {
+ if (!isDefined(pageLayoutEditingWidgetId)) {
+ return null;
+ }
+
+ const widgetInEditMode = draftPageLayout.tabs
+ .flatMap((tab) => tab.widgets)
+ .find((widget) => widget.id === pageLayoutEditingWidgetId);
+
+ if (!isDefined(widgetInEditMode)) {
+ return null;
+ }
+
+ if (!isChartWidget(widgetInEditMode)) {
+ return null;
+ }
+
+ const currentGraphType = widgetInEditMode.configuration.graphType;
+ const graphTypeInfo = GRAPH_TYPE_INFORMATION[currentGraphType];
+ const graphTypeLabel = t(graphTypeInfo.label);
+
+ const headerType =
+ commandMenuPage === CommandMenuPages.PageLayoutGraphFilter
+ ? t`${graphTypeLabel} Chart`
+ : t`Chart`;
+
+ const title = isDefined(editedTitle)
+ ? editedTitle
+ : isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
+ ? widgetInEditMode.title
+ : '';
+
+ return {
+ headerIcon: graphTypeInfo.icon,
+ headerIconColor: iconColor,
+ headerType,
+ title,
+ isReadonly: commandMenuPage === CommandMenuPages.PageLayoutGraphFilter,
+ tab: undefined,
+ widgetInEditMode,
+ };
+ }
+
+ default:
+ return null;
+ }
+};
diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuNavigationHistoryDropdownId.ts b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuNavigationHistoryDropdownId.ts
new file mode 100644
index 0000000000..8bf0c9b33f
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuNavigationHistoryDropdownId.ts
@@ -0,0 +1,2 @@
+export const COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID =
+ 'command-menu-navigation-history-dropdown';
diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSearchBarHeight.ts b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSearchBarHeight.ts
index f76e7d4d84..a6d01f1b73 100644
--- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSearchBarHeight.ts
+++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSearchBarHeight.ts
@@ -1 +1 @@
-export const COMMAND_MENU_SEARCH_BAR_HEIGHT = 56;
+export const COMMAND_MENU_SEARCH_BAR_HEIGHT = 40;
diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSearchBarHeightMobile.ts b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSearchBarHeightMobile.ts
new file mode 100644
index 0000000000..0d5a6fa713
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSearchBarHeightMobile.ts
@@ -0,0 +1 @@
+export const COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE = 52;
diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSidePanelWidth.ts b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSidePanelWidth.ts
new file mode 100644
index 0000000000..5e08c69137
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuSidePanelWidth.ts
@@ -0,0 +1 @@
+export const COMMAND_MENU_SIDE_PANEL_WIDTH = 400;
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx
index ecc943a04a..bb674f3276 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx
+++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx
@@ -8,6 +8,8 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
+import { IconDotsVertical } from 'twenty-ui/display';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
@@ -84,4 +86,45 @@ describe('useCommandMenu', () => {
expect(result.current.isCommandMenuOpened).toBe(false);
});
+
+ it('should navigate command menu and reset navigation stack when resetNavigationStack is true', () => {
+ const { result } = renderHooks();
+
+ act(() => {
+ result.current.commandMenu.navigateCommandMenu({
+ page: CommandMenuPages.Root,
+ pageTitle: 'First Page',
+ pageIcon: IconDotsVertical,
+ resetNavigationStack: false,
+ });
+ });
+
+ expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
+ expect(result.current.commandMenuPageInfo.title).toBe('First Page');
+ expect(result.current.commandMenuNavigationStack).toHaveLength(1);
+
+ act(() => {
+ result.current.commandMenu.navigateCommandMenu({
+ page: CommandMenuPages.SearchRecords,
+ pageTitle: 'Second Page',
+ pageIcon: IconDotsVertical,
+ resetNavigationStack: false,
+ });
+ });
+
+ expect(result.current.commandMenuNavigationStack).toHaveLength(2);
+
+ act(() => {
+ result.current.commandMenu.navigateCommandMenu({
+ page: CommandMenuPages.Root,
+ pageTitle: 'Reset Page',
+ pageIcon: IconDotsVertical,
+ resetNavigationStack: true,
+ });
+ });
+
+ expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
+ expect(result.current.commandMenuPageInfo.title).toBe('Reset Page');
+ expect(result.current.commandMenuNavigationStack).toHaveLength(1);
+ });
});
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useFilterActionsWithCommandMenuSearch.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useFilterActionsWithCommandMenuSearch.test.tsx
new file mode 100644
index 0000000000..1bf16b420a
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useFilterActionsWithCommandMenuSearch.test.tsx
@@ -0,0 +1,102 @@
+import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
+import { ActionScope } from '@/action-menu/actions/types/ActionScope';
+import { ActionType } from '@/action-menu/actions/types/ActionType';
+import { renderHook } from '@testing-library/react';
+import { IconPlus } from 'twenty-ui/display';
+import { useFilterActionsWithCommandMenuSearch } from '../useFilterActionsWithCommandMenuSearch';
+
+const MockComponent = Mock Component
;
+
+describe('useFilterActionsWithCommandMenuSearch', () => {
+ const mockActions: ActionConfig[] = [
+ {
+ key: 'action-1',
+ label: 'Create Record',
+ type: ActionType.Standard,
+ scope: ActionScope.Global,
+ position: 1,
+ Icon: IconPlus,
+ shouldBeRegistered: () => true,
+ component: MockComponent,
+ hotKeys: ['c', 'r'],
+ },
+ {
+ key: 'action-2',
+ label: 'Delete Record',
+ type: ActionType.Standard,
+ scope: ActionScope.RecordSelection,
+ position: 2,
+ Icon: IconPlus,
+ shouldBeRegistered: () => true,
+ component: MockComponent,
+ hotKeys: ['d', 'e', 'l'],
+ },
+ {
+ key: 'action-3',
+ label: 'Update Record',
+ type: ActionType.Standard,
+ scope: ActionScope.Object,
+ position: 3,
+ Icon: IconPlus,
+ shouldBeRegistered: () => true,
+ component: MockComponent,
+ },
+ ];
+
+ it('should return all actions when search is empty', () => {
+ const { result } = renderHook(() =>
+ useFilterActionsWithCommandMenuSearch({ commandMenuSearch: '' }),
+ );
+
+ const filtered =
+ result.current.filterActionsWithCommandMenuSearch(mockActions);
+
+ expect(filtered).toEqual(mockActions);
+ });
+
+ it('should filter actions by label', () => {
+ const { result } = renderHook(() =>
+ useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'Create' }),
+ );
+
+ const filtered =
+ result.current.filterActionsWithCommandMenuSearch(mockActions);
+
+ expect(filtered).toHaveLength(1);
+ expect(filtered[0].key).toBe('action-1');
+ });
+
+ it('should filter actions by hotkeys', () => {
+ const { result } = renderHook(() =>
+ useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'del' }),
+ );
+
+ const filtered =
+ result.current.filterActionsWithCommandMenuSearch(mockActions);
+
+ expect(filtered).toHaveLength(1);
+ expect(filtered[0].key).toBe('action-2');
+ });
+
+ it('should return empty array when no actions match', () => {
+ const { result } = renderHook(() =>
+ useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'xyz' }),
+ );
+
+ const filtered =
+ result.current.filterActionsWithCommandMenuSearch(mockActions);
+
+ expect(filtered).toEqual([]);
+ });
+
+ it('should match actions by either label or hotkeys', () => {
+ const { result } = renderHook(() =>
+ useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'Record' }),
+ );
+
+ const filtered =
+ result.current.filterActionsWithCommandMenuSearch(mockActions);
+
+ expect(filtered).toHaveLength(3);
+ });
+});
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts
index 5535b8e8cf..223c325b46 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts
@@ -69,7 +69,7 @@ export const useCommandMenuHotKeys = () => {
keys: ['@'],
callback: () => {
if (isAiEnabled) {
- openAskAIPage();
+ openAskAIPage({ resetNavigationStack: true });
}
},
containsModifier: false,
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenAskAIPageInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenAskAIPageInCommandMenu.ts
index 2a896cda7e..bb16ee47b1 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/useOpenAskAIPageInCommandMenu.ts
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenAskAIPageInCommandMenu.ts
@@ -1,18 +1,33 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
+import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
+import { useRecoilValue } from 'recoil';
import { IconSparkles } from 'twenty-ui/display';
import { v4 } from 'uuid';
export const useOpenAskAIPageInCommandMenu = () => {
const { navigateCommandMenu } = useCommandMenu();
+ const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
+
+ const openAskAIPage = ({
+ pageTitle,
+ resetNavigationStack,
+ }: {
+ pageTitle?: string | null;
+ resetNavigationStack?: boolean;
+ } = {}) => {
+ const shouldReset =
+ resetNavigationStack !== undefined
+ ? resetNavigationStack
+ : isCommandMenuOpened;
- const openAskAIPage = (pageTitle?: string | null) => {
navigateCommandMenu({
page: CommandMenuPages.AskAI,
pageTitle: pageTitle ?? t`Ask AI`,
pageIcon: IconSparkles,
pageId: v4(),
+ resetNavigationStack: shouldReset,
});
};
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu.ts
index 158735e2a9..6ce936f714 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu.ts
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu.ts
@@ -1,11 +1,14 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
+import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
+import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { IconSearch } from 'twenty-ui/display';
export const useOpenRecordsSearchPageInCommandMenu = () => {
const { navigateCommandMenu } = useCommandMenu();
+ const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
const openRecordsSearchPage = () => {
navigateCommandMenu({
@@ -13,6 +16,7 @@ export const useOpenRecordsSearchPageInCommandMenu = () => {
pageTitle: t`Search`,
pageIcon: IconSearch,
pageId: v4(),
+ resetNavigationStack: isCommandMenuOpened,
});
};
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts
index ef0d7434da..397d88e813 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useWorkflowCommandMenu.ts
@@ -1,12 +1,15 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState';
import { commandMenuWorkflowRunIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowRunIdComponentState';
+import { commandMenuWorkflowStepIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState';
import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { type WorkflowRunStepStatus } from '@/workflow/types/Workflow';
import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab';
+import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
import {
IconBolt,
type IconComponent,
@@ -67,7 +70,12 @@ export const useWorkflowCommandMenu = () => {
const openWorkflowEditStepInCommandMenu = useRecoilCallback(
({ set }) => {
- return (workflowId: string, title: string, icon: IconComponent) => {
+ return (
+ workflowId: string,
+ title: string,
+ icon: IconComponent,
+ stepId?: string,
+ ) => {
const pageId = v4();
set(
@@ -77,6 +85,22 @@ export const useWorkflowCommandMenu = () => {
workflowId,
);
+ if (isDefined(stepId)) {
+ set(
+ commandMenuWorkflowStepIdComponentState.atomFamily({
+ instanceId: pageId,
+ }),
+ stepId,
+ );
+
+ set(
+ workflowSelectedNodeComponentState.atomFamily({
+ instanceId: workflowId,
+ }),
+ stepId,
+ );
+ }
+
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepEdit,
pageTitle: title,
@@ -118,11 +142,13 @@ export const useWorkflowCommandMenu = () => {
workflowVersionId,
title,
icon,
+ stepId,
}: {
workflowId: string;
workflowVersionId: string;
title: string;
icon: IconComponent;
+ stepId?: string;
}) => {
const pageId = v4();
@@ -139,6 +165,22 @@ export const useWorkflowCommandMenu = () => {
workflowVersionId,
);
+ if (isDefined(stepId)) {
+ set(
+ commandMenuWorkflowStepIdComponentState.atomFamily({
+ instanceId: pageId,
+ }),
+ stepId,
+ );
+
+ set(
+ workflowSelectedNodeComponentState.atomFamily({
+ instanceId: workflowVersionId,
+ }),
+ stepId,
+ );
+ }
+
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepView,
pageTitle: title,
@@ -181,6 +223,19 @@ export const useWorkflowCommandMenu = () => {
}),
workflowRunId,
);
+ set(
+ commandMenuWorkflowStepIdComponentState.atomFamily({
+ instanceId: pageId,
+ }),
+ workflowSelectedNode,
+ );
+
+ set(
+ workflowSelectedNodeComponentState.atomFamily({
+ instanceId: workflowRunId,
+ }),
+ workflowSelectedNode,
+ );
navigateCommandMenu({
page: CommandMenuPages.WorkflowRunStepView,
diff --git a/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx
index f5732a23da..7b5584d0bb 100644
--- a/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx
+++ b/packages/twenty-front/src/modules/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage.tsx
@@ -172,6 +172,7 @@ export const CommandMenuMessageThreadPage = () => {
{canReply && !messageChannelLoading && (