From 1949074e2f1cbda8329825e4a6155817963a5fe0 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Mon, 29 Sep 2025 14:39:44 +0800 Subject: [PATCH] add shortcut for open test run panel --- .../workflow/header/test-run-menu.tsx | 100 ++++++++++++++---- 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/web/app/components/workflow/header/test-run-menu.tsx b/web/app/components/workflow/header/test-run-menu.tsx index 8abef9698f..3038505bee 100644 --- a/web/app/components/workflow/header/test-run-menu.tsx +++ b/web/app/components/workflow/header/test-run-menu.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useImperativeHandle, useState } from 'react' +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { PortalToFollowElem, @@ -32,6 +32,29 @@ export type TestRunMenuRef = { toggle: () => void } +type ShortcutMapping = { + option: TriggerOption + shortcutKey: string +} + +const buildShortcutMappings = (options: TestRunOptions): ShortcutMapping[] => { + const mappings: ShortcutMapping[] = [] + + if (options.userInput) + mappings.push({ option: options.userInput, shortcutKey: '~' }) + + let numericShortcut = 0 + + if (options.runAll) + mappings.push({ option: options.runAll, shortcutKey: String(numericShortcut++) }) + + options.triggers.forEach((trigger) => { + mappings.push({ option: trigger, shortcutKey: String(numericShortcut++) }) + }) + + return mappings +} + const TestRunMenu = forwardRef(({ options, onSelect, @@ -39,38 +62,73 @@ const TestRunMenu = forwardRef(({ }, 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]) useImperativeHandle(ref, () => ({ toggle: () => setOpen(prev => !prev), })) - const handleSelect = (option: TriggerOption) => { + const handleSelect = useCallback((option: TriggerOption) => { onSelect(option) setOpen(false) - } + }, [onSelect]) - const renderOption = (option: TriggerOption, shortcutKey: string) => ( -
handleSelect(option)} - > -
-
- {option.icon} + 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}
- {option.name} + {shortcutKey && ( + + )}
- -
- ) + ) + } const hasUserInput = !!options.userInput const hasTriggers = options.triggers.length > 0 const hasRunAll = !!options.runAll - let currentIndex = 0 - return ( (({ {t('workflow.common.chooseStartNodeToRun')}
- {hasUserInput && renderOption(options.userInput!, '~')} + {hasUserInput && renderOption(options.userInput!)} {(hasTriggers || hasRunAll) && hasUserInput && (
)} - {hasRunAll && renderOption(options.runAll!, String(currentIndex++))} + {hasRunAll && renderOption(options.runAll!)} {hasTriggers && options.triggers.map(trigger => - renderOption(trigger, String(currentIndex++)), + renderOption(trigger), )}