583 refactor useCommandMenu hook (#10984)

Closes https://github.com/twentyhq/core-team-issues/issues/583

- Split hook into smaller hooks
- Create tests
This commit is contained in:
Raphaël Bosi
2025-03-18 15:37:28 +01:00
committed by GitHub
parent 324794707a
commit 2680f1d6be
48 changed files with 2120 additions and 918 deletions

View File

@ -9,6 +9,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { COMMAND_MENU_ANIMATION_VARIANTS } from '@/command-menu/constants/CommandMenuAnimationVariants';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
@ -54,11 +55,10 @@ export const CommandMenuContainer = ({
}: {
children: React.ReactNode;
}) => {
const {
toggleCommandMenu,
closeCommandMenu,
onCommandMenuCloseAnimationComplete,
} = useCommandMenu();
const { toggleCommandMenu, closeCommandMenu } = useCommandMenu();
const { commandMenuCloseAnimationCompleteCleanup } =
useCommandMenuCloseAnimationCompleteCleanup();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
@ -154,7 +154,7 @@ export const CommandMenuContainer = ({
<ActionMenuConfirmationModals />
<AnimatePresence
mode="wait"
onExitComplete={onCommandMenuCloseAnimationComplete}
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
>
{isCommandMenuOpened && (
<StyledCommandMenu

View File

@ -23,7 +23,7 @@ export const CommandMenuContextChipGroupsWithRecordSelection = ({
limit: 3,
});
const { openRootCommandMenu } = useCommandMenu();
const { openCommandMenu } = useCommandMenu();
if (loading) {
return null;
@ -46,7 +46,7 @@ export const CommandMenuContextChipGroupsWithRecordSelection = ({
totalCount,
),
Icons: Avatars,
onClick: contextChips.length > 0 ? openRootCommandMenu : undefined,
onClick: contextChips.length > 0 ? openCommandMenu : undefined,
withIconBackground: false,
}
: undefined;

View File

@ -7,6 +7,7 @@ import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/Command
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 { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
@ -91,7 +92,9 @@ export const CommandMenuTopBar = () => {
const isMobile = useIsMobile();
const { closeCommandMenu, goBackFromCommandMenu } = useCommandMenu();
const { closeCommandMenu } = useCommandMenu();
const { goBackFromCommandMenu } = useCommandMenuHistory();
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState,

View File

@ -8,8 +8,6 @@ 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 { IconList, IconSearch } from 'twenty-ui';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
@ -53,7 +51,7 @@ describe('useCommandMenu', () => {
const { result } = renderHooks();
act(() => {
result.current.commandMenu.openRootCommandMenu();
result.current.commandMenu.openCommandMenu();
});
expect(result.current.isCommandMenuOpened).toBe(true);
@ -82,164 +80,4 @@ describe('useCommandMenu', () => {
expect(result.current.isCommandMenuOpened).toBe(false);
});
it('should navigate to a page', () => {
const { result } = renderHooks();
expect(result.current.commandMenuNavigationStack).toEqual([]);
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
expect(result.current.commandMenuPageInfo).toEqual({
title: undefined,
Icon: undefined,
instanceId: '',
});
act(() => {
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
});
});
expect(result.current.commandMenuNavigationStack).toEqual([
{
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
},
]);
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
expect(result.current.commandMenuPageInfo).toEqual({
title: 'Search',
Icon: IconSearch,
instanceId: '1',
});
act(() => {
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconList,
pageId: '2',
});
});
expect(result.current.commandMenuNavigationStack).toEqual([
{
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
},
{
page: CommandMenuPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconList,
pageId: '2',
},
]);
expect(result.current.commandMenuPage).toBe(CommandMenuPages.ViewRecord);
expect(result.current.commandMenuPageInfo).toEqual({
title: 'Company',
Icon: IconList,
instanceId: '2',
});
});
it('should go back from a page', () => {
const { result } = renderHooks();
act(() => {
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
});
});
act(() => {
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconList,
pageId: '2',
});
});
expect(result.current.commandMenuNavigationStack).toEqual([
{
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
},
{
page: CommandMenuPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconList,
pageId: '2',
},
]);
act(() => {
result.current.commandMenu.goBackFromCommandMenu();
});
expect(result.current.commandMenuNavigationStack).toEqual([
{
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
},
]);
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
expect(result.current.commandMenuPageInfo).toEqual({
title: 'Search',
Icon: IconSearch,
instanceId: '1',
});
act(() => {
result.current.commandMenu.goBackFromCommandMenu();
result.current.commandMenu.onCommandMenuCloseAnimationComplete();
});
expect(result.current.commandMenuNavigationStack).toEqual([]);
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
expect(result.current.commandMenuPageInfo).toEqual({
title: undefined,
instanceId: '',
Icon: undefined,
});
expect(result.current.isCommandMenuOpened).toBe(false);
});
it('should navigate to a page in history', () => {
const { result } = renderHooks();
act(() => {
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
});
});
act(() => {
result.current.commandMenu.navigateCommandMenuHistory(0);
});
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
expect(result.current.commandMenuPageInfo).toEqual({
title: 'Search',
Icon: IconSearch,
instanceId: '1',
});
});
});

View File

