mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 00:18:28 +08:00
Fix test run shortcut consistency and improve dropdown styling (#24849)
This commit is contained in:
parent
adc7134af5
commit
6d307cc9fc
@ -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)
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user