Refactored the `CommandMenu` component to make it more readable and easier to refactor. The file was way too big so I introduced a few hooks and eliminated code duplication. Introduced: - `useMatchCommands` hook to match commands with the search - `useCommandMenuCommands` which returns all command menu commands - `useMatchingCommandMenuCommands` to return the commands matched with the search - `CommandMenuContainer` to simplify the `DefaultLayout` - Unmounted the `CommandMenu` when it wasn't opened to improve performances I also introduced a new behavior: Automatically select the first item when opening the command menu: https://github.com/user-attachments/assets/4b683d49-570e-47c9-8939-99f42ed8691c
257 lines
8.3 KiB
TypeScript
257 lines
8.3 KiB
TypeScript
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
|
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
|
|
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
|
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
|
|
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
|
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
|
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
|
|
import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands';
|
|
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
|
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
|
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
|
import styled from '@emotion/styled';
|
|
import { useRef } from 'react';
|
|
import { useRecoilState } from 'recoil';
|
|
import { isDefined } from 'twenty-ui';
|
|
|
|
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
|
|
|
|
type CommandGroupConfig = {
|
|
heading: string;
|
|
items?: any[];
|
|
};
|
|
|
|
const StyledCommandMenu = styled.div`
|
|
background: ${({ theme }) => theme.background.secondary};
|
|
border-left: 1px solid ${({ theme }) => theme.border.color.medium};
|
|
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
|
font-family: ${({ theme }) => theme.font.family};
|
|
height: 100%;
|
|
overflow: hidden;
|
|
padding: 0;
|
|
position: fixed;
|
|
right: 0%;
|
|
top: 0%;
|
|
width: ${() => (useIsMobile() ? '100%' : '500px')};
|
|
z-index: 1000;
|
|
`;
|
|
|
|
const StyledList = styled.div`
|
|
background: ${({ theme }) => theme.background.secondary};
|
|
overscroll-behavior: contain;
|
|
transition: 100ms ease;
|
|
transition-property: height;
|
|
`;
|
|
|
|
const StyledInnerList = styled.div<{ isMobile: boolean }>`
|
|
max-height: ${({ isMobile }) =>
|
|
isMobile
|
|
? `calc(100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px - ${
|
|
COMMAND_MENU_SEARCH_BAR_PADDING * 2
|
|
}px - ${MOBILE_NAVIGATION_BAR_HEIGHT}px)`
|
|
: `calc(100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px - ${
|
|
COMMAND_MENU_SEARCH_BAR_PADDING * 2
|
|
}px)`};
|
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
|
padding-top: ${({ theme }) => theme.spacing(1)};
|
|
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
|
`;
|
|
|
|
const StyledEmpty = styled.div`
|
|
align-items: center;
|
|
color: ${({ theme }) => theme.font.color.light};
|
|
display: flex;
|
|
font-size: ${({ theme }) => theme.font.size.md};
|
|
height: 64px;
|
|
justify-content: center;
|
|
white-space: pre-wrap;
|
|
`;
|
|
|
|
export const CommandMenu = () => {
|
|
const { onItemClick, closeCommandMenu } = useCommandMenu();
|
|
const commandMenuRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [commandMenuSearch, setCommandMenuSearch] = useRecoilState(
|
|
commandMenuSearchState,
|
|
);
|
|
|
|
const isMobile = useIsMobile();
|
|
|
|
useCommandMenuHotKeys();
|
|
|
|
useListenClickOutside({
|
|
refs: [commandMenuRef],
|
|
callback: closeCommandMenu,
|
|
listenerId: 'COMMAND_MENU_LISTENER_ID',
|
|
hotkeyScope: AppHotkeyScope.CommandMenuOpen,
|
|
});
|
|
|
|
const {
|
|
isNoResults,
|
|
isLoading,
|
|
copilotCommands,
|
|
matchingStandardActionRecordSelectionCommands,
|
|
matchingWorkflowRunRecordSelectionCommands,
|
|
matchingStandardActionGlobalCommands,
|
|
matchingWorkflowRunGlobalCommands,
|
|
matchingNavigateCommand,
|
|
peopleCommands,
|
|
companyCommands,
|
|
opportunityCommands,
|
|
noteCommands,
|
|
tasksCommands,
|
|
customObjectCommands,
|
|
} = useMatchingCommandMenuCommands({
|
|
commandMenuSearch,
|
|
});
|
|
|
|
const selectableItems = copilotCommands
|
|
.concat(matchingStandardActionRecordSelectionCommands)
|
|
.concat(matchingWorkflowRunRecordSelectionCommands)
|
|
.concat(matchingStandardActionGlobalCommands)
|
|
.concat(matchingWorkflowRunGlobalCommands)
|
|
.concat(matchingNavigateCommand)
|
|
.concat(peopleCommands)
|
|
.concat(companyCommands)
|
|
.concat(opportunityCommands)
|
|
.concat(noteCommands)
|
|
.concat(tasksCommands)
|
|
.concat(customObjectCommands)
|
|
.filter(isDefined);
|
|
|
|
const selectableItemIds = selectableItems.map((item) => item.id);
|
|
|
|
const commandGroups: CommandGroupConfig[] = [
|
|
{
|
|
heading: 'Copilot',
|
|
items: copilotCommands,
|
|
},
|
|
{
|
|
heading: 'Record Selection',
|
|
items: matchingStandardActionRecordSelectionCommands,
|
|
},
|
|
{
|
|
heading: 'Workflow Record Selection',
|
|
items: matchingWorkflowRunRecordSelectionCommands,
|
|
},
|
|
{
|
|
heading: 'View',
|
|
items: matchingStandardActionGlobalCommands,
|
|
},
|
|
{
|
|
heading: 'Workflows',
|
|
items: matchingWorkflowRunGlobalCommands,
|
|
},
|
|
{
|
|
heading: 'Navigate',
|
|
items: matchingNavigateCommand,
|
|
},
|
|
{
|
|
heading: 'People',
|
|
items: peopleCommands,
|
|
},
|
|
{
|
|
heading: 'Companies',
|
|
items: companyCommands,
|
|
},
|
|
{
|
|
heading: 'Opportunities',
|
|
items: opportunityCommands,
|
|
},
|
|
{
|
|
heading: 'Notes',
|
|
items: noteCommands,
|
|
},
|
|
{
|
|
heading: 'Tasks',
|
|
items: tasksCommands,
|
|
},
|
|
{
|
|
heading: 'Custom Objects',
|
|
items: customObjectCommands,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<CommandMenuDefaultSelectionEffect
|
|
selectableItemIds={selectableItemIds}
|
|
/>
|
|
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
|
|
<CommandMenuTopBar
|
|
commandMenuSearch={commandMenuSearch}
|
|
setCommandMenuSearch={setCommandMenuSearch}
|
|
/>
|
|
<StyledList>
|
|
<ScrollWrapper
|
|
contextProviderName="commandMenu"
|
|
componentInstanceId={`scroll-wrapper-command-menu`}
|
|
>
|
|
<StyledInnerList isMobile={isMobile}>
|
|
<SelectableList
|
|
selectableListId="command-menu-list"
|
|
selectableItemIdArray={selectableItemIds}
|
|
hotkeyScope={AppHotkeyScope.CommandMenu}
|
|
onEnter={(itemId) => {
|
|
const command = selectableItems.find(
|
|
(item) => item.id === itemId,
|
|
);
|
|
|
|
if (isDefined(command)) {
|
|
const {
|
|
to,
|
|
onCommandClick,
|
|
shouldCloseCommandMenuOnClick,
|
|
} = command;
|
|
|
|
onItemClick({
|
|
shouldCloseCommandMenuOnClick,
|
|
onClick: onCommandClick,
|
|
to,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
{isNoResults && !isLoading && (
|
|
<StyledEmpty>No results found</StyledEmpty>
|
|
)}
|
|
{commandGroups.map(({ heading, items }) =>
|
|
items?.length ? (
|
|
<CommandGroup heading={heading} key={heading}>
|
|
{items.map((item) => {
|
|
return (
|
|
<SelectableItem itemId={item.id} key={item.id}>
|
|
<CommandMenuItem
|
|
key={item.id}
|
|
id={item.id}
|
|
Icon={item.Icon}
|
|
label={item.label}
|
|
to={item.to}
|
|
onClick={item.onClick}
|
|
firstHotKey={item.firstHotKey}
|
|
secondHotKey={item.secondHotKey}
|
|
shouldCloseCommandMenuOnClick={
|
|
item.shouldCloseCommandMenuOnClick
|
|
}
|
|
/>
|
|
</SelectableItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
) : null,
|
|
)}
|
|
</SelectableList>
|
|
</StyledInnerList>
|
|
</ScrollWrapper>
|
|
</StyledList>
|
|
</StyledCommandMenu>
|
|
</>
|
|
);
|
|
};
|