@ -0,0 +1,249 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { IconList } from 'twenty-ui';
const mockCloseDropdown = jest.fn();
const mockResetContextStoreStates = jest.fn();
const mockResetSelectedItem = jest.fn();
const mockGoBackToPreviousHotkeyScope = jest.fn();
const mockEmitRightDrawerCloseEvent = jest.fn();
jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({
useDropdownV2: () => ({
closeDropdown: mockCloseDropdown,
}),
}));
jest.mock('@/command-menu/hooks/useResetContextStoreStates', () => ({
useResetContextStoreStates: () => ({
resetContextStoreStates: mockResetContextStoreStates,
}),
}));
jest.mock('@/ui/layout/selectable-list/hooks/useSelectableList', () => ({
useSelectableList: () => ({
resetSelectedItem: mockResetSelectedItem,
}),
}));
jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope', () => ({
usePreviousHotkeyScope: () => ({
goBackToPreviousHotkeyScope: mockGoBackToPreviousHotkeyScope,
}),
}));
jest.mock('@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent', () => ({
emitRightDrawerCloseEvent: () => {
mockEmitRightDrawerCloseEvent();
},
}));
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
<MemoryRouter>{children}</MemoryRouter>
</RecoilRoot>
);
describe('useCommandMenuCloseAnimationCompleteCleanup', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const renderHooks = () => {
const { result } = renderHook(
() => {
const { commandMenuCloseAnimationCompleteCleanup } =
useCommandMenuCloseAnimationCompleteCleanup();
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
const commandMenuNavigationStack = useRecoilValue(
commandMenuNavigationStackState,
);
const commandMenuNavigationRecords = useRecoilValue(
commandMenuNavigationRecordsState,
);
const commandMenuNavigationMorphItemByPage = useRecoilValue(
commandMenuNavigationMorphItemByPageState,
);
const hasUserSelectedCommand = useRecoilValue(
hasUserSelectedCommandState,
);
const isCommandMenuClosing = useRecoilValue(isCommandMenuClosingState);
const viewableRecordId = useRecoilValue(viewableRecordIdState);
// Get setters for state modification in tests
const setCommandMenuPage = useSetRecoilState(commandMenuPageState);
const setCommandMenuPageInfo = useSetRecoilState(
commandMenuPageInfoState,
);
const setIsCommandMenuOpened = useSetRecoilState(
isCommandMenuOpenedState,
);
const setCommandMenuSearch = useSetRecoilState(commandMenuSearchState);
const setCommandMenuNavigationStack = useSetRecoilState(
commandMenuNavigationStackState,
);
const setCommandMenuNavigationRecords = useSetRecoilState(
commandMenuNavigationRecordsState,
);
const setHasUserSelectedCommand = useSetRecoilState(
hasUserSelectedCommandState,
);
const setIsCommandMenuClosing = useSetRecoilState(
isCommandMenuClosingState,
);
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
return {
commandMenuCloseAnimationCompleteCleanup,
commandMenuPage,
commandMenuPageInfo,
isCommandMenuOpened,
commandMenuSearch,
commandMenuNavigationStack,
commandMenuNavigationRecords,
commandMenuNavigationMorphItemByPage,
hasUserSelectedCommand,
isCommandMenuClosing,
viewableRecordId,
setCommandMenuPage,
setCommandMenuPageInfo,
setIsCommandMenuOpened,
setCommandMenuSearch,
setCommandMenuNavigationStack,
setCommandMenuNavigationRecords,
setHasUserSelectedCommand,
setIsCommandMenuClosing,
setViewableRecordId,
};
},
{
wrapper: Wrapper,
},
);
return { result };
};
it('should reset modified states back to default values', () => {
const { result } = renderHooks();
act(() => {
result.current.setCommandMenuPage(CommandMenuPages.ViewRecord);
result.current.setCommandMenuPageInfo({
title: 'Test Record',
Icon: IconList,
instanceId: 'test-id',
});
result.current.setIsCommandMenuOpened(true);
result.current.setCommandMenuSearch('test search');
result.current.setCommandMenuNavigationStack([
{
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconList,
pageId: '1',
},
]);
result.current.setCommandMenuNavigationRecords([
{
objectMetadataItem: { id: '1', nameSingular: 'Record' } as any,
record: { id: '1' } as any,
},
]);
result.current.setHasUserSelectedCommand(true);
result.current.setIsCommandMenuClosing(true);
result.current.setViewableRecordId('record-123');
});
expect(result.current.commandMenuPage).toBe(CommandMenuPages.ViewRecord);
expect(result.current.commandMenuPageInfo).toEqual({
title: 'Test Record',
Icon: IconList,
instanceId: 'test-id',
});
expect(result.current.isCommandMenuOpened).toBe(true);
expect(result.current.commandMenuSearch).toBe('test search');
expect(result.current.commandMenuNavigationStack).toEqual([
{
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconList,
pageId: '1',
},
]);
expect(result.current.commandMenuNavigationRecords).toEqual([
{
objectMetadataItem: { id: '1', nameSingular: 'Record' } as any,
record: { id: '1' } as any,
},
]);
expect(result.current.hasUserSelectedCommand).toBe(true);
expect(result.current.isCommandMenuClosing).toBe(true);
expect(result.current.viewableRecordId).toBe('record-123');
act(() => {
result.current.commandMenuCloseAnimationCompleteCleanup();
});
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
expect(result.current.commandMenuPageInfo).toEqual({
title: undefined,
Icon: undefined,
instanceId: '',
});
expect(result.current.isCommandMenuOpened).toBe(false);
expect(result.current.commandMenuSearch).toBe('');
expect(result.current.commandMenuNavigationStack).toEqual([]);
expect(result.current.commandMenuNavigationRecords).toEqual([]);
expect(result.current.hasUserSelectedCommand).toBe(false);
expect(result.current.isCommandMenuClosing).toBe(false);
expect(result.current.viewableRecordId).toBe(null);
});
it('should call all dependent functions correctly', () => {
const { result } = renderHooks();
act(() => {
result.current.commandMenuCloseAnimationCompleteCleanup();
});
expect(mockCloseDropdown).toHaveBeenCalledTimes(1);
expect(mockResetContextStoreStates).toHaveBeenCalledTimes(2);
expect(mockResetSelectedItem).toHaveBeenCalledTimes(1);
expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalledTimes(1);
expect(mockEmitRightDrawerCloseEvent).toHaveBeenCalledTimes(1);
expect(mockCloseDropdown).toHaveBeenCalledWith(
COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID,
);
expect(mockResetContextStoreStates).toHaveBeenNthCalledWith(
1,
COMMAND_MENU_COMPONENT_INSTANCE_ID,
);
expect(mockResetContextStoreStates).toHaveBeenNthCalledWith(
2,
COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
);
});
});

View File

@ -0,0 +1,147 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
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 { IconList, IconSearch } from 'twenty-ui';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<RecoilRoot>
<MemoryRouter>{children}</MemoryRouter>
</RecoilRoot>
);
const renderHooks = () => {
const { result } = renderHook(
() => {
const commandMenu = useCommandMenu();
const commandMenuHistory = useCommandMenuHistory();
const commandMenuCloseAnimationCompleteCleanup =
useCommandMenuCloseAnimationCompleteCleanup();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
const commandMenuNavigationStack = useRecoilValue(
commandMenuNavigationStackState,
);
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
return {
commandMenu,
commandMenuHistory,
isCommandMenuOpened,
commandMenuNavigationStack,
commandMenuPage,
commandMenuPageInfo,
commandMenuCloseAnimationCompleteCleanup,
};
},
{
wrapper: Wrapper,
},
);
return { result };
};
describe('useCommandMenuHistory', () => {
it('should go back from a page', () => {
const { result } = renderHooks();
act(() => {
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
});
});
act(() => {
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconList,
pageId: '2',
});
});
expect(result.current.commandMenuNavigationStack).toEqual([
{
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
},
{
page: CommandMenuPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconList,
pageId: '2',
},
]);
act(() => {
result.current.commandMenuHistory.goBackFromCommandMenu();
});
expect(result.current.commandMenuNavigationStack).toEqual([
{
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
},
]);
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
expect(result.current.commandMenuPageInfo).toEqual({
title: 'Search',
Icon: IconSearch,
instanceId: '1',
});
act(() => {
result.current.commandMenuHistory.goBackFromCommandMenu();
result.current.commandMenuCloseAnimationCompleteCleanup.commandMenuCloseAnimationCompleteCleanup();
});
expect(result.current.commandMenuNavigationStack).toEqual([]);
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
expect(result.current.commandMenuPageInfo).toEqual({
title: undefined,
instanceId: '',
Icon: undefined,
});
expect(result.current.isCommandMenuOpened).toBe(false);
});
it('should navigate to a page in history', () => {
const { result } = renderHooks();
act(() => {
result.current.commandMenu.navigateCommandMenu({
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: '1',
});
});
act(() => {
result.current.commandMenuHistory.navigateCommandMenuHistory(0);
});
expect(result.current.commandMenuPage).toBe(CommandMenuPages.SearchRecords);
expect(result.current.commandMenuPageInfo).toEqual({
title: 'Search',
Icon: IconSearch,
instanceId: '1',
});
});
});

View File

@ -0,0 +1,101 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { useRecoilValue } from 'recoil';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { Icon123, useIcons } from 'twenty-ui';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('mocked-uuid'),
}));
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreCurrentViewId: 'my-view-id',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreNumberOfSelectedRecords: 0,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
const renderHooks = () => {
const { result } = renderHook(
() => {
const { navigateCommandMenu } = useNavigateCommandMenu();
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuNavigationStack = useRecoilValue(
commandMenuNavigationStackState,
);
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
const { getIcon } = useIcons();
return {
navigateCommandMenu,
commandMenuPage,
commandMenuNavigationStack,
commandMenuPageInfo,
getIcon,
};
},
{
wrapper,
},
);
return { result };
};
describe('useNavigateCommandMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should navigate to the correct page', () => {
const { result } = renderHooks();
act(() => {
result.current.navigateCommandMenu({
page: CommandMenuPages.Root,
pageTitle: 'Root',
pageIcon: Icon123,
pageIconColor: 'red',
pageId: 'mocked-uuid',
resetNavigationStack: false,
});
});
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
expect(result.current.commandMenuNavigationStack).toEqual([
{
page: CommandMenuPages.Root,
pageTitle: 'Root',
pageIcon: Icon123,
pageIconColor: 'red',
pageId: 'mocked-uuid',
},
]);
expect(result.current.commandMenuPageInfo).toEqual({
title: 'Root',
Icon: Icon123,
instanceId: 'mocked-uuid',
});
});
});

