Fix test run shortcut consistency and improve dropdown styling (#24849)

This commit is contained in:
lyzno1 2025-09-01 14:47:21 +08:00 committed by GitHub
parent adc7134af5
commit 6d307cc9fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 78 additions and 33 deletions

View File

@ -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<CommonNodeType>()
const edges = useEdges<CommonEdgeType>()
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)

View File

@ -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<TestRunDropdownRef>(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(() => {
</div>
)
: (
<TestRunDropdown options={dynamicOptions} onSelect={handleTriggerSelect}>
<TestRunDropdown
ref={testRunDropdownRef}
options={dynamicOptions}
onSelect={handleTriggerSelect}
>
<div
className={cn(
'flex h-7 items-center rounded-md px-2.5 text-[13px] font-medium text-components-button-secondary-accent-text',

View File

@ -1,11 +1,11 @@
import type { FC } from 'react'
import { useState } from 'react'
import { forwardRef, useImperativeHandle, 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 type TriggerOption = {
id: string
@ -28,20 +28,28 @@ type TestRunDropdownProps = {
children: React.ReactNode
}
const TestRunDropdown: FC<TestRunDropdownProps> = ({
export type TestRunDropdownRef = {
toggle: () => void
}
const TestRunDropdown = forwardRef<TestRunDropdownRef, TestRunDropdownProps>(({
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) => (
<div
key={option.id}
className='system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
@ -53,9 +61,7 @@ const TestRunDropdown: FC<TestRunDropdownProps> = ({
</div>
<span className='ml-2 truncate'>{option.name}</span>
</div>
<div className='ml-2 flex h-4 w-4 shrink-0 items-center justify-center rounded bg-state-base-hover-alt text-xs font-medium text-text-tertiary'>
{numberDisplay}
</div>
<ShortcutsName keys={[shortcutKey]} className="ml-2" textColor="secondary" />
</div>
)
@ -99,6 +105,8 @@ const TestRunDropdown: FC<TestRunDropdownProps> = ({
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
})
TestRunDropdown.displayName = 'TestRunDropdown'
export default TestRunDropdown

View File

@ -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<CommonNodeType>()
const edges = useEdges<CommonEdgeType>()
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,
}
}

View File

@ -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 })