feat(human-input): expose selected action value (#35451)

This commit is contained in:
Blackoutta 2026-05-11 10:16:29 +08:00 committed by GitHub
parent e8dc706414
commit 0b70eec695
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 33 additions and 13 deletions

View File

@ -1066,8 +1066,13 @@ class WorkflowService:
) )
rendered_content = node.render_form_content_before_submission() rendered_content = node.render_form_content_before_submission()
selected_action = next(
(user_action for user_action in node_data.user_actions if user_action.id == action),
None,
)
outputs: dict[str, Any] = dict(form_inputs) outputs: dict[str, Any] = dict(form_inputs)
outputs["__action_id"] = action outputs["__action_id"] = action
outputs["__action_value"] = selected_action.title if selected_action else ""
outputs["__rendered_content"] = node.render_form_content_with_outputs( outputs["__rendered_content"] = node.render_form_content_with_outputs(
rendered_content, outputs, node_data.outputs_field_names() rendered_content, outputs, node_data.outputs_field_names()
) )

View File

@ -11,6 +11,7 @@ This test suite covers:
import json import json
import uuid import uuid
from types import SimpleNamespace
from typing import Any, cast from typing import Any, cast
from unittest.mock import ANY, MagicMock, Mock, patch, sentinel from unittest.mock import ANY, MagicMock, Mock, patch, sentinel
@ -2649,7 +2650,12 @@ class TestWorkflowServiceHumanInputOperations:
mock_node = MagicMock() mock_node = MagicMock()
mock_node.node_data = MagicMock() mock_node.node_data = MagicMock()
mock_node.node_data.user_actions = [
SimpleNamespace(id="submit", title="card_visa_enterprise_001"),
]
mock_node.node_data.outputs_field_names.return_value = ["field1"] mock_node.node_data.outputs_field_names.return_value = ["field1"]
mock_node.render_form_content_before_submission.return_value = "Ticket: {{#$output.field1#}}"
mock_node.render_form_content_with_outputs.return_value = "Ticket: val1"
with ( with (
patch("services.workflow_service.db"), patch("services.workflow_service.db"),
@ -2665,6 +2671,8 @@ class TestWorkflowServiceHumanInputOperations:
app_model=app_model, account=account, node_id="node-1", form_inputs={"field1": "val1"}, action="submit" app_model=app_model, account=account, node_id="node-1", form_inputs={"field1": "val1"}, action="submit"
) )
assert result["__action_id"] == "submit" assert result["__action_id"] == "submit"
assert result["__action_value"] == "card_visa_enterprise_001"
assert result["__rendered_content"] == "Ticket: val1"
mock_saver_cls.return_value.save.assert_called_once() mock_saver_cls.return_value.save.assert_called_once()
def test_test_human_input_delivery_success(self, service: WorkflowService) -> None: def test_test_human_input_delivery_success(self, service: WorkflowService) -> None:

View File

