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
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()
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["__action_id"] = action
outputs["__action_value"] = selected_action.title if selected_action else ""
outputs["__rendered_content"] = node.render_form_content_with_outputs(
rendered_content, outputs, node_data.outputs_field_names()
)

View File

@@ -11,6 +11,7 @@ This test suite covers:
import json
import uuid
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import ANY, MagicMock, Mock, patch, sentinel
@@ -2649,7 +2650,12 @@ class TestWorkflowServiceHumanInputOperations:
mock_node = 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.render_form_content_before_submission.return_value = "Ticket: {{#$output.field1#}}"
mock_node.render_form_content_with_outputs.return_value = "Ticket: val1"
with (
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"
)
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()
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',
type: VarType.string,
},
{
variable: '__action_value',
type: VarType.string,
},
{
variable: '__rendered_content',
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 = {
...humanInputDefault.defaultValue,
inputs: [],

View File

@@ -313,6 +313,7 @@ describe('human-input/panel', () => {
expect(screen.getByText('approve:editable')).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_value:string:Selected action value')).toBeInTheDocument()
expect(screen.getByText('__rendered_content:string:Rendered content')).toBeInTheDocument()
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: '1invalid' } })
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({
id: 'Approve_action',
@@ -96,7 +96,7 @@ describe('UserActionItem', () => {
id: 'averyveryveryverylon',
}))
expect(onChange).toHaveBeenNthCalledWith(3, expect.objectContaining({
title: 'A very very very lon',
title: 'card_visa_enterprise_001',
}))
expect(mockNotify).toHaveBeenNthCalledWith(1, expect.objectContaining({
type: 'error',
@@ -106,10 +106,7 @@ describe('UserActionItem', () => {
type: 'error',
message: 'nodes.humanInput.userActions.actionIdTooLong',
}))
expect(mockNotify).toHaveBeenNthCalledWith(3, expect.objectContaining({
type: 'error',
message: 'nodes.humanInput.userActions.buttonTextTooLong',
}))
expect(mockNotify).toHaveBeenCalledTimes(2)
})
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 ACTION_ID_MAX_LENGTH = 20
const BUTTON_TEXT_MAX_LENGTH = 20
const ACTION_VALUE_MAX_LENGTH = 100
type UserActionItemProps = {
data: UserAction
@@ -63,9 +63,9 @@ const UserActionItem: FC<UserActionItemProps> = ({
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value
if (value.length > BUTTON_TEXT_MAX_LENGTH) {
value = value.slice(0, BUTTON_TEXT_MAX_LENGTH)
toast.error(t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: BUTTON_TEXT_MAX_LENGTH }))
if (value.length > ACTION_VALUE_MAX_LENGTH) {
value = value.slice(0, ACTION_VALUE_MAX_LENGTH)
toast.error(t(`${i18nPrefix}.userActions.buttonTextTooLong`, { ns: 'workflow', maxLength: ACTION_VALUE_MAX_LENGTH }))
}
onChange({ ...data, title: value })
}

View File

@@ -229,6 +229,11 @@ const Panel: FC<NodePanelProps<HumanInputNodeType>> = ({
type="string"
description="Action ID user triggered"
/>
<VarItem
name="__action_value"
type="string"
description="Selected action value"
/>
<VarItem
name="__rendered_content"
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.actionIdTooLong": "Action ID must be {{maxLength}} characters or less",
"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.chooseStyle": "Choose a button style",
"nodes.humanInput.userActions.emptyTip": "Click the '+' button to add 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.ifElse.addCondition": "Add Condition",
"nodes.ifElse.addSubVariable": "Sub Variable",