fix(explore): render human input preview handles (#37086)

This commit is contained in:
yyh 2026-06-05 11:32:29 +08:00 committed by GitHub
parent a1ad4be61e
commit 9da4d167fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 125 additions and 1 deletions

View File

@ -1,4 +1,5 @@
import { render } from '@testing-library/react'
import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
import { BlockEnum } from '@/app/components/workflow/types'
import CustomNode from '../index'
@ -41,4 +42,39 @@ describe('workflow preview custom node', () => {
expect(getByText('Classifier node')).toBeInTheDocument()
expect(container.querySelector('[data-handleid="class-a"]')).toBeInTheDocument()
})
it('renders human input output handles from the mapped node component', () => {
const props: React.ComponentProps<typeof CustomNode> = {
id: 'human-input-1',
type: 'custom-node',
selected: false,
zIndex: 1,
isConnectable: true,
dragging: false,
xPos: 0,
yPos: 0,
dragHandle: undefined,
data: {
type: BlockEnum.HumanInput,
title: 'Human Input',
desc: '',
delivery_methods: [],
form_content: '',
inputs: [],
user_actions: [
{ id: 'approve', title: 'Approve', button_style: UserActionButtonType.Primary },
],
timeout: 1,
timeout_unit: 'hour',
} as never,
}
const { container, getByText } = render(
<CustomNode {...props} />,
)
expect(getByText('Human Input')).toBeInTheDocument()
expect(container.querySelector('[data-handleid="approve"]')).toBeInTheDocument()
expect(container.querySelector('[data-handleid="__timeout"]')).toBeInTheDocument()
})
})

View File

@ -1,13 +1,14 @@
import { BlockEnum } from '@/app/components/workflow/types'
import HumanInputNode from './human-input/node'
import IfElseNode from './if-else/node'
import IterationNode from './iteration/node'
import LoopNode from './loop/node'
import QuestionClassifierNode from './question-classifier/node'
// todo: add human-input node support
export const NodeComponentMap: Record<string, any> = {
[BlockEnum.QuestionClassifier]: QuestionClassifierNode,
[BlockEnum.IfElse]: IfElseNode,
[BlockEnum.HumanInput]: HumanInputNode,
[BlockEnum.Iteration]: IterationNode,
[BlockEnum.Loop]: LoopNode,
}

View File

@ -0,0 +1,54 @@
import { render } from '@testing-library/react'
import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
import { BlockEnum } from '@/app/components/workflow/types'
import Node from '../node'
vi.mock('reactflow', () => ({
Handle: (props: { id: string, type: string, className?: string }) => (
<div data-testid="handle" data-handleid={props.id} data-type={props.type} className={props.className} />
),
Position: {
Right: 'right',
},
}))
describe('workflow preview human input node', () => {
it('renders one output handle per user action and timeout', () => {
const props: React.ComponentProps<typeof Node> = {
id: 'human-input-1',
type: 'human-input-node',
selected: false,
zIndex: 1,
isConnectable: true,
dragging: false,
xPos: 0,
yPos: 0,
dragHandle: undefined,
data: {
type: BlockEnum.HumanInput,
title: 'Human Input',
desc: '',
delivery_methods: [],
form_content: '',
inputs: [],
user_actions: [
{ id: 'approve', title: 'Approve', button_style: UserActionButtonType.Primary },
{ id: 'regenerate', title: 'Regenerate', button_style: UserActionButtonType.Default },
],
timeout: 1,
timeout_unit: 'hour',
} as never,
}
const { container, getByText } = render(
<Node {...props} />,
)
expect(getByText('approve')).toBeInTheDocument()
expect(getByText('regenerate')).toBeInTheDocument()
expect(getByText('Timeout')).toBeInTheDocument()
expect(container.querySelector('[data-handleid="approve"]')).toBeInTheDocument()
expect(container.querySelector('[data-handleid="regenerate"]')).toBeInTheDocument()
expect(container.querySelector('[data-handleid="__timeout"]')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,33 @@
import type { NodeProps } from 'reactflow'
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import { memo } from 'react'
import { NodeSourceHandle } from '../../node-handle'
function HumanInputNode(props: NodeProps<HumanInputNodeType>) {
const { data } = props
return (
<div className="space-y-0.5 px-3 py-1">
{data.user_actions.map(userAction => (
<div key={userAction.id} className="relative flex h-6 flex-row-reverse items-center px-1">
<span className="truncate system-xs-semibold-uppercase text-text-secondary">{userAction.id}</span>
<NodeSourceHandle
{...props}
handleId={userAction.id}
handleClassName="top-1/2! -right-[21px]! -translate-y-1/2!"
/>
</div>
))}
<div className="relative flex h-6 flex-row-reverse items-center px-1">
<span className="truncate system-xs-semibold-uppercase text-text-secondary">Timeout</span>
<NodeSourceHandle
{...props}
handleId="__timeout"
handleClassName="top-1/2! -right-[21px]! -translate-y-1/2!"
/>
</div>
</div>
)
}
export default memo(HumanInputNode)