7339 implement contextual actions inside the commandmenu (#8000)

Closes #7339


https://github.com/user-attachments/assets/b623caa4-c1b3-448e-8880-4a8301802ba8
This commit is contained in:
Raphaël Bosi
2024-10-29 15:10:45 +01:00
committed by GitHub
parent 8bb07c4a4f
commit fe2c8bb43b
30 changed files with 399 additions and 237 deletions

View File

@ -10,6 +10,7 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { Command, CommandType } from '@/command-menu/types/Command';
import { Company } from '@/companies/types/Company';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
@ -287,6 +288,14 @@ export const CommandMenu = () => {
: true) && cmd.type === CommandType.Create,
);
const matchingActionCommands = commandMenuCommands.filter(
(cmd) =>
(deferredCommandMenuSearch.length > 0
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
checkInLabels(cmd, deferredCommandMenuSearch)
: true) && cmd.type === CommandType.Action,
);
useListenClickOutside({
refs: [commandMenuRef],
callback: closeCommandMenu,
@ -312,6 +321,7 @@ export const CommandMenu = () => {
const selectableItemIds = copilotCommands
.map((cmd) => cmd.id)
.concat(matchingActionCommands.map((cmd) => cmd.id))
.concat(matchingCreateCommand.map((cmd) => cmd.id))
.concat(matchingNavigateCommand.map((cmd) => cmd.id))
.concat(people?.map((person) => person.id))
@ -320,22 +330,28 @@ export const CommandMenu = () => {
.concat(notes?.map((note) => note.id));
const isNoResults =
!matchingActionCommands.length &&
!matchingCreateCommand.length &&
!matchingNavigateCommand.length &&
!people?.length &&
!companies?.length &&
!notes?.length &&
!opportunities?.length;
const isLoading =
isPeopleLoading ||
isNotesLoading ||
isOpportunitiesLoading ||
isCompaniesLoading;
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
return (
<>
{isCommandMenuOpened && (
<StyledCommandMenu ref={commandMenuRef}>
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
<StyledInputContainer>
<StyledInput
autoFocus
@ -393,6 +409,23 @@ export const CommandMenu = () => {
</SelectableItem>
</CommandGroup>
)}
{mainContextStoreComponentInstanceId && (
<CommandGroup heading="Actions">
{matchingActionCommands?.map((actionCommand) => (
<SelectableItem
itemId={actionCommand.id}
key={actionCommand.id}
>
<CommandMenuItem
id={actionCommand.id}
label={actionCommand.label}
Icon={actionCommand.Icon}
onClick={actionCommand.onCommandClick}
/>
</SelectableItem>
))}
</CommandGroup>
)}
<CommandGroup heading="Create">
{matchingCreateCommand.map((cmd) => (
<SelectableItem itemId={cmd.id} key={cmd.id}>

View File

@ -1,7 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
@ -9,7 +9,9 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isDefined } from '~/utils/isDefined';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons';
import { sortByProperty } from '~/utils/array/sortByProperty';
@ -27,10 +29,43 @@ export const useCommandMenu = () => {
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const openCommandMenu = useCallback(() => {
setIsCommandMenuOpened(true);
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
}, [setHotkeyScopeAndMemorizePreviousScope, setIsCommandMenuOpened]);
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
const openCommandMenu = useRecoilCallback(
({ snapshot }) =>
() => {
if (isDefined(mainContextStoreComponentInstanceId)) {
const actionMenuEntries = snapshot.getLoadable(
actionMenuEntriesComponentSelector.selectorFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
);
const actionCommands = actionMenuEntries
.getValue()
?.map((actionMenuEntry) => ({
id: actionMenuEntry.key,
label: actionMenuEntry.label,
Icon: actionMenuEntry.Icon,
onCommandClick: actionMenuEntry.onClick,
type: CommandType.Action,
}));
setCommands(actionCommands);
}
setIsCommandMenuOpened(true);
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
},
[
mainContextStoreComponentInstanceId,
setCommands,
setHotkeyScopeAndMemorizePreviousScope,
setIsCommandMenuOpened,
],
);
const closeCommandMenu = useRecoilCallback(
({ snapshot }) =>

View File

@ -3,13 +3,14 @@ import { IconComponent } from 'twenty-ui';
export enum CommandType {
Navigate = 'Navigate',
Create = 'Create',
Action = 'Action',
}
export type Command = {
id: string;
to: string;
to?: string;
label: string;
type: CommandType.Navigate | CommandType.Create;
type: CommandType.Navigate | CommandType.Create | CommandType.Action;
Icon?: IconComponent;
firstHotKey?: string;
secondHotKey?: string;