mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
fix(web): support dynamic selector in human input step run & email configure
This commit is contained in:
parent
23d39beeed
commit
d65cc21e85
@ -146,7 +146,7 @@ const createConfig = (overrides: Partial<EmailConfig> = {}): EmailConfig => ({
|
||||
})
|
||||
|
||||
const createFormInput = (overrides: Partial<FormInputItem> = {}): FormInputItem => ({
|
||||
type: InputVarType.textInput,
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'user_name',
|
||||
default: {
|
||||
type: 'variable',
|
||||
@ -156,6 +156,16 @@ const createFormInput = (overrides: Partial<FormInputItem> = {}): 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(
|
||||
<EmailSenderModal
|
||||
nodeId="human-node"
|
||||
deliveryId="delivery-1"
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
jumpToEmailConfigModal={vi.fn()}
|
||||
config={createConfig({
|
||||
body: '{{#url#}}',
|
||||
})}
|
||||
formInputs={[createDynamicSelectInput()]}
|
||||
availableNodes={[
|
||||
{
|
||||
id: 'start',
|
||||
type: 'custom',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
title: 'Start',
|
||||
desc: '',
|
||||
type: BlockEnum.Start,
|
||||
},
|
||||
},
|
||||
]}
|
||||
nodesOutputVars={[
|
||||
{
|
||||
nodeId: 'start',
|
||||
title: 'Start',
|
||||
vars: [
|
||||
{
|
||||
variable: 'choices',
|
||||
type: VarType.arrayString,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<HumanInputFormData | null>(null)
|
||||
const [requiredInputs, setRequiredInputs] = useState<Record<string, string>>({})
|
||||
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])
|
||||
|
||||
|
||||
@ -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 []
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user