mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
feat(human-input): expose selected action value (#35451)
This commit is contained in:
parent
e8dc706414
commit
0b70eec695
@ -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()
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
@ -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' }))
|
||||||
|
|||||||
@ -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', () => {
|
||||||
|
|||||||
@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user