diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 5d4ba1c77..61b8ce087 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -392,6 +392,7 @@ export enum FeatureFlagKey { IsAggregateQueryEnabled = 'IsAggregateQueryEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', + IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled', IsCopilotEnabled = 'IsCopilotEnabled', IsCrmMigrationEnabled = 'IsCrmMigrationEnabled', IsEventObjectEnabled = 'IsEventObjectEnabled', @@ -400,7 +401,6 @@ export enum FeatureFlagKey { IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled', IsJsonFilterEnabled = 'IsJsonFilterEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', - IsPageHeaderV2Enabled = 'IsPageHeaderV2Enabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', IsSsoEnabled = 'IsSSOEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 1bd6c5271..ecc79a5e5 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -324,6 +324,7 @@ export enum FeatureFlagKey { IsAggregateQueryEnabled = 'IsAggregateQueryEnabled', IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', + IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled', IsCopilotEnabled = 'IsCopilotEnabled', IsCrmMigrationEnabled = 'IsCrmMigrationEnabled', IsEventObjectEnabled = 'IsEventObjectEnabled', @@ -332,7 +333,6 @@ export enum FeatureFlagKey { IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled', IsJsonFilterEnabled = 'IsJsonFilterEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', - IsPageHeaderV2Enabled = 'IsPageHeaderV2Enabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', IsSsoEnabled = 'IsSSOEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx index 5ae6cf1d6..15f8d0850 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx @@ -35,8 +35,8 @@ export const RecordActionMenuEntriesSetter = () => { FeatureFlagKey.IsWorkflowEnabled, ); - const isPageHeaderV2Enabled = useIsFeatureEnabled( - FeatureFlagKey.IsPageHeaderV2Enabled, + const isCommandMenuV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsCommandMenuV2Enabled, ); if ( @@ -53,7 +53,7 @@ export const RecordActionMenuEntriesSetter = () => { const actionConfig = getActionConfig( objectMetadataItem, - isPageHeaderV2Enabled, + isCommandMenuV2Enabled, ); const actionsToRegister = isDefined(viewType) diff --git a/packages/twenty-front/src/modules/action-menu/actions/utils/getActionConfig.ts b/packages/twenty-front/src/modules/action-menu/actions/utils/getActionConfig.ts index 9d2cd4b21..c63566eda 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/utils/getActionConfig.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/utils/getActionConfig.ts @@ -8,7 +8,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; export const getActionConfig = ( objectMetadataItem: ObjectMetadataItem, - isPageHeaderV2Enabled: boolean, + isCommandMenuV2Enabled: boolean, ) => { switch (objectMetadataItem.nameSingular) { case CoreObjectNameSingular.Workflow: @@ -18,7 +18,7 @@ export const getActionConfig = ( case CoreObjectNameSingular.WorkflowRun: return WORKFLOW_RUNS_ACTIONS_CONFIG; default: - return isPageHeaderV2Enabled + return isCommandMenuV2Enabled ? DEFAULT_ACTIONS_CONFIG_V2 : DEFAULT_ACTIONS_CONFIG_V1; } diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx index 835322427..2590dadd2 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx @@ -25,8 +25,8 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => { FeatureFlagKey.IsWorkflowEnabled, ); - const isPageHeaderV2Enabled = useIsFeatureEnabled( - FeatureFlagKey.IsPageHeaderV2Enabled, + const isCommandMenuV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsCommandMenuV2Enabled, ); const isMobile = useIsMobile(); @@ -54,7 +54,7 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => { }, }} > - {isPageHeaderV2Enabled ? ( + {isCommandMenuV2Enabled ? ( <>{!isMobile && } ) : ( diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx index a1df8e655..3d032945b 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx @@ -33,8 +33,8 @@ export const RecordShowActionMenu = ({ FeatureFlagKey.IsWorkflowEnabled, ); - const isPageHeaderV2Enabled = useIsFeatureEnabled( - FeatureFlagKey.IsPageHeaderV2Enabled, + const isCommandMenuV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsCommandMenuV2Enabled, ); // TODO: refactor RecordShowPageBaseHeader to use the context store @@ -48,7 +48,7 @@ export const RecordShowActionMenu = ({ onActionExecutedCallback: () => {}, }} > - {isPageHeaderV2Enabled ? ( + {isCommandMenuV2Enabled ? ( ) : ( { if ( isRightDrawerOpen && @@ -34,7 +41,12 @@ export const useOpenActivityRightDrawer = ({ return; } - setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); + if (isCommandMenuV2Enabled) { + setHotkeyScope(AppHotkeyScope.CommandMenuOpen, { goto: false }); + } else { + setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); + } + setViewableRecordId(activityId); setViewableRecordNameSingular(objectNameSingular); openRightDrawer(RightDrawerPages.ViewRecord); diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index 12fa5ab0f..48a98407d 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -17,6 +17,9 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; +import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { FeatureFlagKey } from '~/generated-metadata/graphql'; import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; export const useOpenCreateActivityDrawer = ({ @@ -60,6 +63,10 @@ export const useOpenCreateActivityDrawer = ({ isUpsertingActivityInDBState, ); + const isCommandMenuV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsCommandMenuV2Enabled, + ); + const openCreateActivityDrawer = async ({ targetableObjects, customAssignee, @@ -108,7 +115,12 @@ export const useOpenCreateActivityDrawer = ({ setActivityTargetableEntityArray([]); } - setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); + if (isCommandMenuV2Enabled) { + setHotkeyScope(AppHotkeyScope.CommandMenuOpen, { goto: false }); + } else { + setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); + } + setViewableRecordId(activity.id); setIsUpsertingActivityInDB(false); diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 66b152f46..c9c2f9609 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -1,23 +1,19 @@ import { CommandGroup } from '@/command-menu/components/CommandGroup'; import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; -import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar'; import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys'; +import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick'; import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { Command } from '@/command-menu/types/Command'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import styled from '@emotion/styled'; -import { useRef } from 'react'; -import { useRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; const MOBILE_NAVIGATION_BAR_HEIGHT = 64; @@ -27,21 +23,6 @@ type CommandGroupConfig = { items?: Command[]; }; -const StyledCommandMenu = styled.div` - background: ${({ theme }) => theme.background.secondary}; - border-left: 1px solid ${({ theme }) => theme.border.color.medium}; - box-shadow: ${({ theme }) => theme.boxShadow.strong}; - font-family: ${({ theme }) => theme.font.family}; - height: 100%; - overflow: hidden; - padding: 0; - position: fixed; - right: 0%; - top: 0%; - width: ${() => (useIsMobile() ? '100%' : '500px')}; - z-index: 1000; -`; - const StyledList = styled.div` background: ${({ theme }) => theme.background.secondary}; overscroll-behavior: contain; @@ -75,24 +56,12 @@ const StyledEmpty = styled.div` `; export const CommandMenu = () => { - const { onItemClick, closeCommandMenu } = useCommandMenu(); - const commandMenuRef = useRef(null); + const { onItemClick } = useCommandMenuOnItemClick(); - const [commandMenuSearch, setCommandMenuSearch] = useRecoilState( - commandMenuSearchState, - ); + const commandMenuSearch = useRecoilValue(commandMenuSearchState); const isMobile = useIsMobile(); - useCommandMenuHotKeys(); - - useListenClickOutside({ - refs: [commandMenuRef], - callback: closeCommandMenu, - listenerId: 'COMMAND_MENU_LISTENER_ID', - hotkeyScope: AppHotkeyScope.CommandMenuOpen, - }); - const { isNoResults, isLoading, @@ -184,74 +153,66 @@ export const CommandMenu = () => { - - - - - - { - const command = selectableItems.find( - (item) => item.id === itemId, - ); - if (isDefined(command)) { - const { - to, - onCommandClick, - shouldCloseCommandMenuOnClick, - } = command; + + + + { + const command = selectableItems.find( + (item) => item.id === itemId, + ); - onItemClick({ - shouldCloseCommandMenuOnClick, - onClick: onCommandClick, - to, - }); - } - }} - > - {isNoResults && !isLoading && ( - No results found - )} - {commandGroups.map(({ heading, items }) => - items?.length ? ( - - {items.map((item) => { - return ( - - - - ); - })} - - ) : null, - )} - - - - - + if (isDefined(command)) { + const { to, onCommandClick, shouldCloseCommandMenuOnClick } = + command; + + onItemClick({ + shouldCloseCommandMenuOnClick, + onClick: onCommandClick, + to, + }); + } + }} + > + {isNoResults && !isLoading && ( + No results found + )} + {commandGroups.map(({ heading, items }) => + items?.length ? ( + + {items.map((item) => { + return ( + + + + ); + })} + + ) : null, + )} + + + + ); }; 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 fe3802cd0..3fa14a772 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx @@ -3,35 +3,56 @@ import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record- import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; -import { CommandMenu } from '@/command-menu/components/CommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; -import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; +import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import styled from '@emotion/styled'; +import { useRef } from 'react'; import { useRecoilValue } from 'recoil'; +import { useIsMobile } from 'twenty-ui'; import { FeatureFlagKey } from '~/generated/graphql'; -export const CommandMenuContainer = () => { - const { toggleCommandMenu } = useCommandMenu(); - const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); +const StyledCommandMenu = styled.div` + background: ${({ theme }) => theme.background.secondary}; + border-left: 1px solid ${({ theme }) => theme.border.color.medium}; + box-shadow: ${({ theme }) => theme.boxShadow.strong}; + font-family: ${({ theme }) => theme.font.family}; + height: 100%; + overflow: hidden; + padding: 0; + position: fixed; + right: 0%; + top: 0%; + width: ${() => (useIsMobile() ? '100%' : '500px')}; + z-index: 30; +`; + +export const CommandMenuContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { toggleCommandMenu, closeCommandMenu } = useCommandMenu(); const isWorkflowEnabled = useIsFeatureEnabled( FeatureFlagKey.IsWorkflowEnabled, ); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); - useScopedHotkeys( - 'ctrl+k,meta+k', - () => { - closeKeyboardShortcutMenu(); - toggleCommandMenu(); - }, - AppHotkeyScope.CommandMenu, - [toggleCommandMenu], - ); + const commandMenuRef = useRef(null); + + useCommandMenuHotKeys(); + + useListenClickOutside({ + refs: [commandMenuRef], + callback: closeCommandMenu, + listenerId: 'COMMAND_MENU_LISTENER_ID', + hotkeyScope: AppHotkeyScope.CommandMenuOpen, + }); return ( { {isWorkflowEnabled && } - {isCommandMenuOpened && } + {isCommandMenuOpened && ( + + {children} + + )} diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx index c772da280..3907ccb47 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuItem.tsx @@ -2,10 +2,9 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilValue } from 'recoil'; import { IconArrowUpRight, IconComponent, MenuItemCommand } from 'twenty-ui'; +import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; -import { useCommandMenu } from '../hooks/useCommandMenu'; - export type CommandMenuItemProps = { label: string; to?: string; @@ -27,7 +26,7 @@ export const CommandMenuItem = ({ secondHotKey, shouldCloseCommandMenuOnClick, }: CommandMenuItemProps) => { - const { onItemClick } = useCommandMenu(); + const { onItemClick } = useCommandMenuOnItemClick(); if (isNonEmptyString(to) && !Icon) { Icon = IconArrowUpRight; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPages.ts b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPages.ts new file mode 100644 index 000000000..e783f76de --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPages.ts @@ -0,0 +1,4 @@ +export enum CommandMenuPages { + Root = 'root', + ViewRecord = 'view-record', +} diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx new file mode 100644 index 000000000..9c90f6e47 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRouter.tsx @@ -0,0 +1,23 @@ +import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer'; +import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar'; +import { COMMAND_MENU_PAGES_CONFIG } from '@/command-menu/constants/CommandMenuPagesConfig'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const CommandMenuRouter = () => { + const commandMenuPage = useRecoilValue(commandMenuPageState); + + const commandMenuPageComponent = isDefined(commandMenuPage) ? ( + COMMAND_MENU_PAGES_CONFIG.get(commandMenuPage) + ) : ( + <> + ); + + return ( + + + {commandMenuPageComponent} + + ); +}; 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 4bb2bc904..47cd9459a 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx @@ -2,9 +2,11 @@ import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandM import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui'; const StyledInputContainer = styled.div` @@ -50,15 +52,11 @@ const StyledCloseButtonContainer = styled.div` justify-content: center; `; -type CommandMenuTopBarProps = { - commandMenuSearch: string; - setCommandMenuSearch: (search: string) => void; -}; +export const CommandMenuTopBar = () => { + const [commandMenuSearch, setCommandMenuSearch] = useRecoilState( + commandMenuSearchState, + ); -export const CommandMenuTopBar = ({ - commandMenuSearch, - setCommandMenuSearch, -}: CommandMenuTopBarProps) => { const handleSearchChange = (event: React.ChangeEvent) => { setCommandMenuSearch(event.target.value); }; 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 fb0c8bf13..7a277d6f5 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,6 +16,7 @@ import { import { sleep } from '~/utils/sleep'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; @@ -43,7 +44,7 @@ const ContextStoreDecorator: Decorator = (Story) => { const meta: Meta = { title: 'Modules/CommandMenu/CommandMenu', - component: CommandMenu, + component: CommandMenuRouter, decorators: [ (Story) => { const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx new file mode 100644 index 000000000..ca1a09097 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx @@ -0,0 +1,11 @@ +import { CommandMenu } from '@/command-menu/components/CommandMenu'; +import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages'; +import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord'; + +export const COMMAND_MENU_PAGES_CONFIG = new Map< + CommandMenuPages, + React.ReactNode +>([ + [CommandMenuPages.Root, ], + [CommandMenuPages.ViewRecord, ], +]); 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 2ae691d15..a5c88a6cb 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 @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; +import { act } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { RecoilRoot, useRecoilValue } from 'recoil'; @@ -69,20 +69,4 @@ describe('useCommandMenu', () => { expect(result.current.isCommandMenuOpened).toBe(false); }); - - it('onItemClick', () => { - const { result } = renderHooks(); - const onClickMock = jest.fn(); - - act(() => { - result.current.commandMenu.onItemClick({ - shouldCloseCommandMenuOnClick: true, - onClick: onClickMock, - to: '/test', - }); - }); - - expect(result.current.isCommandMenuOpened).toBe(true); - expect(onClickMock).toHaveBeenCalledTimes(1); - }); }); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuOnItemClick.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuOnItemClick.test.tsx new file mode 100644 index 000000000..a5a82dca6 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenuOnItemClick.test.tsx @@ -0,0 +1,54 @@ +import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { RecoilRoot, useRecoilValue } from 'recoil'; +import { useCommandMenuOnItemClick } from '../useCommandMenuOnItemClick'; + +const Wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + +); + +const renderHooks = () => { + const { result } = renderHook( + () => { + const { onItemClick } = useCommandMenuOnItemClick(); + + const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); + + return { + onItemClick, + isCommandMenuOpened, + }; + }, + { + wrapper: Wrapper, + }, + ); + return { result }; +}; + +describe('useCommandMenuOnItemClick', () => { + it('onItemClick', () => { + const { result } = renderHooks(); + const onClickMock = jest.fn(); + + act(() => { + result.current.onItemClick({ + shouldCloseCommandMenuOnClick: true, + onClick: onClickMock, + to: '/test', + }); + }); + + expect(result.current.isCommandMenuOpened).toBe(true); + expect(onClickMock).toHaveBeenCalledTimes(1); + }); +}); 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 3b32e5a6e..fc23fbf96 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -1,6 +1,3 @@ -import { isNonEmptyString } from '@sniptt/guards'; -import { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; @@ -10,6 +7,8 @@ import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { isDefined } from '~/utils/isDefined'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; @@ -17,10 +16,10 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; export const useCommandMenu = () => { - const navigate = useNavigate(); const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState); const { resetSelectedItem } = useSelectableList('command-menu-list'); const { @@ -212,6 +211,8 @@ export const useCommandMenu = () => { ); if (isCommandMenuOpened) { + set(viewableRecordIdState, null); + set(commandMenuPageState, CommandMenuPages.Root); setIsCommandMenuOpened(false); resetSelectedItem(); goBackToPreviousHotkeyScope(); @@ -238,39 +239,21 @@ export const useCommandMenu = () => { [closeCommandMenu, openCommandMenu], ); - const onItemClick = useCallback( - ({ - shouldCloseCommandMenuOnClick, - onClick, - to, - }: { - shouldCloseCommandMenuOnClick?: boolean; - onClick?: () => void; - to?: string; - }) => { - if ( - isDefined(shouldCloseCommandMenuOnClick) && - shouldCloseCommandMenuOnClick - ) { - toggleCommandMenu(); - } - - if (isDefined(onClick)) { - onClick(); - return; - } - if (isNonEmptyString(to)) { - navigate(to); - return; - } + const openRecordInCommandMenu = useRecoilCallback( + ({ set }) => { + return (recordId: string) => { + openCommandMenu(); + set(commandMenuPageState, CommandMenuPages.ViewRecord); + set(viewableRecordIdState, recordId); + }; }, - [navigate, toggleCommandMenu], + [openCommandMenu], ); return { openCommandMenu, closeCommandMenu, + openRecordInCommandMenu, toggleCommandMenu, - onItemClick, }; }; 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 f905b29ce..d237a4f41 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts @@ -2,6 +2,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; @@ -10,7 +11,7 @@ import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; export const useCommandMenuHotKeys = () => { - const { closeCommandMenu } = useCommandMenu(); + const { closeCommandMenu, toggleCommandMenu } = useCommandMenu(); const commandMenuSearch = useRecoilValue(commandMenuSearchState); @@ -24,6 +25,18 @@ export const useCommandMenuHotKeys = () => { 'command-menu', ); + const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); + + useScopedHotkeys( + 'ctrl+k,meta+k', + () => { + closeKeyboardShortcutMenu(); + toggleCommandMenu(); + }, + AppHotkeyScope.CommandMenu, + [toggleCommandMenu], + ); + useScopedHotkeys( [Key.Escape], () => { diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuOnItemClick.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuOnItemClick.ts new file mode 100644 index 000000000..c656e82eb --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuOnItemClick.ts @@ -0,0 +1,41 @@ +import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { isDefined } from 'twenty-ui'; + +export const useCommandMenuOnItemClick = () => { + const { toggleCommandMenu } = useCommandMenu(); + const navigate = useNavigate(); + + const onItemClick = useCallback( + ({ + shouldCloseCommandMenuOnClick, + onClick, + to, + }: { + shouldCloseCommandMenuOnClick?: boolean; + onClick?: () => void; + to?: string; + }) => { + if ( + isDefined(shouldCloseCommandMenuOnClick) && + shouldCloseCommandMenuOnClick + ) { + toggleCommandMenu(); + } + + if (isDefined(onClick)) { + onClick(); + return; + } + if (isNonEmptyString(to)) { + navigate(to); + return; + } + }, + [navigate, toggleCommandMenu], + ); + + return { onItemClick }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/states/commandMenuPageState.ts b/packages/twenty-front/src/modules/command-menu/states/commandMenuPageState.ts new file mode 100644 index 000000000..d5a41917d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/states/commandMenuPageState.ts @@ -0,0 +1,7 @@ +import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages'; +import { createState } from '@ui/utilities/state/utils/createState'; + +export const commandMenuPageState = createState({ + key: 'command-menu/commandMenuPageState', + defaultValue: CommandMenuPages.Root, +}); diff --git a/packages/twenty-front/src/modules/favorites/components/PageFavoriteButton.tsx b/packages/twenty-front/src/modules/favorites/components/PageFavoriteButton.tsx index 3ab95cd67..100abe308 100644 --- a/packages/twenty-front/src/modules/favorites/components/PageFavoriteButton.tsx +++ b/packages/twenty-front/src/modules/favorites/components/PageFavoriteButton.tsx @@ -13,13 +13,13 @@ export const PageFavoriteButton = ({ }: PageFavoriteButtonProps) => { const title = isFavorite ? 'Remove from favorites' : 'Add to favorites'; - const isPageHeaderV2Enabled = useIsFeatureEnabled( - FeatureFlagKey.IsPageHeaderV2Enabled, + const isCommandMenuV2Enabled = useIsFeatureEnabled( + FeatureFlagKey.IsCommandMenuV2Enabled, ); return ( <> - {isPageHeaderV2Enabled ? ( + {isCommandMenuV2Enabled ? (