Since I introduced AnimatePresence to animate the exit of the command menu, the command menu wasn't working properly. By adding this animation, I had to reset the command menu states at the end of the animation, otherwise, the component inside the command menu would throw errors. The problem was that, by closing and instantly reopening the command menu, the `onExitComplete` wasn't triggered and the states weren't reset before the opening. By introducing a new state `isCommandMenuClosingState`, I can reset those states at the beginning of the opening if the animation didn't have the time to finish.
365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
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 { IconDotsVertical, IconSearch, useIcons } 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 {
|
|
CommandMenuNavigationStackItem,
|
|
commandMenuNavigationStackState,
|
|
} from '@/command-menu/states/commandMenuNavigationStackState';
|
|
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
|
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
|
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
|
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 { 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 { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
|
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
|
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
|
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
|
import { t } from '@lingui/core/macro';
|
|
import { useCallback } from 'react';
|
|
import { capitalize, isDefined } from 'twenty-shared';
|
|
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
|
|
|
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 closeCommandMenu = useRecoilCallback(
|
|
({ set }) =>
|
|
() => {
|
|
set(isCommandMenuOpenedState, false);
|
|
set(isCommandMenuClosingState, true);
|
|
},
|
|
[],
|
|
);
|
|
|
|
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,
|
|
});
|
|
set(isCommandMenuOpenedState, false);
|
|
set(commandMenuSearchState, '');
|
|
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);
|
|
},
|
|
[
|
|
copyContextStoreStates,
|
|
onCommandMenuCloseAnimationComplete,
|
|
setHotkeyScopeAndMemorizePreviousScope,
|
|
],
|
|
);
|
|
|
|
const navigateCommandMenu = useRecoilCallback(
|
|
({ snapshot, set }) => {
|
|
return ({
|
|
page,
|
|
pageTitle,
|
|
pageIcon,
|
|
resetNavigationStack = false,
|
|
}: CommandMenuNavigationStackItem & {
|
|
resetNavigationStack?: boolean;
|
|
}) => {
|
|
openCommandMenu();
|
|
set(commandMenuPageState, page);
|
|
set(commandMenuPageInfoState, {
|
|
title: pageTitle,
|
|
Icon: pageIcon,
|
|
});
|
|
|
|
const isCommandMenuClosing = snapshot
|
|
.getLoadable(isCommandMenuClosingState)
|
|
.getValue();
|
|
|
|
const currentNavigationStack = isCommandMenuClosing
|
|
? []
|
|
: snapshot.getLoadable(commandMenuNavigationStackState).getValue();
|
|
|
|
if (resetNavigationStack) {
|
|
set(commandMenuNavigationStackState, [{ page, pageTitle, pageIcon }]);
|
|
} else {
|
|
set(commandMenuNavigationStackState, [
|
|
...currentNavigationStack,
|
|
{ page, pageTitle, pageIcon },
|
|
]);
|
|
}
|
|
};
|
|
},
|
|
[openCommandMenu],
|
|
);
|
|
|
|
const openRootCommandMenu = useCallback(() => {
|
|
navigateCommandMenu({
|
|
page: CommandMenuPages.Root,
|
|
pageTitle: 'Command Menu',
|
|
pageIcon: IconDotsVertical,
|
|
resetNavigationStack: true,
|
|
});
|
|
}, [navigateCommandMenu]);
|
|
|
|
const toggleCommandMenu = useRecoilCallback(
|
|
({ snapshot, set }) =>
|
|
async () => {
|
|
const isCommandMenuOpened = snapshot
|
|
.getLoadable(isCommandMenuOpenedState)
|
|
.getValue();
|
|
|
|
set(commandMenuSearchState, '');
|
|
|
|
if (isCommandMenuOpened) {
|
|
closeCommandMenu();
|
|
} else {
|
|
openRootCommandMenu();
|
|
}
|
|
},
|
|
[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,
|
|
});
|
|
|
|
set(commandMenuNavigationStackState, newNavigationStack);
|
|
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,
|
|
});
|
|
|
|
set(hasUserSelectedCommandState, false);
|
|
};
|
|
}, []);
|
|
|
|
const openRecordInCommandMenu = useRecoilCallback(
|
|
({ set, snapshot }) => {
|
|
return ({
|
|
recordId,
|
|
objectNameSingular,
|
|
isNewRecord = false,
|
|
}: {
|
|
recordId: string;
|
|
objectNameSingular: string;
|
|
isNewRecord?: boolean;
|
|
}) => {
|
|
set(viewableRecordNameSingularState, objectNameSingular);
|
|
set(viewableRecordIdState, recordId);
|
|
|
|
const objectMetadataItem = snapshot
|
|
.getLoadable(
|
|
objectMetadataItemFamilySelector({
|
|
objectName: objectNameSingular,
|
|
objectNameType: 'singular',
|
|
}),
|
|
)
|
|
.getValue();
|
|
|
|
const Icon = objectMetadataItem?.icon
|
|
? getIcon(objectMetadataItem.icon)
|
|
: getIcon('IconList');
|
|
|
|
const capitalizedObjectNameSingular = capitalize(objectNameSingular);
|
|
|
|
navigateCommandMenu({
|
|
page: CommandMenuPages.ViewRecord,
|
|
pageTitle: isNewRecord
|
|
? t`New ${capitalizedObjectNameSingular}`
|
|
: capitalizedObjectNameSingular,
|
|
pageIcon: Icon,
|
|
// TODO: remove this once we can store the navigation stack page states
|
|
resetNavigationStack: true,
|
|
});
|
|
};
|
|
},
|
|
[getIcon, navigateCommandMenu],
|
|
);
|
|
|
|
const openRecordsSearchPage = () => {
|
|
navigateCommandMenu({
|
|
page: CommandMenuPages.SearchRecords,
|
|
pageTitle: 'Search',
|
|
pageIcon: IconSearch,
|
|
});
|
|
};
|
|
|
|
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,
|
|
});
|
|
|
|
set(hasUserSelectedCommandState, false);
|
|
};
|
|
},
|
|
[copyContextStoreStates],
|
|
);
|
|
|
|
return {
|
|
openRootCommandMenu,
|
|
closeCommandMenu,
|
|
onCommandMenuCloseAnimationComplete,
|
|
navigateCommandMenu,
|
|
navigateCommandMenuHistory,
|
|
goBackFromCommandMenu,
|
|
openRecordsSearchPage,
|
|
openRecordInCommandMenu,
|
|
toggleCommandMenu,
|
|
setGlobalCommandMenuContext,
|
|
};
|
|
};
|