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, useCallback,
useMemo, useMemo,
} from 'react' } from 'react'
import { useEdges, useNodes, useStore as useReactflowStore } from 'reactflow' import { useStore as useReactflowStore } from 'reactflow'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
useStore, useStore,
useWorkflowStore, useWorkflowStore,
} from '@/app/components/workflow/store' } from '@/app/components/workflow/store'
import { import {
useChecklist,
useChecklistBeforePublish, useChecklistBeforePublish,
useNodesReadOnly, useNodesReadOnly,
useNodesSyncDraft, useNodesSyncDraft,
useWorkflowRunValidation,
} from '@/app/components/workflow/hooks' } from '@/app/components/workflow/hooks'
import AppPublisher from '@/app/components/app/app-publisher' import AppPublisher from '@/app/components/app/app-publisher'
import { useFeatures } from '@/app/components/base/features/hooks' import { useFeatures } from '@/app/components/base/features/hooks'
import type {
CommonEdgeType,
CommonNodeType,
} from '@/app/components/workflow/types'
import { import {
BlockEnum, BlockEnum,
InputVarType, InputVarType,
@ -81,17 +77,13 @@ const AppPublisherTrigger = () => {
}, [appID, setAppDetail]) }, [appID, setAppDetail])
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!) const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
const nodes = useNodes<CommonNodeType>() const { validateBeforeRun } = useWorkflowRunValidation()
const edges = useEdges<CommonEdgeType>()
const needWarningNodes = useChecklist(nodes, edges)
const updatePublishedWorkflow = useInvalidateAppWorkflow() const updatePublishedWorkflow = useInvalidateAppWorkflow()
const onPublish = useCallback(async (params?: PublishWorkflowParams) => { const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
// First check if there are any items in the checklist // First check if there are any items in the checklist
if (needWarningNodes.length > 0) { if (!validateBeforeRun())
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
throw new Error('Checklist has unresolved items') throw new Error('Checklist has unresolved items')
}
// Then perform the detailed validation // Then perform the detailed validation
if (await handleCheckBeforePublish()) { if (await handleCheckBeforePublish()) {
@ -112,7 +104,7 @@ const AppPublisherTrigger = () => {
else { else {
throw new Error('Checklist failed') 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) => { const onPublisherToggle = useCallback((state: boolean) => {
if (state) if (state)

View File

@ -1,5 +1,5 @@
import type { FC } from 'react' import type { FC } from 'react'
import { memo } from 'react' import { memo, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
RiLoader2Line, RiLoader2Line,
@ -10,12 +10,13 @@ import {
useIsChatMode, useIsChatMode,
useNodesReadOnly, useNodesReadOnly,
useWorkflowRun, useWorkflowRun,
useWorkflowRunValidation,
useWorkflowStartRun, useWorkflowStartRun,
} from '../hooks' } from '../hooks'
import { WorkflowRunningStatus } from '../types' import { WorkflowRunningStatus } from '../types'
import ViewHistory from './view-history' import ViewHistory from './view-history'
import Checklist from './checklist' 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 type { TriggerOption } from './test-run-dropdown'
import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options' import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
@ -31,20 +32,37 @@ const RunMode = memo(() => {
const { t } = useTranslation() const { t } = useTranslation()
const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun() const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun()
const { handleStopRun } = useWorkflowRun() const { handleStopRun } = useWorkflowRun()
const { validateBeforeRun } = useWorkflowRunValidation()
const workflowRunningData = useStore(s => s.workflowRunningData) const workflowRunningData = useStore(s => s.workflowRunningData)
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
const dynamicOptions = useDynamicTestRunOptions() 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 = () => { const handleStop = () => {
handleStopRun(workflowRunningData?.task_id || '') handleStopRun(workflowRunningData?.task_id || '')
} }
const handleTriggerSelect = (option: TriggerOption) => { const handleTriggerSelect = (option: TriggerOption) => {
// Validate checklist before running any workflow
if (!validateBeforeRun())
return
if (option.type === 'user_input') { if (option.type === 'user_input') {
handleWorkflowStartRunInWorkflow() handleWorkflowStartRunInWorkflow()
} }
else { 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) console.log('TODO: Handle trigger execution for type:', option.type, 'nodeId:', option.nodeId)
} }
} }
@ -71,7 +89,11 @@ const RunMode = memo(() => {
</div> </div>
) )
: ( : (
<TestRunDropdown options={dynamicOptions} onSelect={handleTriggerSelect}> <TestRunDropdown
ref={testRunDropdownRef}
options={dynamicOptions}
onSelect={handleTriggerSelect}
>
<div <div
className={cn( className={cn(
'flex h-7 items-center rounded-md px-2.5 text-[13px] font-medium text-components-button-secondary-accent-text', '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 { forwardRef, useImperativeHandle, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
PortalToFollowElem, PortalToFollowElem,
PortalToFollowElemContent, PortalToFollowElemContent,
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import ShortcutsName from '../shortcuts-name'
export type TriggerOption = { export type TriggerOption = {
id: string id: string
@ -28,20 +28,28 @@ type TestRunDropdownProps = {
children: React.ReactNode children: React.ReactNode
} }
const TestRunDropdown: FC<TestRunDropdownProps> = ({ export type TestRunDropdownRef = {
toggle: () => void
}
const TestRunDropdown = forwardRef<TestRunDropdownRef, TestRunDropdownProps>(({
options, options,
onSelect, onSelect,
children, children,
}) => { }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
useImperativeHandle(ref, () => ({
toggle: () => setOpen(prev => !prev),
}))
const handleSelect = (option: TriggerOption) => { const handleSelect = (option: TriggerOption) => {
onSelect(option) onSelect(option)
setOpen(false) setOpen(false)
} }
const renderOption = (option: TriggerOption, numberDisplay: string) => ( const renderOption = (option: TriggerOption, shortcutKey: string) => (
<div <div
key={option.id} 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' 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> </div>
<span className='ml-2 truncate'>{option.name}</span> <span className='ml-2 truncate'>{option.name}</span>
</div> </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'> <ShortcutsName keys={[shortcutKey]} className="ml-2" textColor="secondary" />
{numberDisplay}
</div>
</div> </div>
) )
@ -99,6 +105,8 @@ const TestRunDropdown: FC<TestRunDropdownProps> = ({
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </PortalToFollowElem>
) )
} })
TestRunDropdown.displayName = 'TestRunDropdown'
export default TestRunDropdown export default TestRunDropdown

View File

@ -4,8 +4,9 @@ import {
useRef, useRef,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow' import { useEdges, useNodes, useStoreApi } from 'reactflow'
import type { import type {
CommonEdgeType,
CommonNodeType, CommonNodeType,
Edge, Edge,
Node, Node,
@ -326,3 +327,24 @@ export const useChecklistBeforePublish = () => {
handleCheckBeforePublish, 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, useWorkflowCanvasMaximize,
useWorkflowMoveMode, useWorkflowMoveMode,
useWorkflowOrganize, useWorkflowOrganize,
useWorkflowStartRun,
} from '.' } from '.'
export const useShortcuts = (): void => { export const useShortcuts = (): void => {
@ -28,7 +27,6 @@ export const useShortcuts = (): void => {
dimOtherNodes, dimOtherNodes,
undimAllNodes, undimAllNodes,
} = useNodesInteractions() } = useNodesInteractions()
const { handleStartWorkflowRun } = useWorkflowStartRun()
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore() const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { handleEdgeDelete } = useEdgesInteractions() const { handleEdgeDelete } = useEdgesInteractions()
@ -61,9 +59,8 @@ export const useShortcuts = (): void => {
} }
const shouldHandleShortcut = useCallback((e: KeyboardEvent) => { const shouldHandleShortcut = useCallback((e: KeyboardEvent) => {
const { showFeaturesPanel } = workflowStore.getState() return !isEventTargetInputArea(e.target as HTMLElement)
return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement) }, [])
}, [workflowStore])
useKeyPress(['delete', 'backspace'], (e) => { useKeyPress(['delete', 'backspace'], (e) => {
if (shouldHandleShortcut(e)) { if (shouldHandleShortcut(e)) {
@ -99,7 +96,11 @@ export const useShortcuts = (): void => {
useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => { useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => {
if (shouldHandleShortcut(e)) { if (shouldHandleShortcut(e)) {
e.preventDefault() 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 }) }, { exactMatch: true, useCapture: true })