Fix human input typing regressions

This commit is contained in:
JzoNg 2026-04-22 08:01:54 +08:00
parent 85d05f5113
commit c2fd595a82
12 changed files with 91 additions and 68 deletions

View File

@ -20,6 +20,7 @@ import ExpirationTime from '@/app/components/base/chat/chat/answer/human-input-c
import { getButtonStyle } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
import Loading from '@/app/components/base/loading'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { isParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types'
import useDocumentTitle from '@/hooks/use-document-title'
import { useParams } from '@/next/navigation'
import { useGetHumanInputForm, useSubmitHumanInputForm } from '@/service/use-share'
@ -67,7 +68,14 @@ const FormContent = () => {
return
const initialInputs: Record<string, string> = {}
formData.inputs.forEach((item) => {
initialInputs[item.output_variable_name] = item.default.type === 'variable' ? formData.resolved_default_values[item.output_variable_name] || '' : item.default.value
if (isParagraphFormInput(item)) {
initialInputs[item.output_variable_name] = item.default.type === 'variable'
? formData.resolved_default_values[item.output_variable_name] || ''
: item.default.value
return
}
initialInputs[item.output_variable_name] = ''
})
setInputs(initialInputs)
}, [formData?.inputs, formData?.resolved_default_values])

View File