View File

@ -0,0 +1,89 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useOpenCalendarEventInCommandMenu } from '@/command-menu/hooks/useOpenCalendarEventInCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { IconCalendarEvent } from 'twenty-ui';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('mocked-uuid'),
}));
const mockNavigateCommandMenu = jest.fn();
jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({
useNavigateCommandMenu: () => ({
navigateCommandMenu: mockNavigateCommandMenu,
}),
}));
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreCurrentViewId: 'my-view-id',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreNumberOfSelectedRecords: 0,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
const renderHooks = () => {
const { result } = renderHook(
() => {
const { openCalendarEventInCommandMenu } =
useOpenCalendarEventInCommandMenu();
const viewableRecordId = useRecoilComponentValueV2(
viewableRecordIdComponentState,
'mocked-uuid',
);
return {
openCalendarEventInCommandMenu,
viewableRecordId,
};
},
{
wrapper,
},
);
return { result };
};
describe('useOpenCalendarEventInCommandMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should set the correct states and navigate to the calendar event page', () => {
const { result } = renderHooks();
const calendarEventId = 'calendar-event-123';
act(() => {
result.current.openCalendarEventInCommandMenu(calendarEventId);
});
expect(result.current.viewableRecordId).toBe(calendarEventId);
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
page: CommandMenuPages.ViewCalendarEvent,
pageTitle: 'Calendar Event',
pageIcon: IconCalendarEvent,
pageId: 'mocked-uuid',
});
});
});

View File

@ -0,0 +1,89 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useOpenEmailThreadInCommandMenu } from '@/command-menu/hooks/useOpenEmailThreadInCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { IconMail } from 'twenty-ui';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('mocked-uuid'),
}));
const mockNavigateCommandMenu = jest.fn();
jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({
useNavigateCommandMenu: () => ({
navigateCommandMenu: mockNavigateCommandMenu,
}),
}));
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreCurrentViewId: 'my-view-id',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreNumberOfSelectedRecords: 0,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
const renderHooks = () => {
const { result } = renderHook(
() => {
const { openEmailThreadInCommandMenu } =
useOpenEmailThreadInCommandMenu();
const viewableRecordId = useRecoilComponentValueV2(
viewableRecordIdComponentState,
'mocked-uuid',
);
return {
openEmailThreadInCommandMenu,
viewableRecordId,
};
},
{
wrapper,
},
);
return { result };
};
describe('useOpenEmailThreadInCommandMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should set the correct states and navigate to the email thread page', () => {
const { result } = renderHooks();
const emailThreadId = 'email-thread-123';
act(() => {
result.current.openEmailThreadInCommandMenu(emailThreadId);
});
expect(result.current.viewableRecordId).toBe(emailThreadId);
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
page: CommandMenuPages.ViewEmailThread,
pageTitle: 'Email Thread',
pageIcon: IconMail,
pageId: 'mocked-uuid',
});
});
});

View File

@ -0,0 +1,178 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { useRecoilValue } from 'recoil';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIcons } from 'twenty-ui';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('mocked-uuid'),
}));
const mockNavigateCommandMenu = jest.fn();
jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({
useNavigateCommandMenu: () => ({
navigateCommandMenu: mockNavigateCommandMenu,
}),
}));
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreCurrentViewId: 'my-view-id',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreNumberOfSelectedRecords: 0,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
const renderHooks = () => {
const { result } = renderHook(
() => {
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuNavigationMorphItemByPage = useRecoilValue(
commandMenuNavigationMorphItemByPageState,
);
const viewableRecordId = useRecoilComponentValueV2(
viewableRecordIdComponentState,
'mocked-uuid',
);
const viewableRecordNameSingular = useRecoilComponentValueV2(
viewableRecordNameSingularComponentState,
'mocked-uuid',
);
const currentObjectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState,
'mocked-uuid',
);
const targetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
'mocked-uuid',
);
const numberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
'mocked-uuid',
);
const currentViewType = useRecoilComponentValueV2(
contextStoreCurrentViewTypeComponentState,
'mocked-uuid',
);
const { getIcon } = useIcons();
return {
openRecordInCommandMenu,
viewableRecordId,
commandMenuPage,
commandMenuNavigationMorphItemByPage,
viewableRecordNameSingular,
currentObjectMetadataItemId,
targetedRecordsRule,
numberOfSelectedRecords,
currentViewType,
getIcon,
};
},
{
wrapper,
},
);
return { result };
};
describe('useOpenRecordInCommandMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should set the correct states and navigate to the record page', () => {
const { result } = renderHooks();
const recordId = 'record-123';
const objectNameSingular = 'person';
act(() => {
result.current.openRecordInCommandMenu({
recordId,
objectNameSingular,
});
});
expect(result.current.viewableRecordId).toBe(recordId);
expect(result.current.viewableRecordNameSingular).toBe(objectNameSingular);
expect(result.current.currentObjectMetadataItemId).toBe(
personMockObjectMetadataItem.id,
);
expect(result.current.targetedRecordsRule).toEqual({
mode: 'selection',
selectedRecordIds: [recordId],
});
expect(result.current.numberOfSelectedRecords).toBe(1);
expect(result.current.currentViewType).toBe(ContextStoreViewType.ShowPage);
expect(result.current.commandMenuNavigationMorphItemByPage.size).toBe(1);
expect(
result.current.commandMenuNavigationMorphItemByPage.get('mocked-uuid'),
).toEqual({
objectMetadataId: personMockObjectMetadataItem.id,
recordId,
});
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
page: CommandMenuPages.ViewRecord,
pageTitle: 'Person',
pageIcon: result.current.getIcon(personMockObjectMetadataItem.icon),
pageIconColor: 'currentColor',
pageId: 'mocked-uuid',
resetNavigationStack: false,
});
});
it('should set the correct page title for a new record', () => {
const { result } = renderHooks();
const recordId = 'new-record-123';
const objectNameSingular = 'person';
act(() => {
result.current.openRecordInCommandMenu({
recordId,
objectNameSingular,
isNewRecord: true,
});
});
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
page: CommandMenuPages.ViewRecord,
pageTitle: 'New Person',
pageIcon: result.current.getIcon(personMockObjectMetadataItem.icon),
pageIconColor: 'currentColor',
pageId: 'mocked-uuid',
resetNavigationStack: false,
});
});
});

View File

