import { type MouseEvent, type MouseEventHandler, type ReactElement, cloneElement, forwardRef, isValidElement, useCallback, useEffect, useImperativeHandle, useMemo, useState, } from 'react' import { useTranslation } from 'react-i18next' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import ShortcutsName from '../shortcuts-name' export enum TriggerType { UserInput = 'user_input', Schedule = 'schedule', Webhook = 'webhook', Plugin = 'plugin', All = 'all', } export type TriggerOption = { id: string type: TriggerType name: string icon: React.ReactNode nodeId?: string relatedNodeIds?: string[] enabled: boolean } export type TestRunOptions = { userInput?: TriggerOption triggers: TriggerOption[] runAll?: TriggerOption } type TestRunMenuProps = { options: TestRunOptions onSelect: (option: TriggerOption) => void children: React.ReactNode } export type TestRunMenuRef = { toggle: () => void } type ShortcutMapping = { option: TriggerOption shortcutKey: string } const buildShortcutMappings = (options: TestRunOptions): ShortcutMapping[] => { const mappings: ShortcutMapping[] = [] if (options.userInput && options.userInput.enabled !== false) mappings.push({ option: options.userInput, shortcutKey: '~' }) let numericShortcut = 0 if (options.runAll && options.runAll.enabled !== false) mappings.push({ option: options.runAll, shortcutKey: String(numericShortcut++) }) options.triggers.forEach((trigger) => { if (trigger.enabled !== false) mappings.push({ option: trigger, shortcutKey: String(numericShortcut++) }) }) return mappings } const TestRunMenu = forwardRef(({ options, onSelect, children, }, ref) => { const { t } = useTranslation() const [open, setOpen] = useState(false) const shortcutMappings = useMemo(() => buildShortcutMappings(options), [options]) const shortcutKeyById = useMemo(() => { const map = new Map() shortcutMappings.forEach(({ option, shortcutKey }) => { map.set(option.id, shortcutKey) }) return map }, [shortcutMappings]) const handleSelect = useCallback((option: TriggerOption) => { onSelect(option) setOpen(false) }, [onSelect]) const enabledOptions = useMemo(() => { const flattened: TriggerOption[] = [] if (options.userInput) flattened.push(options.userInput) if (options.runAll) flattened.push(options.runAll) flattened.push(...options.triggers) return flattened.filter(option => option.enabled !== false) }, [options]) const hasSingleEnabledOption = enabledOptions.length === 1 const soleEnabledOption = hasSingleEnabledOption ? enabledOptions[0] : undefined const runSoleOption = useCallback(() => { if (soleEnabledOption) handleSelect(soleEnabledOption) }, [handleSelect, soleEnabledOption]) useImperativeHandle(ref, () => ({ toggle: () => { if (hasSingleEnabledOption) { runSoleOption() return } setOpen(prev => !prev) }, }), [hasSingleEnabledOption, runSoleOption]) useEffect(() => { if (!open) return const handleKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented || event.repeat || event.altKey || event.ctrlKey || event.metaKey) return const normalizedKey = event.key === '`' ? '~' : event.key const mapping = shortcutMappings.find(({ shortcutKey }) => shortcutKey === normalizedKey) if (mapping) { event.preventDefault() handleSelect(mapping.option) } } window.addEventListener('keydown', handleKeyDown) return () => { window.removeEventListener('keydown', handleKeyDown) } }, [handleSelect, open, shortcutMappings]) const renderOption = (option: TriggerOption) => { const shortcutKey = shortcutKeyById.get(option.id) return (
handleSelect(option)} >
{option.icon}
{option.name}
{shortcutKey && ( )}
) } const hasUserInput = !!options.userInput && options.userInput.enabled !== false const hasTriggers = options.triggers.some(trigger => trigger.enabled !== false) const hasRunAll = !!options.runAll && options.runAll.enabled !== false if (hasSingleEnabledOption && soleEnabledOption) { const handleRunClick = (event?: MouseEvent) => { if (event?.defaultPrevented) return runSoleOption() } if (isValidElement(children)) { const childElement = children as ReactElement<{ onClick?: MouseEventHandler }> const originalOnClick = childElement.props?.onClick return cloneElement(childElement, { onClick: (event: MouseEvent) => { if (typeof originalOnClick === 'function') originalOnClick(event) if (event?.defaultPrevented) return runSoleOption() }, }) } return ( {children} ) } return ( setOpen(!open)}>
{children}
{t('workflow.common.chooseStartNodeToRun')}
{hasUserInput && renderOption(options.userInput!)} {(hasTriggers || hasRunAll) && hasUserInput && (
)} {hasRunAll && renderOption(options.runAll!)} {hasTriggers && options.triggers .filter(trigger => trigger.enabled !== false) .map(trigger => renderOption(trigger))}
) }) TestRunMenu.displayName = 'TestRunMenu' export default TestRunMenu