diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index b8c2ed5e6f..eb78e0a68b 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -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() ) diff --git a/api/tests/unit_tests/services/test_workflow_service.py b/api/tests/unit_tests/services/test_workflow_service.py index 1711e66b23..e152ab923c 100644 --- a/api/tests/unit_tests/services/test_workflow_service.py +++ b/api/tests/unit_tests/services/test_workflow_service.py @@ -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: diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 101d15a140..32c7c82e33 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -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, diff --git a/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx b/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx index cbc9cddd4a..8d41a45468 100644 --- a/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/__tests__/human-input.spec.tsx @@ -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: [], diff --git a/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx index 6ff61dce3f..0f0a6839eb 100644 --- a/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/__tests__/panel.spec.tsx @@ -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' })) diff --git a/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx b/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx index a47a012f49..5e9074a42b 100644 --- a/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/__tests__/user-action.spec.tsx @@ -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', () => { diff --git a/web/app/components/workflow/nodes/human-input/components/user-action.tsx b/web/app/components/workflow/nodes/human-input/components/user-action.tsx index a83ea4f8f2..94581281e8 100644 --- a/web/app/components/workflow/nodes/human-input/components/user-action.tsx +++ b/web/app/components/workflow/nodes/human-input/components/user-action.tsx @@ -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 = ({ const handleTextChange = (e: React.ChangeEvent) => { 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 }) } diff --git a/web/app/components/workflow/nodes/human-input/panel.tsx b/web/app/components/workflow/nodes/human-input/panel.tsx index fa0914c098..99b5da42eb 100644 --- a/web/app/components/workflow/nodes/human-input/panel.tsx +++ b/web/app/components/workflow/nodes/human-input/panel.tsx @@ -229,6 +229,11 @@ const Panel: FC> = ({ type="string" description="Action ID user triggered" /> + {{actionName}} has been triggered", "nodes.ifElse.addCondition": "Add Condition", "nodes.ifElse.addSubVariable": "Sub Variable",