@ -0,0 +1,157 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { useRecoilValue } from 'recoil';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people';
const mockCopyContextStoreStates = jest.fn();
jest.mock(
'@/command-menu/hooks/useCopyContextStoreAndActionMenuStates',
() => ({
useCopyContextStoreStates: () => ({
copyContextStoreStates: mockCopyContextStoreStates,
}),
}),
);
const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
const peopleMock = getPeopleRecordConnectionMock();
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
contextStoreCurrentObjectMetadataNameSingular:
personMockObjectMetadataItem.nameSingular,
contextStoreCurrentViewId: 'my-view-id',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [peopleMock[0].id, peopleMock[1].id],
},
contextStoreNumberOfSelectedRecords: 2,
contextStoreCurrentViewType: ContextStoreViewType.Table,
onInitializeRecoilSnapshot: (snapshot) => {
snapshot.set(recordStoreFamilyState(peopleMock[0].id), peopleMock[0]);
snapshot.set(recordStoreFamilyState(peopleMock[1].id), peopleMock[1]);
},
});
describe('useSetGlobalCommandMenuContext', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should reset all command menu context states', () => {
const { result } = renderHook(
() => {
const { setGlobalCommandMenuContext } =
useSetGlobalCommandMenuContext();
const targetedRecordsRule = useRecoilValue(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
);
const numberOfSelectedRecords = useRecoilValue(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
);
const filters = useRecoilValue(
contextStoreFiltersComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
);
const currentViewType = useRecoilValue(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
);
const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState);
const hasUserSelectedCommand = useRecoilValue(
hasUserSelectedCommandState,
);
return {
setGlobalCommandMenuContext,
targetedRecordsRule,
numberOfSelectedRecords,
filters,
currentViewType,
commandMenuPageInfo,
hasUserSelectedCommand,
};
},
{
wrapper,
},
);
expect(result.current.targetedRecordsRule).toEqual({
mode: 'selection',
selectedRecordIds: [peopleMock[0].id, peopleMock[1].id],
});
expect(result.current.numberOfSelectedRecords).toBe(2);
expect(result.current.filters).toEqual([]);
expect(result.current.currentViewType).toBe(ContextStoreViewType.Table);
expect(result.current.commandMenuPageInfo).toEqual({
title: undefined,
Icon: undefined,
instanceId: '',
});
expect(result.current.hasUserSelectedCommand).toBe(false);
act(() => {
result.current.setGlobalCommandMenuContext();
});
expect(result.current.targetedRecordsRule).toEqual({
mode: 'selection',
selectedRecordIds: [],
});
expect(result.current.numberOfSelectedRecords).toBe(0);
expect(result.current.filters).toEqual([]);
expect(result.current.currentViewType).toBe(ContextStoreViewType.Table);
expect(result.current.commandMenuPageInfo).toEqual({
title: undefined,
Icon: undefined,
instanceId: '',
});
expect(result.current.hasUserSelectedCommand).toBe(false);
});
it('should call copyContextStoreStates with correct parameters', () => {
const { result } = renderHook(() => useSetGlobalCommandMenuContext(), {
wrapper,
});
act(() => {
result.current.setGlobalCommandMenuContext();
});
expect(mockCopyContextStoreStates).toHaveBeenCalledWith({
instanceIdToCopyFrom: COMMAND_MENU_COMPONENT_INSTANCE_ID,
instanceIdToCopyTo: COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
});
});
});

View File

@ -0,0 +1,207 @@
import { renderHook } from '@testing-library/react';
import { useRecoilValue } from 'recoil';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { t } from '@lingui/core/macro';
import { act } from 'react';
import { IconBolt, IconSettingsAutomation, useIcons } from 'twenty-ui';
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { useWorkflowCommandMenu } from '../useWorkflowCommandMenu';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('mocked-uuid'),
}));
const mockNavigateCommandMenu = jest.fn();
jest.mock('@/command-menu/hooks/useNavigateCommandMenu', () => ({
useNavigateCommandMenu: () => ({
navigateCommandMenu: mockNavigateCommandMenu,
}),
}));
const workflowMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'workflow',
)!;
jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({
useObjectMetadataItem: jest.fn(() => ({
objectMetadataItem: workflowMockObjectMetadataItem,
})),
}));
const wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
apolloMocks: [],
componentInstanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
contextStoreCurrentObjectMetadataNameSingular:
workflowMockObjectMetadataItem.nameSingular,
contextStoreCurrentViewId: 'my-view-id',
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreNumberOfSelectedRecords: 0,
contextStoreCurrentViewType: ContextStoreViewType.Table,
});
const renderHooks = () => {
const { result } = renderHook(
() => {
const {
openWorkflowTriggerTypeInCommandMenu,
openStepSelectInCommandMenu,
openWorkflowEditStepInCommandMenu,
openWorkflowViewStepInCommandMenu,
} = useWorkflowCommandMenu();
const commandMenuPage = useRecoilValue(commandMenuPageState);
const commandMenuNavigationMorphItemByPage = useRecoilValue(
commandMenuNavigationMorphItemByPageState,
);
const viewableRecordId = useRecoilComponentValueV2(
viewableRecordIdComponentState,
'mocked-uuid',
);
const viewableRecordNameSingular = useRecoilComponentValueV2(
viewableRecordNameSingularComponentState,
'mocked-uuid',
);
const currentObjectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState,
'mocked-uuid',
);
const targetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
'mocked-uuid',
);
const numberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
'mocked-uuid',
);
const currentViewType = useRecoilComponentValueV2(
contextStoreCurrentViewTypeComponentState,
'mocked-uuid',
);
const workflowId = useRecoilComponentValueV2(
workflowIdComponentState,
'mocked-uuid',
);
const { getIcon } = useIcons();
return {
openWorkflowTriggerTypeInCommandMenu,
openStepSelectInCommandMenu,
openWorkflowEditStepInCommandMenu,
openWorkflowViewStepInCommandMenu,
workflowId,
viewableRecordId,
commandMenuPage,
commandMenuNavigationMorphItemByPage,
viewableRecordNameSingular,
currentObjectMetadataItemId,
targetedRecordsRule,
numberOfSelectedRecords,
currentViewType,
getIcon,
};
},
{
wrapper,
},
);
return { result };
};
describe('useWorkflowCommandMenu', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should navigate to the workflow step select trigger type page', () => {
const { result } = renderHooks();
act(() => {
result.current.openWorkflowTriggerTypeInCommandMenu('test-workflow-id');
});
expect(result.current.workflowId).toBe('test-workflow-id');
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
page: CommandMenuPages.WorkflowStepSelectTriggerType,
pageTitle: t`Trigger Type`,
pageIcon: IconBolt,
pageId: 'mocked-uuid',
});
});
it('should navigate to the workflow step select action page', () => {
const { result } = renderHooks();
act(() => {
result.current.openStepSelectInCommandMenu('test-workflow-id');
});
expect(result.current.workflowId).toBe('test-workflow-id');
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
page: CommandMenuPages.WorkflowStepSelectAction,
pageTitle: t`Select Action`,
pageIcon: IconSettingsAutomation,
pageId: 'mocked-uuid',
});
});
it('should navigate to the workflow step edit page', () => {
const { result } = renderHooks();
act(() => {
result.current.openWorkflowEditStepInCommandMenu(
'test-workflow-id',
'Edit Step',
IconSettingsAutomation,
);
});
expect(result.current.workflowId).toBe('test-workflow-id');
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
page: CommandMenuPages.WorkflowStepEdit,
pageTitle: 'Edit Step',
pageIcon: IconSettingsAutomation,
pageId: 'mocked-uuid',
});
});
it('should navigate to the workflow step view page', () => {
const { result } = renderHooks();
act(() => {
result.current.openWorkflowViewStepInCommandMenu(
'test-workflow-id',
'View Step',
IconSettingsAutomation,
);
});
expect(result.current.workflowId).toBe('test-workflow-id');
expect(mockNavigateCommandMenu).toHaveBeenCalledWith({
page: CommandMenuPages.WorkflowStepView,
pageTitle: 'View Step',
pageIcon: IconSettingsAutomation,
pageId: 'mocked-uuid',
});
});
});

View File

