diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx
index 9805b6375..6a0d8067c 100644
--- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx
+++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarAllActionsButton.tsx
@@ -1,4 +1,5 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconLayoutSidebarRightExpand, getOsControlSymbol } from 'twenty-ui';
@@ -38,11 +39,17 @@ const StyledSeparator = styled.div<{ size: 'sm' | 'md' }>`
export const RecordIndexActionMenuBarAllActionsButton = () => {
const theme = useTheme();
- const { openCommandMenu } = useCommandMenu();
+ const { navigateCommandMenu } = useCommandMenu();
return (
<>
- openCommandMenu()}>
+
+ navigateCommandMenu({
+ page: CommandMenuPages.Root,
+ })
+ }
+ >
All Actions
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 b5e2f027d..255010704 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx
@@ -1,4 +1,5 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
+import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
import { RecordAgnosticActionsKey } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKey';
@@ -91,7 +92,10 @@ export const CommandMenuContainer = ({
value={{
isInRightDrawer: false,
onActionExecutedCallback: ({ key }) => {
- if (key !== RecordAgnosticActionsKey.SEARCH_RECORDS) {
+ if (
+ key !== RecordAgnosticActionsKey.SEARCH_RECORDS &&
+ key !== NoSelectionRecordActionKeys.CREATE_NEW_RECORD
+ ) {
toggleCommandMenu();
}
},
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 895ca9356..02fe9fa61 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextChip.tsx
@@ -1,7 +1,12 @@
import styled from '@emotion/styled';
+import { isNonEmptyString } from '@sniptt/guards';
import { Fragment } from 'react/jsx-runtime';
+import { isDefined } from 'twenty-shared';
-const StyledChip = styled.div`
+const StyledChip = styled.button<{
+ withText: boolean;
+ onClick?: () => void;
+}>`
align-items: center;
background: ${({ theme }) => theme.background.transparent.light};
border: 1px solid ${({ theme }) => theme.border.color.medium};
@@ -10,11 +15,21 @@ const StyledChip = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(6)};
- padding: 0 ${({ theme }) => theme.spacing(2)};
+ /* If the chip has text, we add extra padding to have a more balanced design */
+ padding: 0
+ ${({ theme, withText }) => (withText ? theme.spacing(2) : theme.spacing(1))};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.text.lineHeight.lg};
color: ${({ theme }) => theme.font.color.primary};
+ cursor: ${({ onClick }) => (isDefined(onClick) ? 'pointer' : 'default')};
+
+ &:hover {
+ background: ${({ onClick, theme }) =>
+ isDefined(onClick)
+ ? theme.background.transparent.medium
+ : theme.background.transparent.light};
+ }
`;
const StyledIconsContainer = styled.div`
@@ -24,18 +39,26 @@ const StyledIconsContainer = styled.div`
export const CommandMenuContextChip = ({
Icons,
text,
+ onClick,
+ testId,
}: {
Icons: React.ReactNode[];
text?: string;
+ onClick?: () => void;
+ testId?: string;
}) => {
return (
-
+
{Icons.map((Icon, index) => (
{Icon}
))}
- {text}
+ {text && {text}}
);
};
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 220b9a15a..77832b49a 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx
@@ -9,12 +9,21 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
-import { IconX, LightIconButton, useIsMobile } from 'twenty-ui';
+import {
+ Button,
+ IconChevronLeft,
+ IconX,
+ LightIconButton,
+ getOsControlSymbol,
+ useIsMobile,
+} from 'twenty-ui';
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledInputContainer = styled.div`
align-items: center;
@@ -81,7 +90,7 @@ export const CommandMenuTopBar = () => {
const isMobile = useIsMobile();
- const { closeCommandMenu } = useCommandMenu();
+ const { closeCommandMenu, goBackFromCommandMenu } = useCommandMenu();
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
@@ -93,9 +102,22 @@ export const CommandMenuTopBar = () => {
const theme = useTheme();
+ const isCommandMenuV2Enabled = useIsFeatureEnabled(
+ FeatureFlagKey.IsCommandMenuV2Enabled,
+ );
+
return (
+ {isCommandMenuV2Enabled && (
+ ]}
+ onClick={() => {
+ goBackFromCommandMenu();
+ }}
+ testId="command-menu-go-back-button"
+ />
+ )}
{commandMenuPage !== CommandMenuPages.SearchRecords &&
isDefined(contextStoreCurrentObjectMetadataId) && (
{
)}
{!isMobile && (
-
-
-
+ <>
+ {isCommandMenuV2Enabled ? (
+
+ ) : (
+
+
+
+ )}
+ >
)}
);
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 3c6c9c920..204b9fd97 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
@@ -16,10 +16,13 @@ import { sleep } from '~/utils/sleep';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
+import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { HttpResponse, graphql } from 'msw';
+import { FeatureFlagKey } from '~/generated/graphql';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
import { getCompaniesMock } from '~/testing/mock-data/companies';
@@ -29,6 +32,20 @@ const openTimeout = 50;
const companiesMock = getCompaniesMock();
+// Mock workspace with feature flag enabled
+const mockWorkspaceWithFeatureFlag = {
+ ...mockCurrentWorkspace,
+ featureFlags: [
+ ...(mockCurrentWorkspace.featureFlags || []),
+ {
+ id: 'mock-id',
+ key: FeatureFlagKey.IsCommandMenuV2Enabled,
+ value: true,
+ workspaceId: mockCurrentWorkspace.id,
+ },
+ ],
+};
+
const ContextStoreDecorator: Decorator = (Story) => {
return (
= {
const setIsCommandMenuOpened = useSetRecoilState(
isCommandMenuOpenedState,
);
+ const setCommandMenuNavigationStack = useSetRecoilState(
+ commandMenuNavigationStackState,
+ );
- setCurrentWorkspace(mockCurrentWorkspace);
+ setCurrentWorkspace(mockWorkspaceWithFeatureFlag);
setCurrentWorkspaceMember(mockedWorkspaceMemberData);
setIsCommandMenuOpened(true);
+ setCommandMenuNavigationStack([
+ {
+ page: CommandMenuPages.Root,
+ },
+ ]);
return ;
},
@@ -168,3 +193,28 @@ export const NoResultsSearchFallback: Story = {
},
},
};
+
+export const GoBack: Story = {
+ play: async () => {
+ const canvas = within(document.body);
+ const goBackButton = await canvas.findByTestId(
+ 'command-menu-go-back-button',
+ );
+ await userEvent.click(goBackButton);
+ await expect(goBackButton).not.toBeVisible();
+ },
+};
+
+export const ClickOnSearchRecordsAndGoBack: Story = {
+ play: async () => {
+ const canvas = within(document.body);
+ const searchRecordsButton = await canvas.findByText('Search records');
+ await userEvent.click(searchRecordsButton);
+ await sleep(openTimeout);
+ const goBackButton = await canvas.findByTestId(
+ 'command-menu-go-back-button',
+ );
+ await userEvent.click(goBackButton);
+ expect(await canvas.findByText('Search records')).toBeVisible();
+ },
+};
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 ae593f49d..66b56a3b0 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
@@ -53,7 +53,9 @@ describe('useCommandMenu', () => {
const { result } = renderHooks();
act(() => {
- result.current.commandMenu.openCommandMenu();
+ result.current.commandMenu.navigateCommandMenu({
+ page: CommandMenuPages.Root,
+ });
});
expect(result.current.isCommandMenuOpened).toBe(true);
diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts
index a25545aa3..8d4fe6d1f 100644
--- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts
+++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts
@@ -42,8 +42,16 @@ export const useCommandMenu = () => {
const { resetContextStoreStates } = useResetContextStoreStates();
const openCommandMenu = useRecoilCallback(
- ({ set }) =>
+ ({ snapshot, set }) =>
() => {
+ const isCommandMenuOpened = snapshot
+ .getLoadable(isCommandMenuOpenedState)
+ .getValue();
+
+ if (isCommandMenuOpened) {
+ return;
+ }
+
copyContextStoreStates({
instanceIdToCopyFrom: mainContextStoreComponentInstanceId,
instanceIdToCopyTo: 'command-menu',
@@ -88,24 +96,6 @@ export const useCommandMenu = () => {
[goBackToPreviousHotkeyScope, resetContextStoreStates, resetSelectedItem],
);
- const toggleCommandMenu = useRecoilCallback(
- ({ snapshot, set }) =>
- async () => {
- const isCommandMenuOpened = snapshot
- .getLoadable(isCommandMenuOpenedState)
- .getValue();
-
- set(commandMenuSearchState, '');
-
- if (isCommandMenuOpened) {
- closeCommandMenu();
- } else {
- openCommandMenu();
- }
- },
- [closeCommandMenu, openCommandMenu],
- );
-
const navigateCommandMenu = useRecoilCallback(
({ snapshot, set }) => {
return ({
@@ -133,6 +123,26 @@ export const useCommandMenu = () => {
[openCommandMenu],
);
+ const toggleCommandMenu = useRecoilCallback(
+ ({ snapshot, set }) =>
+ async () => {
+ const isCommandMenuOpened = snapshot
+ .getLoadable(isCommandMenuOpenedState)
+ .getValue();
+
+ set(commandMenuSearchState, '');
+
+ if (isCommandMenuOpened) {
+ closeCommandMenu();
+ } else {
+ navigateCommandMenu({
+ page: CommandMenuPages.Root,
+ });
+ }
+ },
+ [closeCommandMenu, navigateCommandMenu],
+ );
+
const goBackFromCommandMenu = useRecoilCallback(
({ snapshot, set }) => {
return () => {
@@ -257,7 +267,6 @@ export const useCommandMenu = () => {
);
return {
- openCommandMenu,
closeCommandMenu,
navigateCommandMenu,
navigateCommandMenuHistory,
diff --git a/packages/twenty-front/src/modules/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton.tsx b/packages/twenty-front/src/modules/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton.tsx
index 01cfbe3c2..b52eba66d 100644
--- a/packages/twenty-front/src/modules/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton.tsx
@@ -7,11 +7,12 @@ import {
} from 'twenty-ui';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
+import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
export const PageHeaderOpenCommandMenuButton = () => {
- const { openCommandMenu } = useCommandMenu();
+ const { navigateCommandMenu } = useCommandMenu();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
@@ -30,7 +31,11 @@ export const PageHeaderOpenCommandMenuButton = () => {
accent="default"
hotkeys={[getOsControlSymbol(), 'K']}
ariaLabel="Open command menu"
- onClick={openCommandMenu}
+ onClick={() => {
+ navigateCommandMenu({
+ page: CommandMenuPages.Root,
+ });
+ }}
/>
) : (
{
dataTestId="more-showpage-button"
accent="default"
variant="secondary"
- onClick={openCommandMenu}
+ onClick={() => {
+ navigateCommandMenu({
+ page: CommandMenuPages.Root,
+ });
+ }}
/>
)}
>