diff --git a/web/app/components/workflow/workflow-preview/components/nodes/__tests__/index.spec.tsx b/web/app/components/workflow/workflow-preview/components/nodes/__tests__/index.spec.tsx index fc7dc8cc68..44cfeed0b1 100644 --- a/web/app/components/workflow/workflow-preview/components/nodes/__tests__/index.spec.tsx +++ b/web/app/components/workflow/workflow-preview/components/nodes/__tests__/index.spec.tsx @@ -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 = { + 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( + , + ) + + expect(getByText('Human Input')).toBeInTheDocument() + expect(container.querySelector('[data-handleid="approve"]')).toBeInTheDocument() + expect(container.querySelector('[data-handleid="__timeout"]')).toBeInTheDocument() + }) }) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/constants.ts b/web/app/components/workflow/workflow-preview/components/nodes/constants.ts index 072b5bd249..37df7499bd 100644 --- a/web/app/components/workflow/workflow-preview/components/nodes/constants.ts +++ b/web/app/components/workflow/workflow-preview/components/nodes/constants.ts @@ -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 = { [BlockEnum.QuestionClassifier]: QuestionClassifierNode, [BlockEnum.IfElse]: IfElseNode, + [BlockEnum.HumanInput]: HumanInputNode, [BlockEnum.Iteration]: IterationNode, [BlockEnum.Loop]: LoopNode, } diff --git a/web/app/components/workflow/workflow-preview/components/nodes/human-input/__tests__/node.spec.tsx b/web/app/components/workflow/workflow-preview/components/nodes/human-input/__tests__/node.spec.tsx new file mode 100644 index 0000000000..5bdb591198 --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/human-input/__tests__/node.spec.tsx @@ -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 }) => ( +
+ ), + Position: { + Right: 'right', + }, +})) + +describe('workflow preview human input node', () => { + it('renders one output handle per user action and timeout', () => { + const props: React.ComponentProps = { + 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( + , + ) + + 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() + }) +}) diff --git a/web/app/components/workflow/workflow-preview/components/nodes/human-input/node.tsx b/web/app/components/workflow/workflow-preview/components/nodes/human-input/node.tsx new file mode 100644 index 0000000000..9248640d1e --- /dev/null +++ b/web/app/components/workflow/workflow-preview/components/nodes/human-input/node.tsx @@ -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) { + const { data } = props + + return ( +
+ {data.user_actions.map(userAction => ( +
+ {userAction.id} + +
+ ))} +
+ Timeout + +
+
+ ) +} + +export default memo(HumanInputNode)