@ -1,79 +1,17 @@
import { useRecoilCallback } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import {
IconBolt,
IconCalendarEvent,
IconComponent,
IconDotsVertical,
IconMail,
IconSearch,
IconSettingsAutomation,
useIcons,
} from 'twenty-ui';
import { IconDotsVertical } from 'twenty-ui';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { useCallback } from 'react';
import { capitalize, isDefined } from 'twenty-shared';
import { v4 } from 'uuid';
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
export type CommandMenuNavigationStackItem = {
page: CommandMenuPages;
pageTitle: string;
pageIcon: IconComponent;
pageIconColor?: string;
pageId?: string;
};
export const useCommandMenu = () => {
const { resetSelectedItem } = useSelectableList('command-menu-list');
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const { getIcon } = useIcons();
const { copyContextStoreStates } = useCopyContextStoreStates();
const { resetContextStoreStates } = useResetContextStoreStates();
const { closeDropdown } = useDropdownV2();
const theme = useTheme();
const { navigateCommandMenu } = useNavigateCommandMenu();
const closeCommandMenu = useRecoilCallback(
({ set }) =>
@ -85,141 +23,7 @@ export const useCommandMenu = () => {
[],
);
const onCommandMenuCloseAnimationComplete = useRecoilCallback(
({ set }) =>
() => {
closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
resetContextStoreStates(COMMAND_MENU_COMPONENT_INSTANCE_ID);
resetContextStoreStates(COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID);
set(viewableRecordIdState, null);
set(commandMenuPageState, CommandMenuPages.Root);
set(commandMenuPageInfoState, {
title: undefined,
Icon: undefined,
instanceId: '',
});
set(isCommandMenuOpenedState, false);
set(commandMenuSearchState, '');
set(commandMenuNavigationMorphItemByPageState, new Map());
set(commandMenuNavigationRecordsState, []);
set(commandMenuNavigationStackState, []);
resetSelectedItem();
set(hasUserSelectedCommandState, false);
goBackToPreviousHotkeyScope();
emitRightDrawerCloseEvent();
set(isCommandMenuClosingState, false);
},
[
closeDropdown,
goBackToPreviousHotkeyScope,
resetContextStoreStates,
resetSelectedItem,
],
);
const openCommandMenu = useRecoilCallback(
({ snapshot, set }) =>
() => {
const isCommandMenuOpened = snapshot
.getLoadable(isCommandMenuOpenedState)
.getValue();
const isCommandMenuClosing = snapshot
.getLoadable(isCommandMenuClosingState)
.getValue();
if (isCommandMenuClosing) {
onCommandMenuCloseAnimationComplete();
}
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
if (isCommandMenuOpened) {
return;
}
copyContextStoreStates({
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID,
});
set(isCommandMenuOpenedState, true);
set(hasUserSelectedCommandState, false);
set(isDragSelectionStartEnabledState, false);
},
[
copyContextStoreStates,
onCommandMenuCloseAnimationComplete,
setHotkeyScopeAndMemorizePreviousScope,
],
);
const navigateCommandMenu = useRecoilCallback(
({ snapshot, set }) => {
return ({
page,
pageTitle,
pageIcon,
pageIconColor,
pageId,
resetNavigationStack = false,
}: CommandMenuNavigationStackItem & {
resetNavigationStack?: boolean;
}) => {
if (!pageId) {
pageId = v4();
}
openCommandMenu();
set(commandMenuPageState, page);
set(commandMenuPageInfoState, {
title: pageTitle,
Icon: pageIcon,
instanceId: pageId,
});
const isCommandMenuClosing = snapshot
.getLoadable(isCommandMenuClosingState)
.getValue();
const currentNavigationStack = isCommandMenuClosing
? []
: snapshot.getLoadable(commandMenuNavigationStackState).getValue();
if (resetNavigationStack) {
set(commandMenuNavigationStackState, [
{
page,
pageTitle,
pageIcon,
pageIconColor,
pageId,
},
]);
set(commandMenuNavigationRecordsState, []);
set(commandMenuNavigationMorphItemByPageState, new Map());
} else {
set(commandMenuNavigationStackState, [
...currentNavigationStack,
{
page,
pageTitle,
pageIcon,
pageIconColor,
pageId,
},
]);
}
};
},
[openCommandMenu],
);
const openRootCommandMenu = useCallback(() => {
const openCommandMenu = useCallback(() => {
navigateCommandMenu({
page: CommandMenuPages.Root,
pageTitle: 'Command Menu',
@ -240,503 +44,16 @@ export const useCommandMenu = () => {
if (isCommandMenuOpened) {
closeCommandMenu();
} else {
openRootCommandMenu();
openCommandMenu();
}
},
[closeCommandMenu, openRootCommandMenu],
);
const goBackFromCommandMenu = useRecoilCallback(
({ snapshot, set }) => {
return () => {
const currentNavigationStack = snapshot
.getLoadable(commandMenuNavigationStackState)
.getValue();
const newNavigationStack = currentNavigationStack.slice(0, -1);
const lastNavigationStackItem = newNavigationStack.at(-1);
if (!isDefined(lastNavigationStackItem)) {
closeCommandMenu();
return;
}
set(commandMenuPageState, lastNavigationStackItem.page);
set(commandMenuPageInfoState, {
title: lastNavigationStackItem.pageTitle,
Icon: lastNavigationStackItem.pageIcon,
instanceId: lastNavigationStackItem.pageId,
});
set(commandMenuNavigationStackState, newNavigationStack);
const currentMorphItems = snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue();
if (currentNavigationStack.length > 0) {
const removedItem = currentNavigationStack.at(-1);
if (isDefined(removedItem)) {
const newMorphItems = new Map(currentMorphItems);
newMorphItems.delete(removedItem.pageId);
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
}
}
set(hasUserSelectedCommandState, false);
};
},
[closeCommandMenu],
);
const navigateCommandMenuHistory = useRecoilCallback(({ snapshot, set }) => {
return (pageIndex: number) => {
const currentNavigationStack = snapshot
.getLoadable(commandMenuNavigationStackState)
.getValue();
const newNavigationStack = currentNavigationStack.slice(0, pageIndex + 1);
set(commandMenuNavigationStackState, newNavigationStack);
const newNavigationStackItem = newNavigationStack.at(-1);
if (!isDefined(newNavigationStackItem)) {
throw new Error(
`No command menu navigation stack item found for index ${pageIndex}`,
);
}
set(commandMenuPageState, newNavigationStackItem?.page);
set(commandMenuPageInfoState, {
title: newNavigationStackItem?.pageTitle,
Icon: newNavigationStackItem?.pageIcon,
instanceId: newNavigationStackItem?.pageId,
});
const currentMorphItems = snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue();
const newMorphItems = new Map(
Array.from(currentMorphItems.entries()).filter(([pageId]) =>
newNavigationStack.some((item) => item.pageId === pageId),
),
);
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
set(hasUserSelectedCommandState, false);
};
}, []);
const openRecordInCommandMenu = useRecoilCallback(
({ set, snapshot }) => {
return ({
recordId,
objectNameSingular,
isNewRecord = false,
}: {
recordId: string;
objectNameSingular: string;
isNewRecord?: boolean;
}) => {
const pageComponentInstanceId = v4();
set(
viewableRecordNameSingularComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
objectNameSingular,
);
set(
viewableRecordIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
recordId,
);
set(viewableRecordIdState, recordId);
const objectMetadataItem = snapshot
.getLoadable(
objectMetadataItemFamilySelector({
objectName: objectNameSingular,
objectNameType: 'singular',
}),
)
.getValue();
if (!objectMetadataItem) {
throw new Error(
`No object metadata item found for object name ${objectNameSingular}`,
);
}
set(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
objectMetadataItem.id,
);
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
{
mode: 'selection',
selectedRecordIds: [recordId],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
1,
);
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
ContextStoreViewType.ShowPage,
);
set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
snapshot
.getLoadable(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}),
)
.getValue(),
);
const currentMorphItems = snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue();
const morphItemToAdd = {
objectMetadataId: objectMetadataItem.id,
recordId,
};
const newMorphItems = new Map([
...currentMorphItems,
[pageComponentInstanceId, morphItemToAdd],
]);
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
const Icon = objectMetadataItem?.icon
? getIcon(objectMetadataItem.icon)
: getIcon('IconList');
const IconColor = getIconColorForObjectType({
objectType: objectMetadataItem.nameSingular,
theme,
});
const capitalizedObjectNameSingular = capitalize(objectNameSingular);
navigateCommandMenu({
page: CommandMenuPages.ViewRecord,
pageTitle: isNewRecord
? t`New ${capitalizedObjectNameSingular}`
: capitalizedObjectNameSingular,
pageIcon: Icon,
pageIconColor: IconColor,
pageId: pageComponentInstanceId,
resetNavigationStack: false,
});
};
},
[getIcon, navigateCommandMenu, theme],
);
const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepSelectTriggerType,
pageTitle: t`Trigger Type`,
pageIcon: IconBolt,
pageId,
});
};
},
[navigateCommandMenu],
);
const openWorkflowActionInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepSelectAction,
pageTitle: t`Select Action`,
pageIcon: IconSettingsAutomation,
pageId,
});
};
},
[navigateCommandMenu],
);
const openWorkflowEditStepInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string, title: string, icon: IconComponent) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepEdit,
pageTitle: title,
pageIcon: icon,
pageId,
});
};
},
[navigateCommandMenu],
);
const openWorkflowViewStepInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string, title: string, icon: IconComponent) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepView,
pageTitle: title,
pageIcon: icon,
pageId,
});
};
},
[navigateCommandMenu],
);
const openWorkflowViewRunStepInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string, title: string, icon: IconComponent) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowRunStepView,
pageTitle: title,
pageIcon: icon,
pageId,
});
};
},
[navigateCommandMenu],
);
const openRecordsSearchPage = () => {
navigateCommandMenu({
page: CommandMenuPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconSearch,
pageId: v4(),
});
};
const openCalendarEventInCommandMenu = useRecoilCallback(
({ set }) => {
return (calendarEventId: string) => {
const pageComponentInstanceId = v4();
set(
viewableRecordIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
calendarEventId,
);
// TODO: Uncomment this once we need to calendar event title in the navigation
// const objectMetadataItem = snapshot
// .getLoadable(objectMetadataItemsState)
// .getValue()
// .find(
// ({ nameSingular }) =>
// nameSingular === CoreObjectNameSingular.CalendarEvent,
// );
// set(
// commandMenuNavigationMorphItemsState,
// new Map([
// ...snapshot
// .getLoadable(commandMenuNavigationMorphItemsState)
// .getValue(),
// [
// pageComponentInstanceId,
// {
// objectMetadataId: objectMetadataItem?.id,
// recordId: calendarEventId,
// },
// ],
// ]),
// );
navigateCommandMenu({
page: CommandMenuPages.ViewCalendarEvent,
pageTitle: 'Calendar Event',
pageIcon: IconCalendarEvent,
pageId: pageComponentInstanceId,
});
};
},
[navigateCommandMenu],
);
const openEmailThreadInCommandMenu = useRecoilCallback(
({ set }) => {
return (emailThreadId: string) => {
const pageComponentInstanceId = v4();
set(
viewableRecordIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
emailThreadId,
);
// TODO: Uncomment this once we need to show the thread title in the navigation
// const objectMetadataItem = snapshot
// .getLoadable(objectMetadataItemsState)
// .getValue()
// .find(
// ({ nameSingular }) =>
// nameSingular === CoreObjectNameSingular.MessageThread,
// );
// set(
// commandMenuNavigationMorphItemsState,
// new Map([
// ...snapshot
// .getLoadable(commandMenuNavigationMorphItemsState)
// .getValue(),
// [
// pageComponentInstanceId,
// {
// objectMetadataId: objectMetadataItem?.id,
// recordId: emailThreadId,
// },
// ],
// ]),
// );
navigateCommandMenu({
page: CommandMenuPages.ViewEmailThread,
pageTitle: 'Email Thread',
pageIcon: IconMail,
pageId: pageComponentInstanceId,
});
};
},
[navigateCommandMenu],
);
const setGlobalCommandMenuContext = useRecoilCallback(
({ set }) => {
return () => {
copyContextStoreStates({
instanceIdToCopyFrom: COMMAND_MENU_COMPONENT_INSTANCE_ID,
instanceIdToCopyTo: COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
});
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
{
mode: 'selection',
selectedRecordIds: [],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
0,
);
set(
contextStoreFiltersComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
[],
);
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
ContextStoreViewType.Table,
);
set(commandMenuPageInfoState, {
title: undefined,
Icon: undefined,
instanceId: '',
});
set(hasUserSelectedCommandState, false);
};
},
[copyContextStoreStates],
[closeCommandMenu, openCommandMenu],
);
return {
openRootCommandMenu,
openCommandMenu,
closeCommandMenu,
onCommandMenuCloseAnimationComplete,
navigateCommandMenu,
navigateCommandMenuHistory,
goBackFromCommandMenu,
openRecordsSearchPage,
openRecordInCommandMenu,
toggleCommandMenu,
setGlobalCommandMenuContext,
openCalendarEventInCommandMenu,
openEmailThreadInCommandMenu,
openWorkflowTriggerTypeInCommandMenu,
openWorkflowActionInCommandMenu,
openWorkflowEditStepInCommandMenu,
openWorkflowViewStepInCommandMenu,
openWorkflowViewRunStepInCommandMenu,
};
};