@ -91,12 +91,12 @@ describe('ContentItem', () => {
content="{{#$output.user_bio#}}"
formInputFields={[
{
type: 'text-input',
type: 'select',
output_variable_name: 'user_bio',
default: {
option_source: {
type: 'constant',
value: '',
selector: [],
value: [],
},
} as FormInputItem,
]}

View File

@ -114,14 +114,16 @@ describe('HumanInputForm', () => {
...mockFormData,
inputs: [
{
type: 'text-input',
type: 'select',
output_variable_name: 'field2',
default: { type: 'variable', value: '', selector: [] },
option_source: { type: 'variable', value: [], selector: [] },
} as FormInputItem,
{
type: 'number',
type: 'file',
output_variable_name: 'field3',
default: { type: 'constant', value: '0', selector: [] },
allowed_file_extensions: [],
allowed_file_types: [],
allowed_file_upload_methods: [],
} as FormInputItem,
],
resolved_default_values: { field2: 'default value' },

View File

@ -1,5 +1,6 @@
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { Locale } from '@/i18n-config'
import type { HumanInputResolvedValue } from '@/types/workflow'
import dayjs from 'dayjs'
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
import relativeTime from 'dayjs/plugin/relativeTime'
@ -34,13 +35,18 @@ export const splitByOutputVar = (content: string): string[] => {
return parts.filter(part => part.length > 0)
}
export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Record<string, string> = {}) => {
const initialInputs: Record<string, string | undefined> = {}
export const initializeInputs = (formInputs: FormInputItem[], defaultValues: Record<string, HumanInputResolvedValue> = {}) => {
const initialInputs: Record<string, string> = {}
formInputs.forEach((item) => {
if (isParagraphFormInput(item))
initialInputs[item.output_variable_name] = item.default.type === 'variable' ? defaultValues[item.output_variable_name] || '' : item.default.value
else
initialInputs[item.output_variable_name] = undefined
if (isParagraphFormInput(item)) {
const resolvedValue = defaultValues[item.output_variable_name]
initialInputs[item.output_variable_name] = item.default.type === 'variable' && typeof resolvedValue === 'string'
? resolvedValue
: item.default.value
return
}
initialInputs[item.output_variable_name] = ''
})
return initialInputs
}

View File

@ -1,15 +1,16 @@
import type { ComponentProps } from 'react'
import type { WorkflowNodesMap } from '../../workflow-variable-block/node'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { FormInputItem, ParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types'
import type { ValueSelector } from '@/app/components/workflow/types'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { cleanup, fireEvent, render } from '@testing-library/react'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
import { BlockEnum, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import HITLInputComponentUI from '../component-ui'
import { HITLInputNode } from '../node'
const createFormInput = (overrides?: Partial<FormInputItem>): FormInputItem => ({
const createParagraphFormInput = (overrides?: Partial<ParagraphFormInput>): ParagraphFormInput => ({
type: InputVarType.paragraph,
output_variable_name: 'customer_name',
default: {
@ -93,7 +94,7 @@ describe('HITLInputComponentUI', () => {
const selector = ['node-2', 'answer'] as ValueSelector
const { getByText } = renderComponent({
formInput: createFormInput({
formInput: createParagraphFormInput({
default: {
type: 'variable',
selector,
@ -114,14 +115,15 @@ describe('HITLInputComponentUI', () => {
it('should render select option summary for constant options', () => {
const { getByText } = renderComponent({
formInput: createFormInput({
formInput: {
type: InputVarType.select,
output_variable_name: 'customer_name',
option_source: {
type: 'constant',
selector: [],
value: ['alpha', 'beta'],
},
}),
} satisfies FormInputItem,
})
expect(getByText('alpha, beta')).toBeInTheDocument()
@ -129,13 +131,14 @@ describe('HITLInputComponentUI', () => {
it('should render file-list summary with max uploads', () => {
const { getByText } = renderComponent({
formInput: createFormInput({
formInput: {
type: InputVarType.multiFiles,
output_variable_name: 'customer_name',
allowed_file_extensions: ['.pdf'],
allowed_file_types: ['document'],
allowed_file_upload_methods: ['local_file'],
allowed_file_types: [SupportUploadFileTypes.document],
allowed_file_upload_methods: [TransferMethod.local_file],
max_upload_count: 4,
}),
} satisfies FormInputItem,
})
expect(getByText(/document/)).toBeInTheDocument()
@ -240,7 +243,7 @@ describe('HITLInputComponentUI', () => {
it('should render variable selector when workflowNodesMap fallback is used', () => {
const { getByText } = renderComponent({
workflowNodesMap: undefined as unknown as WorkflowNodesMap,
formInput: createFormInput({
formInput: createParagraphFormInput({
default: {
type: 'variable',
selector: ['node-2', 'answer'] as ValueSelector,

View File

@ -1,5 +1,5 @@
import type { RefObject } from 'react'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { FormInputItem, ParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { InputVarType } from '@/app/components/workflow/types'
@ -15,15 +15,17 @@ vi.mock('../../../hooks', () => ({
vi.mock('../component-ui', () => ({
default: ({ formInput, onChange }: { formInput?: FormInputItem, onChange: (payload: FormInputItem) => void }) => {
const basePayload: FormInputItem = formInput ?? {
type: InputVarType.paragraph,
output_variable_name: 'user_name',
default: {
type: 'constant',
selector: [],
value: 'hello',
},
}
const basePayload: ParagraphFormInput = (formInput && formInput.type === InputVarType.paragraph
? formInput
: {
type: InputVarType.paragraph,
output_variable_name: 'user_name',
default: {
type: 'constant',
selector: [],
value: 'hello',
},
}) satisfies ParagraphFormInput
return (
<div>
<button
@ -63,7 +65,7 @@ const createHookReturn = (): [RefObject<HTMLDivElement | null>, boolean] => {
return [{ current: null }, false]
}
const createInput = (overrides?: Partial<FormInputItem>): FormInputItem => ({
const createInput = (overrides?: Partial<ParagraphFormInput>): ParagraphFormInput => ({
type: InputVarType.paragraph,
output_variable_name: 'user_name',
default: {

View File

@ -1,7 +1,8 @@
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { FormInputItem, ParagraphFormInput } from '@/app/components/workflow/nodes/human-input/types'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { InputVarType, VarType } from '@/app/components/workflow/types'
import { InputVarType, SupportUploadFileTypes, VarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import InputField from '../input-field'
type VarReferencePickerProps = {
@ -55,16 +56,16 @@ vi.mock('@/app/components/workflow/nodes/_base/components/file-upload-setting',
__esModule: true,
default: ({ onChange }: { onChange: (payload: {
allowed_file_extensions: string[]
allowed_file_types: string[]
allowed_file_upload_methods: string[]
allowed_file_types: SupportUploadFileTypes[]
allowed_file_upload_methods: TransferMethod[]
max_length?: number
}) => void }) => (
<button
type="button"
onClick={() => onChange({
allowed_file_extensions: ['.pdf'],
allowed_file_types: ['document'],
allowed_file_upload_methods: ['local_file'],
allowed_file_types: [SupportUploadFileTypes.document],
allowed_file_upload_methods: [TransferMethod.local_file],
max_length: 4,
})}
>
@ -73,7 +74,7 @@ vi.mock('@/app/components/workflow/nodes/_base/components/file-upload-setting',
),
}))
const createPayload = (overrides?: Partial<FormInputItem>): FormInputItem => ({
const createPayload = (overrides?: Partial<ParagraphFormInput>): ParagraphFormInput => ({
type: InputVarType.paragraph,
output_variable_name: 'valid_name',
default: {

View File

@ -21,6 +21,8 @@ import Modal from '../../../modal'
import InputField from './input-field'
import VariableBlock from './variable-block'
const i18nPrefix = 'nodes.humanInput.insertInputField'
type HITLInputComponentUIProps = {
nodeId: string
varName: string

View File

@ -1,6 +1,6 @@
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 type { UploadFileSetting, ValueSelector } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { produce } from 'immer'
@ -151,36 +151,27 @@ const InputField: React.FC<InputFieldProps> = ({
}
})
}, [])
const handleFilePayloadChange = useCallback((payload: {
allowed_file_extensions: string[]
allowed_file_types: string[]
allowed_file_upload_methods: string[]
}) => {
const handleFilePayloadChange = useCallback((payload: UploadFileSetting) => {
setTempPayload((prev) => {
if (!isFileFormInput(prev))
return prev
return {
...prev,
allowed_file_extensions: payload.allowed_file_extensions,
allowed_file_extensions: payload.allowed_file_extensions || [],
allowed_file_types: payload.allowed_file_types,
allowed_file_upload_methods: payload.allowed_file_upload_methods,
}
})
}, [])
const handleFileListPayloadChange = useCallback((payload: {
allowed_file_extensions: string[]
allowed_file_types: string[]
allowed_file_upload_methods: string[]
max_length?: number
}) => {
const handleFileListPayloadChange = useCallback((payload: UploadFileSetting) => {
setTempPayload((prev) => {
if (!isFileListFormInput(prev))
return prev
return {
...prev,
allowed_file_extensions: payload.allowed_file_extensions,
allowed_file_extensions: payload.allowed_file_extensions || [],
allowed_file_types: payload.allowed_file_types,
allowed_file_upload_methods: payload.allowed_file_upload_methods,
max_upload_count: payload.max_length,
@ -296,7 +287,10 @@ const InputField: React.FC<InputFieldProps> = ({
{isFileFormInput(tempPayload) && (
<div className="mt-4">
<FileUploadSetting
payload={tempPayload}
payload={{
...tempPayload,
max_length: 1,
}}
isMultiple={false}
onChange={handleFilePayloadChange}
/>
@ -307,7 +301,7 @@ const InputField: React.FC<InputFieldProps> = ({
<FileUploadSetting
payload={{
...tempPayload,
max_length: tempPayload.max_upload_count,
max_length: tempPayload.max_upload_count || 5,
}}
isMultiple
onChange={handleFileListPayloadChange}

View File

@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react'
import { InputVarType } from '@/app/components/workflow/types'
import { Note, rehypeNotes, rehypeVariable, Variable } from '../variable-in-markdown'
describe('variable-in-markdown', () => {
@ -87,7 +88,7 @@ describe('variable-in-markdown', () => {
const { rerender } = render(
<Note
input={{
type: 'paragraph',
type: InputVarType.paragraph,
output_variable_name: 'approval',
default: {
type: 'variable',
@ -104,7 +105,7 @@ describe('variable-in-markdown', () => {
rerender(
<Note
input={{
type: 'paragraph',
type: InputVarType.paragraph,
output_variable_name: 'approval',
default: {
type: 'constant',

View File

@ -1,4 +1,4 @@
import type { FormInputItem, HumanInputNodeType } from '../../types'
import type { HumanInputNodeType, ParagraphFormInput } from '../../types'
import { act, renderHook } from '@testing-library/react'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
import useFormContent from '../use-form-content'
@ -15,7 +15,7 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
default: (...args: unknown[]) => mockUseNodeCrud(...args),
}))
const createFormInput = (overrides: Partial<FormInputItem> = {}): FormInputItem => ({
const createFormInput = (overrides: Partial<ParagraphFormInput> = {}): ParagraphFormInput => ({
type: InputVarType.paragraph,
output_variable_name: 'old_name',
default: {

View File

@ -3,7 +3,11 @@ import type {
UploadFileSetting,
ValueSelector,
} from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types'
import {
InputVarType,
SupportUploadFileTypes,
} from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
export type HumanInputNodeType = CommonNodeType & {
delivery_methods: DeliveryMethod[]
@ -175,8 +179,8 @@ export const createDefaultFileFormInput = (
type: InputVarType.singleFile,
output_variable_name,
allowed_file_extensions: [],
allowed_file_types: ['image'],
allowed_file_upload_methods: ['local_file', 'remote_url'],
allowed_file_types: [SupportUploadFileTypes.image],
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
})
export const createDefaultFileListFormInput = (
@ -185,8 +189,8 @@ export const createDefaultFileListFormInput = (
type: InputVarType.multiFiles,
output_variable_name,
allowed_file_extensions: [],
allowed_file_types: ['image'],
allowed_file_upload_methods: ['local_file', 'remote_url'],
allowed_file_types: [SupportUploadFileTypes.image],
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
max_upload_count: 5,
})