import { Dialog, Popover, Transition } from '@headlessui/react' import { Fragment, useEffect } from 'react' import { useLocation } from 'react-router-dom' import CommandBarArgument from '@src/components/CommandBar/CommandBarArgument' import CommandBarReview from '@src/components/CommandBar/CommandBarReview' import CommandComboBox from '@src/components/CommandComboBox' import { CustomIcon } from '@src/components/CustomIcon' import Tooltip from '@src/components/Tooltip' import { useNetworkContext } from '@src/hooks/useNetworkContext' import { EngineConnectionStateType } from '@src/lang/std/engineConnection' import useHotkeyWrapper from '@src/lib/hotkeyWrapper' import { commandBarActor, useCommandBarState, } from '@src/machines/commandBarMachine' import toast from 'react-hot-toast' export const COMMAND_PALETTE_HOTKEY = 'mod+k' export const CommandBar = () => { const { pathname } = useLocation() const commandBarState = useCommandBarState() const { immediateState } = useNetworkContext() const { context: { selectedCommand, currentArgument, commands }, } = commandBarState const isSelectionArgument = currentArgument?.inputType === 'selection' || currentArgument?.inputType === 'selectionMixed' const WrapperComponent = isSelectionArgument ? Popover : Dialog // Close the command bar when navigating useEffect(() => { if (commandBarState.matches('Closed')) return commandBarActor.send({ type: 'Close' }) }, [pathname]) /** * if the engine connection is about to end, we don't want users * to be able to perform commands that might require that connection, * so we just close the command palette. * TODO: instead, let each command control whether it is disabled, and * don't just bail out */ useEffect(() => { if ( !commandBarActor.getSnapshot().matches('Closed') && (immediateState.type === EngineConnectionStateType.Disconnecting || immediateState.type === EngineConnectionStateType.Disconnected) ) { commandBarActor.send({ type: 'Close' }) toast.error('Exiting command flow because engine disconnected') } }, [immediateState, commandBarActor]) // Hook up keyboard shortcuts useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => { if (commandBarState.context.commands.length === 0) return if (commandBarState.matches('Closed')) { commandBarActor.send({ type: 'Open' }) } else { commandBarActor.send({ type: 'Close' }) } }) function stepBack() { if (!currentArgument) { if (commandBarState.matches('Review')) { const entries = Object.entries(selectedCommand?.args || {}).filter( ([_, argConfig]) => !argConfig.hidden && (typeof argConfig.required === 'function' ? argConfig.required(commandBarState.context) : argConfig.required) ) const currentArgName = entries[entries.length - 1][0] const currentArg = { name: currentArgName, ...entries[entries.length - 1][1], } commandBarActor.send({ type: 'Edit argument', data: { arg: currentArg, }, }) } else { commandBarActor.send({ type: 'Deselect command' }) } } else { const entries = Object.entries(selectedCommand?.args || {}).filter( (a) => !a[1].hidden ) const index = entries.findIndex( ([key, _]) => key === currentArgument.name ) if (index === 0) { commandBarActor.send({ type: 'Deselect command' }) } else { commandBarActor.send({ type: 'Change current argument', data: { arg: { name: entries[index - 1][0], ...entries[index - 1][1] }, }, }) } } } return ( { if (selectedCommand?.onCancel) selectedCommand.onCancel() commandBarActor.send({ type: 'Clear' }) }} as={Fragment} > { commandBarActor.send({ type: 'Close' }) }} className={ 'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' + (isSelectionArgument ? 'pointer-events-none' : '') } data-testid="command-bar-wrapper" > {commandBarState.matches('Selecting command') ? ( ) : commandBarState.matches('Gathering arguments') ? ( ) : ( commandBarState.matches('Review') && ( ) )}
{!commandBarState.matches('Selecting command') && ( )}
) } export default CommandBar