View File

@ -0,0 +1,69 @@
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilCallback } from 'recoil';
export const useCommandMenuCloseAnimationCompleteCleanup = () => {
const { resetSelectedItem } = useSelectableList('command-menu-list');
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
const { resetContextStoreStates } = useResetContextStoreStates();
const { closeDropdown } = useDropdownV2();
const commandMenuCloseAnimationCompleteCleanup = useRecoilCallback(
({ set }) =>
() => {
closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
resetContextStoreStates(COMMAND_MENU_COMPONENT_INSTANCE_ID);
resetContextStoreStates(COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID);
set(viewableRecordIdState, null);
set(commandMenuPageState, CommandMenuPages.Root);
set(commandMenuPageInfoState, {
title: undefined,
Icon: undefined,
instanceId: '',
});
set(isCommandMenuOpenedState, false);
set(commandMenuSearchState, '');
set(commandMenuNavigationMorphItemByPageState, new Map());
set(commandMenuNavigationRecordsState, []);
set(commandMenuNavigationStackState, []);
resetSelectedItem();
set(hasUserSelectedCommandState, false);
goBackToPreviousHotkeyScope();
emitRightDrawerCloseEvent();
set(isCommandMenuClosingState, false);
},
[
closeDropdown,
goBackToPreviousHotkeyScope,
resetContextStoreStates,
resetSelectedItem,
],
);
return {
commandMenuCloseAnimationCompleteCleanup,
};
};

View File

@ -1,5 +1,5 @@
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
@ -24,7 +24,7 @@ export const useCommandMenuContextChips = () => {
commandMenuNavigationStackState,
);
const { navigateCommandMenuHistory } = useCommandMenu();
const { navigateCommandMenuHistory } = useCommandMenuHistory();
const theme = useTheme();

View File

