mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
Add human input field type selector
This commit is contained in:
parent
93945d603e
commit
71803d7c76
@ -18,6 +18,26 @@ vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/configuration/config-var/config-modal/type-select', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onSelect }: { onSelect: (item: { value: InputVarType }) => void }) => (
|
||||
<div>
|
||||
<button type="button" onClick={() => onSelect({ value: InputVarType.paragraph })}>
|
||||
select-paragraph
|
||||
</button>
|
||||
<button type="button" onClick={() => onSelect({ value: InputVarType.select })}>
|
||||
select-select
|
||||
</button>
|
||||
<button type="button" onClick={() => onSelect({ value: InputVarType.singleFile })}>
|
||||
select-file
|
||||
</button>
|
||||
<button type="button" onClick={() => onSelect({ value: InputVarType.multiFiles })}>
|
||||
select-file-list
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createPayload = (overrides?: Partial<FormInputItem>): FormInputItem => ({
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'valid_name',
|
||||
@ -274,4 +294,34 @@ describe('InputField', () => {
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('should switch to select payload when field type changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<InputField
|
||||
nodeId="node-7"
|
||||
isEdit={false}
|
||||
payload={createPayload()}
|
||||
onChange={onChange}
|
||||
onCancel={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'select-select' }))
|
||||
await user.click(screen.getByRole('button', { name: /workflow\.nodes\.humanInput\.insertInputField\.insert/i }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
expect(onChange.mock.calls[0]![0]).toEqual({
|
||||
type: InputVarType.select,
|
||||
output_variable_name: 'valid_name',
|
||||
option_source: {
|
||||
type: 'constant',
|
||||
selector: [],
|
||||
value: [],
|
||||
},
|
||||
})
|
||||
expect(screen.queryByText(/workflow\.nodes\.humanInput\.insertInputField\.prePopulateField/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { Item as TypeSelectItem } from '@/app/components/app/configuration/config-var/config-modal/type-select'
|
||||
import type { FormInputItem, FormInputItemDefault, ParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
@ -5,11 +6,14 @@ import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TypeSelector from '@/app/components/app/configuration/config-var/config-modal/type-select'
|
||||
import Input from '@/app/components/base/input'
|
||||
import {
|
||||
createDefaultFormInputByType,
|
||||
createDefaultParagraphFormInput,
|
||||
isParagraphFormInput,
|
||||
} from '@/app/components/workflow/nodes/human-input/types'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import PrePopulate from './pre-populate'
|
||||
|
||||
@ -31,9 +35,33 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [tempPayload, setTempPayload] = useState<FormInputItem>(() => payload || createDefaultParagraphFormInput())
|
||||
const fieldTypeItems = useMemo<TypeSelectItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
name: t('variableConfig.paragraph', { ns: 'appDebug' }),
|
||||
value: InputVarType.paragraph,
|
||||
},
|
||||
{
|
||||
name: t('variableConfig.select', { ns: 'appDebug' }),
|
||||
value: InputVarType.select,
|
||||
},
|
||||
{
|
||||
name: t('variableConfig.single-file', { ns: 'appDebug' }),
|
||||
value: InputVarType.singleFile,
|
||||
},
|
||||
{
|
||||
name: t('variableConfig.multi-files', { ns: 'appDebug' }),
|
||||
value: InputVarType.multiFiles,
|
||||
},
|
||||
]
|
||||
}, [t])
|
||||
const paragraphPayload = useMemo<ParagraphFormInput>(() => {
|
||||
if (isParagraphFormInput(tempPayload))
|
||||
return tempPayload
|
||||
if (isParagraphFormInput(tempPayload)) {
|
||||
return {
|
||||
...tempPayload,
|
||||
default: tempPayload.default || createDefaultParagraphFormInput().default,
|
||||
}
|
||||
}
|
||||
|
||||
return createDefaultParagraphFormInput(tempPayload.output_variable_name)
|
||||
}, [tempPayload])
|
||||
@ -50,6 +78,9 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
return
|
||||
onChange(tempPayload)
|
||||
}, [nameValid, onChange, tempPayload])
|
||||
const handleTypeChange = useCallback((item: TypeSelectItem) => {
|
||||
setTempPayload(prev => createDefaultFormInputByType(item.value as FormInputItem['type'], prev.output_variable_name))
|
||||
}, [])
|
||||
const handleDefaultValueChange = useCallback((key: keyof FormInputItemDefault) => {
|
||||
return (value: ValueSelector | string) => {
|
||||
const nextValue = produce(paragraphPayload, (draft) => {
|
||||
@ -85,6 +116,18 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
return (
|
||||
<div className="w-[372px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-3 shadow-lg backdrop-blur-[5px]">
|
||||
<div className="system-md-semibold text-text-primary">{t(`${i18nPrefix}.title`, { ns: 'workflow' })}</div>
|
||||
<div className="mt-3">
|
||||
<div className="system-xs-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.fieldType`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="mt-1.5">
|
||||
<TypeSelector
|
||||
value={tempPayload.type}
|
||||
items={fieldTypeItems}
|
||||
onSelect={handleTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<div className="system-xs-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.saveResponseAs`, { ns: 'workflow' })}
|
||||
@ -105,22 +148,24 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="mb-1.5 system-xs-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.prePopulateField`, { ns: 'workflow' })}
|
||||
{isParagraphFormInput(tempPayload) && (
|
||||
<div className="mt-4">
|
||||
<div className="mb-1.5 system-xs-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.prePopulateField`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<PrePopulate
|
||||
isVariable={paragraphPayload.default.type === 'variable'}
|
||||
onIsVariableChange={(isVariable) => {
|
||||
handleDefaultValueChange('type')(isVariable ? 'variable' : 'constant')
|
||||
}}
|
||||
nodeId={nodeId}
|
||||
valueSelector={paragraphPayload.default.selector}
|
||||
onValueSelectorChange={handleDefaultValueChange('selector')}
|
||||
value={paragraphPayload.default.value}
|
||||
onValueChange={handleDefaultValueChange('value')}
|
||||
/>
|
||||
</div>
|
||||
<PrePopulate
|
||||
isVariable={paragraphPayload.default.type === 'variable'}
|
||||
onIsVariableChange={(isVariable) => {
|
||||
handleDefaultValueChange('type')(isVariable ? 'variable' : 'constant')
|
||||
}}
|
||||
nodeId={nodeId}
|
||||
valueSelector={paragraphPayload.default.selector}
|
||||
onValueSelectorChange={handleDefaultValueChange('selector')}
|
||||
value={paragraphPayload.default.value}
|
||||
onValueChange={handleDefaultValueChange('value')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<Button data-testid="hitl-input-cancel-btn" onClick={onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
{isEdit
|
||||
|
||||
@ -156,3 +156,53 @@ export const createDefaultParagraphFormInput = (
|
||||
value: '',
|
||||
},
|
||||
})
|
||||
|
||||
export const createDefaultSelectFormInput = (
|
||||
output_variable_name = '',
|
||||
): SelectFormInput => ({
|
||||
type: InputVarType.select,
|
||||
output_variable_name,
|
||||
option_source: {
|
||||
type: 'constant',
|
||||
selector: [],
|
||||
value: [],
|
||||
},
|
||||
})
|
||||
|
||||
export const createDefaultFileFormInput = (
|
||||
output_variable_name = '',
|
||||
): FileFormInput => ({
|
||||
type: InputVarType.singleFile,
|
||||
output_variable_name,
|
||||
allowed_file_extensions: [],
|
||||
allowed_file_types: ['image'],
|
||||
allowed_file_upload_methods: ['local_file', 'remote_url'],
|
||||
})
|
||||
|
||||
export const createDefaultFileListFormInput = (
|
||||
output_variable_name = '',
|
||||
): FileListFormInput => ({
|
||||
type: InputVarType.multiFiles,
|
||||
output_variable_name,
|
||||
allowed_file_extensions: [],
|
||||
allowed_file_types: ['image'],
|
||||
allowed_file_upload_methods: ['local_file', 'remote_url'],
|
||||
max_upload_count: 5,
|
||||
})
|
||||
|
||||
export const createDefaultFormInputByType = (
|
||||
type: FormInputItem['type'],
|
||||
output_variable_name = '',
|
||||
): FormInputItem => {
|
||||
switch (type) {
|
||||
case InputVarType.select:
|
||||
return createDefaultSelectFormInput(output_variable_name)
|
||||
case InputVarType.singleFile:
|
||||
return createDefaultFileFormInput(output_variable_name)
|
||||
case InputVarType.multiFiles:
|
||||
return createDefaultFileListFormInput(output_variable_name)
|
||||
case InputVarType.paragraph:
|
||||
default:
|
||||
return createDefaultParagraphFormInput(output_variable_name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"nodes.humanInput.formContent.preview": "Preview",
|
||||
"nodes.humanInput.formContent.title": "Form Content",
|
||||
"nodes.humanInput.formContent.tooltip": "What users will see after opening the form. Supports Markdown formatting.",
|
||||
"nodes.humanInput.insertInputField.fieldType": "Field Type",
|
||||
"nodes.humanInput.insertInputField.insert": "Insert",
|
||||
"nodes.humanInput.insertInputField.prePopulateField": "Pre-populate Field",
|
||||
"nodes.humanInput.insertInputField.prePopulateFieldPlaceholder": "Add <staticContent/> or <variable/> users will see this content initially, or leave empty.",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user