@ -221,6 +221,10 @@ export const HUMAN_INPUT_OUTPUT_STRUCT: Var[] = [
variable: '__action_id', variable: '__action_id',
type: VarType.string, type: VarType.string,
}, },
{
variable: '__action_value',
type: VarType.string,
},
{ {
variable: '__rendered_content', variable: '__rendered_content',
type: VarType.string, type: VarType.string,

View File

@ -516,7 +516,7 @@ describe('DSL Import with Human Input Node', () => {
]) ])
}) })
it('should return empty output variables when no form inputs exist', () => { it('should return no output variables when no form inputs exist', () => {
const payload = { const payload = {
...humanInputDefault.defaultValue, ...humanInputDefault.defaultValue,
inputs: [], inputs: [],

View File

@ -313,6 +313,7 @@ describe('human-input/panel', () => {
expect(screen.getByText('approve:editable')).toBeInTheDocument() expect(screen.getByText('approve:editable')).toBeInTheDocument()
expect(screen.getByText('review_result:string:Form input value')).toBeInTheDocument() expect(screen.getByText('review_result:string:Form input value')).toBeInTheDocument()
expect(screen.getByText('__action_id:string:Action ID user triggered')).toBeInTheDocument() expect(screen.getByText('__action_id:string:Action ID user triggered')).toBeInTheDocument()
expect(screen.getByText('__action_value:string:Selected action value')).toBeInTheDocument()
expect(screen.getByText('__rendered_content:string:Rendered content')).toBeInTheDocument() expect(screen.getByText('__rendered_content:string:Rendered content')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'delivery-method:editable' })) await user.click(screen.getByRole('button', { name: 'delivery-method:editable' }))

View File

@ -87,7 +87,7 @@ describe('UserActionItem', () => {
fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: 'Approve action' } }) fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: 'Approve action' } })
fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: '1invalid' } }) fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: '1invalid' } })
fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: 'averyveryveryverylongidentifier' } }) fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.actionNamePlaceholder'), { target: { value: 'averyveryveryverylongidentifier' } })
fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.buttonTextPlaceholder'), { target: { value: 'A very very very long button title' } }) fireEvent.change(screen.getByTestId('nodes.humanInput.userActions.buttonTextPlaceholder'), { target: { value: 'card_visa_enterprise_001' } })
expect(onChange).toHaveBeenNthCalledWith(1, expect.objectContaining({ expect(onChange).toHaveBeenNthCalledWith(1, expect.objectContaining({
id: 'Approve_action', id: 'Approve_action',
@ -96,7 +96,7 @@ describe('UserActionItem', () => {
id: 'averyveryveryverylon', id: 'averyveryveryverylon',
})) }))
expect(onChange).toHaveBeenNthCalledWith(3, expect.objectContaining({ expect(onChange).toHaveBeenNthCalledWith(3, expect.objectContaining({
title: 'A very very very lon', title: 'card_visa_enterprise_001',
})) }))
expect(mockNotify).toHaveBeenNthCalledWith(1, expect.objectContaining({ expect(mockNotify).toHaveBeenNthCalledWith(1, expect.objectContaining({
type: 'error', type: 'error',
@ -106,10 +106,7 @@ describe('UserActionItem', () => {
type: 'error', type: 'error',
message: 'nodes.humanInput.userActions.actionIdTooLong', message: 'nodes.humanInput.userActions.actionIdTooLong',
})) }))
expect(mockNotify).toHaveBeenNthCalledWith(3, expect.objectContaining({ expect(mockNotify).toHaveBeenCalledTimes(2)
type: 'error',
message: 'nodes.humanInput.userActions.buttonTextTooLong',
}))
}) })
it('should support clearing ids, updating button style, deleting, and readonly mode', () => { it('should support clearing ids, updating button style, deleting, and readonly mode', () => {

View File

@ -12,7 +12,7 @@ import ButtonStyleDropdown from './button-style-dropdown'
const i18nPrefix = 'nodes.humanInput' const i18nPrefix = 'nodes.humanInput'
const ACTION_ID_MAX_LENGTH = 20 const ACTION_ID_MAX_LENGTH = 20
const BUTTON_TEXT_MAX_LENGTH = 20 const ACTION_VALUE_MAX_LENGTH = 100
type UserActionItemProps = { type UserActionItemProps = {
data: UserAction data: UserAction
@ -63,9 +63,9 @@ const UserActionItem: FC<UserActionItemProps> = ({
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value let value = e.target.value
if (value.length > BUTTON_TEXT_MAX_LENGTH) { if (value.length > ACTION_VALUE_MAX_LENGTH) {
value = value.slice(0, BUTTON_TEXT_MAX_LENGTH) value = value.slice(0, ACTION_VALUE_MAX_LENGTH)
toast.error(t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: BUTTON_TEXT_MAX_LENGTH })) toast.error(t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: ACTION_VALUE_MAX_LENGTH }))
} }
onChange({ ...data, title: value }) onChange({ ...data, title: value })
} }

View File

@ -229,6 +229,11 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
type="string" type="string"
description="Action ID user triggered" description="Action ID user triggered"
/> />
<VarItem
name="__action_value"
type="string"
description="Selected action value"
/>
<VarItem <VarItem
name="__rendered_content" name="__rendered_content"
type="string" type="string"

View File

@ -646,12 +646,12 @@
"nodes.humanInput.userActions.actionIdFormatTip": "Action ID must start with a letter or underscores, followed by letters, numbers, or underscores", "nodes.humanInput.userActions.actionIdFormatTip": "Action ID must start with a letter or underscores, followed by letters, numbers, or underscores",
"nodes.humanInput.userActions.actionIdTooLong": "Action ID must be {{maxLength}} characters or less", "nodes.humanInput.userActions.actionIdTooLong": "Action ID must be {{maxLength}} characters or less",
"nodes.humanInput.userActions.actionNamePlaceholder": "Action Name", "nodes.humanInput.userActions.actionNamePlaceholder": "Action Name",
"nodes.humanInput.userActions.buttonTextPlaceholder": "Button display Text", "nodes.humanInput.userActions.buttonTextPlaceholder": "Action Value",
"nodes.humanInput.userActions.buttonTextTooLong": "Button text must be {{maxLength}} characters or less", "nodes.humanInput.userActions.buttonTextTooLong": "Button text must be {{maxLength}} characters or less",
"nodes.humanInput.userActions.chooseStyle": "Choose a button style", "nodes.humanInput.userActions.chooseStyle": "Choose a button style",
"nodes.humanInput.userActions.emptyTip": "Click the '+' button to add user actions", "nodes.humanInput.userActions.emptyTip": "Click the '+' button to add user actions",
"nodes.humanInput.userActions.title": "User Actions", "nodes.humanInput.userActions.title": "User Actions",
"nodes.humanInput.userActions.tooltip": "Define buttons that users can click to respond to this form. Each button can trigger different workflow paths. Action ID must start with a letter or underscores, followed by letters, numbers, or underscores.", "nodes.humanInput.userActions.tooltip": "Define buttons that users can click to respond to this form. Action ID controls branching. Action Value is exposed downstream as the selected built-in output. Action ID must start with a letter or underscores, followed by letters, numbers, or underscores.",
"nodes.humanInput.userActions.triggered": "<strong>{{actionName}}</strong> has been triggered", "nodes.humanInput.userActions.triggered": "<strong>{{actionName}}</strong> has been triggered",
"nodes.ifElse.addCondition": "Add Condition", "nodes.ifElse.addCondition": "Add Condition",
"nodes.ifElse.addSubVariable": "Sub Variable", "nodes.ifElse.addSubVariable": "Sub Variable",