@ -0,0 +1,103 @@
import { useRecoilCallback } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { isDefined } from 'twenty-shared';
export const useCommandMenuHistory = () => {
const { closeCommandMenu } = useCommandMenu();
const goBackFromCommandMenu = useRecoilCallback(
({ snapshot, set }) => {
return () => {
const currentNavigationStack = snapshot
.getLoadable(commandMenuNavigationStackState)
.getValue();
const newNavigationStack = currentNavigationStack.slice(0, -1);
const lastNavigationStackItem = newNavigationStack.at(-1);
if (!isDefined(lastNavigationStackItem)) {
closeCommandMenu();
return;
}
set(commandMenuPageState, lastNavigationStackItem.page);
set(commandMenuPageInfoState, {
title: lastNavigationStackItem.pageTitle,
Icon: lastNavigationStackItem.pageIcon,
instanceId: lastNavigationStackItem.pageId,
});
set(commandMenuNavigationStackState, newNavigationStack);
const currentMorphItems = snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue();
if (currentNavigationStack.length > 0) {
const removedItem = currentNavigationStack.at(-1);
if (isDefined(removedItem)) {
const newMorphItems = new Map(currentMorphItems);
newMorphItems.delete(removedItem.pageId);
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
}
}
set(hasUserSelectedCommandState, false);
};
},
[closeCommandMenu],
);
const navigateCommandMenuHistory = useRecoilCallback(({ snapshot, set }) => {
return (pageIndex: number) => {
const currentNavigationStack = snapshot
.getLoadable(commandMenuNavigationStackState)
.getValue();
const newNavigationStack = currentNavigationStack.slice(0, pageIndex + 1);
set(commandMenuNavigationStackState, newNavigationStack);
const newNavigationStackItem = newNavigationStack.at(-1);
if (!isDefined(newNavigationStackItem)) {
throw new Error(
`No command menu navigation stack item found for index ${pageIndex}`,
);
}
set(commandMenuPageState, newNavigationStackItem.page);
set(commandMenuPageInfoState, {
title: newNavigationStackItem.pageTitle,
Icon: newNavigationStackItem.pageIcon,
instanceId: newNavigationStackItem.pageId,
});
const currentMorphItems = snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue();
const newMorphItems = new Map(
Array.from(currentMorphItems.entries()).filter(([pageId]) =>
newNavigationStack.some((item) => item.pageId === pageId),
),
);
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
set(hasUserSelectedCommandState, false);
};
}, []);
return {
goBackFromCommandMenu,
navigateCommandMenuHistory,
};
};

View File

@ -1,5 +1,8 @@
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
@ -13,12 +16,13 @@ import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
export const useCommandMenuHotKeys = () => {
const {
openRecordsSearchPage,
toggleCommandMenu,
goBackFromCommandMenu,
setGlobalCommandMenuContext,
} = useCommandMenu();
const { toggleCommandMenu } = useCommandMenu();
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
const { goBackFromCommandMenu } = useCommandMenuHistory();
const { setGlobalCommandMenuContext } = useSetGlobalCommandMenuContext();
const commandMenuSearch = useRecoilValue(commandMenuSearchState);

View File

@ -0,0 +1,137 @@
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { commandMenuNavigationRecordsState } from '@/command-menu/states/commandMenuNavigationRecordsState';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useRecoilCallback } from 'recoil';
import { IconComponent } from 'twenty-ui';
import { v4 } from 'uuid';
export type CommandMenuNavigationStackItem = {
page: CommandMenuPages;
pageTitle: string;
pageIcon: IconComponent;
pageIconColor?: string;
pageId?: string;
};
export const useNavigateCommandMenu = () => {
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
const { copyContextStoreStates } = useCopyContextStoreStates();
const { commandMenuCloseAnimationCompleteCleanup } =
useCommandMenuCloseAnimationCompleteCleanup();
const openCommandMenu = useRecoilCallback(
({ snapshot, set }) =>
() => {
const isCommandMenuOpened = snapshot
.getLoadable(isCommandMenuOpenedState)
.getValue();
const isCommandMenuClosing = snapshot
.getLoadable(isCommandMenuClosingState)
.getValue();
if (isCommandMenuClosing) {
commandMenuCloseAnimationCompleteCleanup();
}
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
if (isCommandMenuOpened) {
return;
}
copyContextStoreStates({
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID,
});
set(isCommandMenuOpenedState, true);
set(hasUserSelectedCommandState, false);
set(isDragSelectionStartEnabledState, false);
},
[
copyContextStoreStates,
commandMenuCloseAnimationCompleteCleanup,
setHotkeyScopeAndMemorizePreviousScope,
],
);
const navigateCommandMenu = useRecoilCallback(
({ snapshot, set }) => {
return ({
page,
pageTitle,
pageIcon,
pageIconColor,
pageId,
resetNavigationStack = false,
}: CommandMenuNavigationStackItem & {
resetNavigationStack?: boolean;
}) => {
const computedPageId = pageId || v4();
openCommandMenu();
set(commandMenuPageState, page);
set(commandMenuPageInfoState, {
title: pageTitle,
Icon: pageIcon,
instanceId: computedPageId,
});
const isCommandMenuClosing = snapshot
.getLoadable(isCommandMenuClosingState)
.getValue();
const currentNavigationStack = isCommandMenuClosing
? []
: snapshot.getLoadable(commandMenuNavigationStackState).getValue();
if (resetNavigationStack) {
set(commandMenuNavigationStackState, [
{
page,
pageTitle,
pageIcon,
pageIconColor,
pageId: computedPageId,
},
]);
set(commandMenuNavigationRecordsState, []);
set(commandMenuNavigationMorphItemByPageState, new Map());
} else {
set(commandMenuNavigationStackState, [
...currentNavigationStack,
{
page,
pageTitle,
pageIcon,
pageIconColor,
pageId: computedPageId,
},
]);
}
};
},
[openCommandMenu],
);
return {
navigateCommandMenu,
};
};

View File

@ -0,0 +1,63 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
import { IconCalendarEvent } from 'twenty-ui';
import { v4 } from 'uuid';
export const useOpenCalendarEventInCommandMenu = () => {
const { navigateCommandMenu } = useNavigateCommandMenu();
const openCalendarEventInCommandMenu = useRecoilCallback(
({ set }) => {
return (calendarEventId: string) => {
const pageComponentInstanceId = v4();
set(
viewableRecordIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
calendarEventId,
);
// TODO: Uncomment this once we need to calendar event title in the navigation
// const objectMetadataItem = snapshot
// .getLoadable(objectMetadataItemsState)
// .getValue()
// .find(
// ({ nameSingular }) =>
// nameSingular === CoreObjectNameSingular.CalendarEvent,
// );
// set(
// commandMenuNavigationMorphItemsState,
// new Map([
// ...snapshot
// .getLoadable(commandMenuNavigationMorphItemsState)
// .getValue(),
// [
// pageComponentInstanceId,
// {
// objectMetadataId: objectMetadataItem?.id,
// recordId: calendarEventId,
// },
// ],
// ]),
// );
navigateCommandMenu({
page: CommandMenuPages.ViewCalendarEvent,
pageTitle: t`Calendar Event`,
pageIcon: IconCalendarEvent,
pageId: pageComponentInstanceId,
});
};
},
[navigateCommandMenu],
);
return {
openCalendarEventInCommandMenu,
};
};

View File

