diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx index 7c62b20b8a..6ec34bba09 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/test-email-sender.spec.tsx @@ -146,7 +146,7 @@ const createConfig = (overrides: Partial = {}): EmailConfig => ({ }) const createFormInput = (overrides: Partial = {}): FormInputItem => ({ - type: InputVarType.textInput, + type: InputVarType.paragraph, output_variable_name: 'user_name', default: { type: 'variable', @@ -156,6 +156,16 @@ const createFormInput = (overrides: Partial = {}): FormInputItem ...overrides, }) +const createDynamicSelectInput = (): FormInputItem => ({ + type: InputVarType.select, + output_variable_name: 'decision', + option_source: { + type: 'variable', + selector: ['start', 'choices'], + value: [], + }, +}) + describe('human-input/delivery-method/test-email-sender', () => { beforeEach(() => { vi.clearAllMocks() @@ -243,6 +253,68 @@ describe('human-input/delivery-method/test-email-sender', () => { expect(handleOpenChange).toHaveBeenCalledWith(false) }) + it('should submit variables referenced by dynamic select option sources', async () => { + const user = userEvent.setup() + const { requests } = setupFetch() + + renderWithProviders( + , + ) + + const sendButton = screen.getByRole('button', { name: 'workflow.nodes.humanInput.deliveryMethod.emailSender.send' }) + expect(sendButton).toBeDisabled() + + await user.type(screen.getByPlaceholderText('choices'), 'approve,reject') + expect(sendButton).toBeEnabled() + + await user.click(sendButton) + + await waitFor(() => expect(requests).toContainEqual(expect.objectContaining({ + url: 'http://localhost:5001/console/api/apps/app-1/workflows/draft/human-input/nodes/human-node/delivery-test', + method: 'POST', + body: { + delivery_method_id: 'delivery-1', + inputs: { + '#start.choices#': 'approve,reject', + }, + }, + }))) + }) + it('should render fallback variable inputs and allow cancelling', async () => { const user = userEvent.setup() setupFetch() diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx index 1e967edc7d..957699334c 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx @@ -2,7 +2,6 @@ import type { EmailConfig, FormInputItem } from '../../types' import type { Node, NodeOutPutVar, - ValueSelector, Var, } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' @@ -26,8 +25,7 @@ import { InputVarType, VarType } from '@/app/components/workflow/types' import { useAppContext } from '@/context/app-context' import { useMembers } from '@/service/use-common' import { useTestEmailSender } from '@/service/use-workflow' -import { isParagraphFormInput } from '../../types' -import { isOutput } from '../../utils' +import { getHumanInputFormDependencySelectors, isOutput } from '../../utils' import EmailInput from './recipient/email-input' const i18nPrefix = 'nodes.humanInput' @@ -97,14 +95,9 @@ const EmailSenderModal = ({ const accounts = members?.accounts || [] const generatedInputs = useMemo(() => { - const defaultValueSelectors = (formInputs || []).reduce((acc, input) => { - if (isParagraphFormInput(input) && input.default.type === 'variable') { - acc.push(input.default.selector) - } - return acc - }, [] as ValueSelector[]) + const formInputDependencySelectors = getHumanInputFormDependencySelectors(formInputs || []) const valueSelectors = doGetInputVars((formContent || '') + (config?.body || '')) - const variables = unionBy([...valueSelectors, ...defaultValueSelectors], item => item.join('.')).map((item) => { + const variables = unionBy([...valueSelectors, ...formInputDependencySelectors], item => item.join('.')).map((item) => { const varInfo = getNodeInfoById(availableNodes, item[0]!)?.data return { diff --git a/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts b/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts index 84114498ff..b984ee2b2f 100644 --- a/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts +++ b/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts @@ -170,6 +170,61 @@ describe('human-input/hooks/use-single-run-form-params', () => { ]) }) + it('should include variables referenced by dynamic select option sources', () => { + currentInputs = createPayload({ + inputs: [ + { + type: InputVarType.paragraph, + output_variable_name: 'summary', + default: { + type: 'variable', + selector: ['start', 'topic'], + value: '', + }, + }, + { + type: InputVarType.select, + output_variable_name: 'choice', + option_source: { + type: 'variable', + selector: ['start', 'choices'], + value: [], + }, + }, + ], + }) + getInputVars.mockReturnValue([ + createInputVar(), + createInputVar({ + label: 'Choices', + variable: '#start.choices#', + value_selector: ['start', 'choices'], + }), + ]) + + const { result } = renderHook(() => useSingleRunFormParams({ + id: 'node-1', + payload: currentInputs, + runInputData: {}, + getInputVars, + setRunInputData: mockSetRunInputData, + })) + + expect(getInputVars).toHaveBeenCalledWith([ + '{{#start.topic#}}', + '{{#start.choices#}}', + 'Summary: {{#start.topic#}}', + ]) + expect(result.current.forms[0]!.inputs).toEqual([ + expect.objectContaining({ variable: '#start.topic#' }), + expect.objectContaining({ variable: '#start.choices#' }), + ]) + expect(result.current.getDependentVars()).toEqual([ + ['start', 'topic'], + ['start', 'choices'], + ]) + }) + it('should fetch and submit generated forms in workflow mode while keeping required inputs', async () => { const formDataWithFiles = { ...mockFormData, diff --git a/web/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts b/web/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts index e4172a9c20..db7f95de36 100644 --- a/web/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts @@ -10,8 +10,7 @@ import { getProcessedHumanInputFormInputs } from '@/app/components/base/chat/cha import { fetchHumanInputNodeStepRunForm, submitHumanInputNodeStepRunForm } from '@/service/workflow' import { AppModeEnum } from '@/types/app' import useNodeCrud from '../../_base/hooks/use-node-crud' -import { isParagraphFormInput } from '../types' -import { isOutput } from '../utils' +import { getHumanInputFormDependencySelectors, isOutput } from '../utils' const i18nPrefix = 'nodes.humanInput' @@ -36,13 +35,9 @@ const useSingleRunFormParams = ({ const [formData, setFormData] = useState(null) const [requiredInputs, setRequiredInputs] = useState>({}) const generatedInputs = useMemo(() => { - const defaultInputs = inputs.inputs.reduce((acc, input) => { - if (isParagraphFormInput(input) && input.default.type === 'variable') { - acc.push(`{{#${input.default.selector.join('.')}#}}`) - } - return acc - }, [] as string[]) - const allInputs = getInputVars([...defaultInputs, inputs.form_content || '']).filter(item => !isOutput(item.value_selector || [])) + const formInputDependencyInputs = getHumanInputFormDependencySelectors(inputs.inputs) + .map(selector => `{{#${selector.join('.')}#}}`) + const allInputs = getInputVars([...formInputDependencyInputs, inputs.form_content || '']).filter(item => !isOutput(item.value_selector || [])) return allInputs }, [getInputVars, inputs.form_content, inputs.inputs]) diff --git a/web/app/components/workflow/nodes/human-input/utils.ts b/web/app/components/workflow/nodes/human-input/utils.ts index 59358fab1e..6b032430bf 100644 --- a/web/app/components/workflow/nodes/human-input/utils.ts +++ b/web/app/components/workflow/nodes/human-input/utils.ts @@ -1,3 +1,19 @@ +import type { FormInputItem } from './types' +import type { ValueSelector } from '@/app/components/workflow/types' +import { isParagraphFormInput, isSelectFormInput } from './types' + export const isOutput = (valueSelector: string[]) => { return valueSelector[0] === '$output' } + +export const getHumanInputFormDependencySelectors = (inputs: FormInputItem[]): ValueSelector[] => { + return inputs.flatMap((input) => { + if (isParagraphFormInput(input) && input.default.type === 'variable' && input.default.selector.length > 0) + return [input.default.selector] + + if (isSelectFormInput(input) && input.option_source.type === 'variable' && input.option_source.selector.length > 0) + return [input.option_source.selector] + + return [] + }) +}