From 6d307cc9fcbe83429324b53afb184ca5898ee5d6 Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:47:21 +0800 Subject: [PATCH] Fix test run shortcut consistency and improve dropdown styling (#24849) --- .../workflow-header/app-publisher-trigger.tsx | 18 ++++------- .../workflow/header/run-and-history.tsx | 30 ++++++++++++++++--- .../workflow/header/test-run-dropdown.tsx | 26 ++++++++++------ .../workflow/hooks/use-checklist.ts | 24 ++++++++++++++- .../workflow/hooks/use-shortcuts.ts | 13 ++++---- 5 files changed, 78 insertions(+), 33 deletions(-) diff --git a/web/app/components/workflow-app/components/workflow-header/app-publisher-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/app-publisher-trigger.tsx index 69a3772496..c658fafb9a 100644 --- a/web/app/components/workflow-app/components/workflow-header/app-publisher-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/app-publisher-trigger.tsx @@ -3,24 +3,20 @@ import { useCallback, useMemo, } from 'react' -import { useEdges, useNodes, useStore as useReactflowStore } from 'reactflow' +import { useStore as useReactflowStore } from 'reactflow' import { useTranslation } from 'react-i18next' import { useStore, useWorkflowStore, } from '@/app/components/workflow/store' import { - useChecklist, useChecklistBeforePublish, useNodesReadOnly, useNodesSyncDraft, + useWorkflowRunValidation, } from '@/app/components/workflow/hooks' import AppPublisher from '@/app/components/app/app-publisher' import { useFeatures } from '@/app/components/base/features/hooks' -import type { - CommonEdgeType, - CommonNodeType, -} from '@/app/components/workflow/types' import { BlockEnum, InputVarType, @@ -81,17 +77,13 @@ const AppPublisherTrigger = () => { }, [appID, setAppDetail]) const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!) - const nodes = useNodes() - const edges = useEdges() - const needWarningNodes = useChecklist(nodes, edges) + const { validateBeforeRun } = useWorkflowRunValidation() const updatePublishedWorkflow = useInvalidateAppWorkflow() const onPublish = useCallback(async (params?: PublishWorkflowParams) => { // First check if there are any items in the checklist - if (needWarningNodes.length > 0) { - notify({ type: 'error', message: t('workflow.panel.checklistTip') }) + if (!validateBeforeRun()) throw new Error('Checklist has unresolved items') - } // Then perform the detailed validation if (await handleCheckBeforePublish()) { @@ -112,7 +104,7 @@ const AppPublisherTrigger = () => { else { throw new Error('Checklist failed') } - }, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, invalidateAppTriggers, workflowStore, resetWorkflowVersionHistory]) + }, [validateBeforeRun, handleCheckBeforePublish, publishWorkflow, updatePublishedWorkflow, appID, updateAppDetail, invalidateAppTriggers, workflowStore, resetWorkflowVersionHistory]) const onPublisherToggle = useCallback((state: boolean) => { if (state) diff --git a/web/app/components/workflow/header/run-and-history.tsx b/web/app/components/workflow/header/run-and-history.tsx index aced70a669..348dc931b1 100644 --- a/web/app/components/workflow/header/run-and-history.tsx +++ b/web/app/components/workflow/header/run-and-history.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import { memo } from 'react' +import { memo, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { RiLoader2Line, @@ -10,12 +10,13 @@ import { useIsChatMode, useNodesReadOnly, useWorkflowRun, + useWorkflowRunValidation, useWorkflowStartRun, } from '../hooks' import { WorkflowRunningStatus } from '../types' import ViewHistory from './view-history' import Checklist from './checklist' -import TestRunDropdown from './test-run-dropdown' +import TestRunDropdown, { type TestRunDropdownRef } from './test-run-dropdown' import type { TriggerOption } from './test-run-dropdown' import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options' import cn from '@/utils/classnames' @@ -31,20 +32,37 @@ const RunMode = memo(() => { const { t } = useTranslation() const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun() const { handleStopRun } = useWorkflowRun() + const { validateBeforeRun } = useWorkflowRunValidation() const workflowRunningData = useStore(s => s.workflowRunningData) const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running const dynamicOptions = useDynamicTestRunOptions() + const testRunDropdownRef = useRef(null) + + useEffect(() => { + // @ts-expect-error - Dynamic property for backward compatibility with keyboard shortcuts + window._toggleTestRunDropdown = () => { + testRunDropdownRef.current?.toggle() + } + return () => { + // @ts-expect-error - Dynamic property cleanup + delete window._toggleTestRunDropdown + } + }, []) const handleStop = () => { handleStopRun(workflowRunningData?.task_id || '') } const handleTriggerSelect = (option: TriggerOption) => { + // Validate checklist before running any workflow + if (!validateBeforeRun()) + return + if (option.type === 'user_input') { handleWorkflowStartRunInWorkflow() } else { - // TODO: Implement trigger-specific execution logic for schedule, webhook, plugin types + // Placeholder for trigger-specific execution logic for schedule, webhook, plugin types console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId) } } @@ -71,7 +89,11 @@ const RunMode = memo(() => { ) : ( - +
= ({ +export type TestRunDropdownRef = { + toggle: () => void +} + +const TestRunDropdown = forwardRef(({ options, onSelect, children, -}) => { +}, ref) => { const { t } = useTranslation() const [open, setOpen] = useState(false) + useImperativeHandle(ref, () => ({ + toggle: () => setOpen(prev => !prev), + })) + const handleSelect = (option: TriggerOption) => { onSelect(option) setOpen(false) } - const renderOption = (option: TriggerOption, numberDisplay: string) => ( + const renderOption = (option: TriggerOption, shortcutKey: string) => (
= ({
{option.name}
-
- {numberDisplay} -
+ ) @@ -99,6 +105,8 @@ const TestRunDropdown: FC = ({ ) -} +}) + +TestRunDropdown.displayName = 'TestRunDropdown' export default TestRunDropdown diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 87d0fa83c9..8308a7395e 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -4,8 +4,9 @@ import { useRef, } from 'react' import { useTranslation } from 'react-i18next' -import { useStoreApi } from 'reactflow' +import { useEdges, useNodes, useStoreApi } from 'reactflow' import type { + CommonEdgeType, CommonNodeType, Edge, Node, @@ -326,3 +327,24 @@ export const useChecklistBeforePublish = () => { handleCheckBeforePublish, } } + +export const useWorkflowRunValidation = () => { + const { t } = useTranslation() + const nodes = useNodes() + const edges = useEdges() + const needWarningNodes = useChecklist(nodes, edges) + const { notify } = useToastContext() + + const validateBeforeRun = useCallback(() => { + if (needWarningNodes.length > 0) { + notify({ type: 'error', message: t('workflow.panel.checklistTip') }) + return false + } + return true + }, [needWarningNodes, notify, t]) + + return { + validateBeforeRun, + hasValidationErrors: needWarningNodes.length > 0, + } +} diff --git a/web/app/components/workflow/hooks/use-shortcuts.ts b/web/app/components/workflow/hooks/use-shortcuts.ts index b2d71555d7..d81e2fc553 100644 --- a/web/app/components/workflow/hooks/use-shortcuts.ts +++ b/web/app/components/workflow/hooks/use-shortcuts.ts @@ -14,7 +14,6 @@ import { useWorkflowCanvasMaximize, useWorkflowMoveMode, useWorkflowOrganize, - useWorkflowStartRun, } from '.' export const useShortcuts = (): void => { @@ -28,7 +27,6 @@ export const useShortcuts = (): void => { dimOtherNodes, undimAllNodes, } = useNodesInteractions() - const { handleStartWorkflowRun } = useWorkflowStartRun() const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { handleEdgeDelete } = useEdgesInteractions() @@ -61,9 +59,8 @@ export const useShortcuts = (): void => { } const shouldHandleShortcut = useCallback((e: KeyboardEvent) => { - const { showFeaturesPanel } = workflowStore.getState() - return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement) - }, [workflowStore]) + return !isEventTargetInputArea(e.target as HTMLElement) + }, []) useKeyPress(['delete', 'backspace'], (e) => { if (shouldHandleShortcut(e)) { @@ -99,7 +96,11 @@ export const useShortcuts = (): void => { useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => { if (shouldHandleShortcut(e)) { e.preventDefault() - handleStartWorkflowRun() + // @ts-expect-error - Dynamic property added by run-and-history component + if (window._toggleTestRunDropdown) { + // @ts-expect-error - Dynamic property added by run-and-history component + window._toggleTestRunDropdown() + } } }, { exactMatch: true, useCapture: true })