@ -0,0 +1,62 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useRecoilCallback } from 'recoil';
import { IconMail } from 'twenty-ui';
import { v4 } from 'uuid';
export const useOpenEmailThreadInCommandMenu = () => {
const { navigateCommandMenu } = useNavigateCommandMenu();
const openEmailThreadInCommandMenu = useRecoilCallback(
({ set }) => {
return (emailThreadId: string) => {
const pageComponentInstanceId = v4();
set(
viewableRecordIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
emailThreadId,
);
// TODO: Uncomment this once we need to show the thread title in the navigation
// const objectMetadataItem = snapshot
// .getLoadable(objectMetadataItemsState)
// .getValue()
// .find(
// ({ nameSingular }) =>
// nameSingular === CoreObjectNameSingular.MessageThread,
// );
// set(
// commandMenuNavigationMorphItemsState,
// new Map([
// ...snapshot
// .getLoadable(commandMenuNavigationMorphItemsState)
// .getValue(),
// [
// pageComponentInstanceId,
// {
// objectMetadataId: objectMetadataItem?.id,
// recordId: emailThreadId,
// },
// ],
// ]),
// );
navigateCommandMenu({
page: CommandMenuPages.ViewEmailThread,
pageTitle: 'Email Thread',
pageIcon: IconMail,
pageId: pageComponentInstanceId,
});
};
},
[navigateCommandMenu],
);
return {
openEmailThreadInCommandMenu,
};
};

View File

@ -0,0 +1,158 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
import { commandMenuNavigationMorphItemByPageState } from '@/command-menu/states/commandMenuNavigationMorphItemsState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorForObjectType';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
import { capitalize } from 'twenty-shared';
import { useIcons } from 'twenty-ui';
import { v4 } from 'uuid';
export const useOpenRecordInCommandMenu = () => {
const { navigateCommandMenu } = useCommandMenu();
const theme = useTheme();
const { getIcon } = useIcons();
const openRecordInCommandMenu = useRecoilCallback(
({ set, snapshot }) => {
return ({
recordId,
objectNameSingular,
isNewRecord = false,
}: {
recordId: string;
objectNameSingular: string;
isNewRecord?: boolean;
}) => {
const pageComponentInstanceId = v4();
set(
viewableRecordNameSingularComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
objectNameSingular,
);
set(
viewableRecordIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
recordId,
);
set(viewableRecordIdState, recordId);
const objectMetadataItem = snapshot
.getLoadable(
objectMetadataItemFamilySelector({
objectName: objectNameSingular,
objectNameType: 'singular',
}),
)
.getValue();
if (!objectMetadataItem) {
throw new Error(
`No object metadata item found for object name ${objectNameSingular}`,
);
}
set(
contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
objectMetadataItem.id,
);
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
{
mode: 'selection',
selectedRecordIds: [recordId],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
1,
);
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
ContextStoreViewType.ShowPage,
);
set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: pageComponentInstanceId,
}),
snapshot
.getLoadable(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}),
)
.getValue(),
);
const currentMorphItems = snapshot
.getLoadable(commandMenuNavigationMorphItemByPageState)
.getValue();
const morphItemToAdd = {
objectMetadataId: objectMetadataItem.id,
recordId,
};
const newMorphItems = new Map(currentMorphItems);
newMorphItems.set(pageComponentInstanceId, morphItemToAdd);
set(commandMenuNavigationMorphItemByPageState, newMorphItems);
const Icon = objectMetadataItem?.icon
? getIcon(objectMetadataItem.icon)
: getIcon('IconList');
const IconColor = getIconColorForObjectType({
objectType: objectMetadataItem.nameSingular,
theme,
});
const capitalizedObjectNameSingular = capitalize(objectNameSingular);
navigateCommandMenu({
page: CommandMenuPages.ViewRecord,
pageTitle: isNewRecord
? t`New ${capitalizedObjectNameSingular}`
: capitalizedObjectNameSingular,
pageIcon: Icon,
pageIconColor: IconColor,
pageId: pageComponentInstanceId,
resetNavigationStack: false,
});
};
},
[getIcon, navigateCommandMenu, theme],
);
return {
openRecordInCommandMenu,
};
};

View File

@ -0,0 +1,22 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
import { IconSearch } from 'twenty-ui';
import { v4 } from 'uuid';
export const useOpenRecordsSearchPageInCommandMenu = () => {
const { navigateCommandMenu } = useCommandMenu();
const openRecordsSearchPage = () => {
navigateCommandMenu({
page: CommandMenuPages.SearchRecords,
pageTitle: t`Search`,
pageIcon: IconSearch,
pageId: v4(),
});
};
return {
openRecordsSearchPage,
};
};

View File

@ -1,4 +1,4 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { t } from '@lingui/core/macro';
@ -24,7 +24,7 @@ export const useSearchRecords = () => {
},
});
const { openRecordInCommandMenu } = useCommandMenu();
const { openRecordInCommandMenu } = useOpenRecordInCommandMenu();
const commands = useMemo(() => {
return (globalSearchData?.globalSearch ?? []).map((searchRecord) => {

View File

@ -0,0 +1,70 @@
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRecoilCallback } from 'recoil';
export const useSetGlobalCommandMenuContext = () => {
const { copyContextStoreStates } = useCopyContextStoreStates();
const setGlobalCommandMenuContext = useRecoilCallback(
({ set }) => {
return () => {
copyContextStoreStates({
instanceIdToCopyFrom: COMMAND_MENU_COMPONENT_INSTANCE_ID,
instanceIdToCopyTo: COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
});
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
{
mode: 'selection',
selectedRecordIds: [],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
0,
);
set(
contextStoreFiltersComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
[],
);
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
}),
ContextStoreViewType.Table,
);
set(commandMenuPageInfoState, {
title: undefined,
Icon: undefined,
instanceId: '',
});
set(hasUserSelectedCommandState, false);
};
},
[copyContextStoreStates],
);
return {
setGlobalCommandMenuContext,
};
};

View File

@ -0,0 +1,124 @@
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { workflowIdComponentState } from '@/command-menu/pages/workflow/states/workflowIdComponentState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
import { IconBolt, IconComponent, IconSettingsAutomation } from 'twenty-ui';
import { v4 } from 'uuid';
export const useWorkflowCommandMenu = () => {
const { navigateCommandMenu } = useNavigateCommandMenu();
const openWorkflowTriggerTypeInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepSelectTriggerType,
pageTitle: t`Trigger Type`,
pageIcon: IconBolt,
pageId,
});
};
},
[navigateCommandMenu],
);
const openStepSelectInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepSelectAction,
pageTitle: t`Select Action`,
pageIcon: IconSettingsAutomation,
pageId,
});
};
},
[navigateCommandMenu],
);
const openWorkflowEditStepInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string, title: string, icon: IconComponent) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepEdit,
pageTitle: title,
pageIcon: icon,
pageId,
});
};
},
[navigateCommandMenu],
);
const openWorkflowViewStepInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string, title: string, icon: IconComponent) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowStepView,
pageTitle: title,
pageIcon: icon,
pageId,
});
};
},
[navigateCommandMenu],
);
const openWorkflowRunViewStepInCommandMenu = useRecoilCallback(
({ set }) => {
return (workflowId: string, title: string, icon: IconComponent) => {
const pageId = v4();
set(
workflowIdComponentState.atomFamily({ instanceId: pageId }),
workflowId,
);
navigateCommandMenu({
page: CommandMenuPages.WorkflowRunStepView,
pageTitle: title,
pageIcon: icon,
pageId,
});
};
},
[navigateCommandMenu],
);
return {
openWorkflowTriggerTypeInCommandMenu,
openStepSelectInCommandMenu,
openWorkflowEditStepInCommandMenu,
openWorkflowViewStepInCommandMenu,
openWorkflowRunViewStepInCommandMenu,
};
};

View File

@ -1,4 +1,4 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useWorkflowCommandMenu } from '@/command-menu/hooks/useWorkflowCommandMenu';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import {
WorkflowTriggerType,
@ -26,7 +26,7 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
const { openWorkflowEditStepInCommandMenu } = useCommandMenu();
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
const handleTriggerTypeClick = ({
type,