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 705fcb6be4..000f66d3f1 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 @@ -286,6 +286,9 @@ describe('human-input/panel', () => { availableVars: [{ variable: ['start', 'email'], type: VarType.string, + }, { + variable: ['code', 'result'], + type: VarType.arrayString, }, { variable: ['start', 'files'], type: VarType.file, @@ -314,6 +317,12 @@ describe('human-input/panel', () => { expect(screen.getByText('review_result:string:Form input value')).toBeInTheDocument() expect(screen.getByText('__action_id:string:Action ID user triggered')).toBeInTheDocument() expect(screen.getByText('__rendered_content:string:Rendered content')).toBeInTheDocument() + expect(mockDeliveryMethod).toHaveBeenCalledWith(expect.objectContaining({ + nodesOutputVars: [ + expect.objectContaining({ type: VarType.string }), + expect.objectContaining({ type: VarType.arrayString }), + ], + })) await user.click(screen.getByRole('button', { name: 'delivery-method:editable' })) await user.click(screen.getByRole('button', { name: /workflow\.nodes\.humanInput\.formContent\.preview/ })) 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 6ec34bba09..87f5fe2701 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 @@ -2,7 +2,7 @@ import type { ReactNode } from 'react' import type { EmailConfig, FormInputItem } from '../../../types' import type { App, AppSSO } from '@/types/app' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen, waitFor } from '@testing-library/react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useStore as useAppStore } from '@/app/components/app/store' import { HooksStoreContext } from '@/app/components/workflow/hooks-store/provider' @@ -161,7 +161,7 @@ const createDynamicSelectInput = (): FormInputItem => ({ output_variable_name: 'decision', option_source: { type: 'variable', - selector: ['start', 'choices'], + selector: ['code', 'result'], value: [], }, }) @@ -243,7 +243,7 @@ describe('human-input/delivery-method/test-email-sender', () => { delivery_method_id: 'delivery-1', inputs: { '#start.user_name#': 'Ada', - '#start.score#': '42', + '#start.score#': 42, }, }, })) @@ -270,23 +270,32 @@ describe('human-input/delivery-method/test-email-sender', () => { formInputs={[createDynamicSelectInput()]} availableNodes={[ { - id: 'start', + id: 'code', type: 'custom', position: { x: 0, y: 0 }, data: { - title: 'Start', + title: 'Code', desc: '', - type: BlockEnum.Start, + type: BlockEnum.Code, + variables: [], + code_language: 'python3', + code: '', + outputs: { + result: { + type: VarType.arrayString, + children: null, + }, + }, }, }, ]} nodesOutputVars={[ { - nodeId: 'start', - title: 'Start', + nodeId: 'code', + title: 'Code', vars: [ { - variable: 'choices', + variable: 'result', type: VarType.arrayString, }, ], @@ -298,7 +307,11 @@ describe('human-input/delivery-method/test-email-sender', () => { const sendButton = screen.getByRole('button', { name: 'workflow.nodes.humanInput.deliveryMethod.emailSender.send' }) expect(sendButton).toBeDisabled() - await user.type(screen.getByPlaceholderText('choices'), 'approve,reject') + expect(screen.queryByPlaceholderText('result')).not.toBeInTheDocument() + + fireEvent.change(screen.getByTestId('monaco-editor'), { + target: { value: '["approve","reject"]' }, + }) expect(sendButton).toBeEnabled() await user.click(sendButton) @@ -309,7 +322,7 @@ describe('human-input/delivery-method/test-email-sender', () => { body: { delivery_method_id: 'delivery-1', inputs: { - '#start.choices#': 'approve,reject', + '#code.result#': ['approve', 'reject'], }, }, }))) 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 957699334c..7247dd78e4 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 @@ -7,6 +7,7 @@ import type { import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' +import { toast } from '@langgenius/dify-ui/toast' import { RiArrowRightSFill } from '@remixicon/react' import { noop, unionBy } from 'es-toolkit/compat' import { memo, useCallback, useMemo, useState } from 'react' @@ -15,6 +16,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import Divider from '@/app/components/base/divider' import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants' import FormItem from '@/app/components/workflow/nodes/_base/components/before-run-form/form-item' +import { formatValue } from '@/app/components/workflow/nodes/_base/components/before-run-form/helpers' import { getNodeInfoById, isConversationVar, @@ -69,6 +71,45 @@ const getOriginVar = (valueSelector: string[], list: NodeOutPutVar[]) => { return undefined } +const varTypeToInputVarType = (type: VarType) => { + if (type === VarType.number) + return InputVarType.number + if (type === VarType.boolean) + return InputVarType.checkbox + if ([VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(type)) + return InputVarType.json + if (type === VarType.file) + return InputVarType.singleFile + if (type === VarType.arrayFile) + return InputVarType.multiFiles + + return InputVarType.textInput +} + +const formatEmailSenderInputs = ( + variables: Array<{ variable: string, type: InputVarType, label: unknown }>, + values: Record, +) => { + const formattedValues: Record = {} + let parseErrorJsonField = '' + + variables.forEach((variable) => { + try { + formattedValues[variable.variable] = formatValue(values[variable.variable], variable.type) + } + catch { + parseErrorJsonField = typeof variable.label === 'object' && variable.label !== null && 'variable' in variable.label + ? String(variable.label.variable) + : variable.variable + } + }) + + return { + formattedValues, + parseErrorJsonField, + } +} + const EmailSenderModal = ({ nodeId, deliveryId, @@ -126,7 +167,7 @@ const EmailSenderModal = ({ return { label: item.label || item.variable, variable: item.variable, - type: originalVar.type === VarType.number ? InputVarType.number : InputVarType.textInput, + type: varTypeToInputVarType(originalVar.type), required: true, } }) @@ -160,20 +201,25 @@ const EmailSenderModal = ({ const handleConfirm = useCallback(async () => { if (!confirmChecked) return + const { formattedValues, parseErrorJsonField } = formatEmailSenderInputs(generatedInputs, inputs) + if (parseErrorJsonField) { + toast.error(t('errorMsg.invalidJson', { ns: 'workflow', field: parseErrorJsonField })) + return + } setSendingEmail(true) try { await testEmailSender({ appID: appDetail?.id || '', nodeID: nodeId, deliveryID: deliveryId, - inputs, + inputs: formattedValues, }) setDone(true) } finally { setSendingEmail(false) } - }, [confirmChecked, testEmailSender, appDetail?.id, nodeId, deliveryId, inputs]) + }, [confirmChecked, generatedInputs, inputs, testEmailSender, appDetail?.id, nodeId, deliveryId, t]) if (done) { return ( diff --git a/web/app/components/workflow/nodes/human-input/panel.tsx b/web/app/components/workflow/nodes/human-input/panel.tsx index fb256144cf..44b6ab3d1f 100644 --- a/web/app/components/workflow/nodes/human-input/panel.tsx +++ b/web/app/components/workflow/nodes/human-input/panel.tsx @@ -69,7 +69,7 @@ const Panel: FC> = ({ const { availableVars, availableNodesWithParent } = useAvailableVarList(id, { onlyLeafNodeVar: false, filterVar: (varPayload: Var) => { - return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) + return [VarType.string, VarType.number, VarType.secret, VarType.arrayString].includes(varPayload.type) }, })