refactor(web): migrate runtime variable inputs (#41739)

Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
yyh 2026-09-03 09:38:39 +00:00 committed by GitHub
parent aee0062cb2
commit 4beffa97e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 118 additions and 188 deletions

View File

@ -368,14 +368,6 @@
"count": 1
}
},
"web/app/components/app/configuration/debug/chat-user-input.tsx": {
"jsx-a11y/no-autofocus": {
"count": 2
},
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx": {
"typescript/no-explicit-any": {
"count": 6
@ -414,14 +406,6 @@
"count": 1
}
},
"web/app/components/app/configuration/prompt-value-panel/index.tsx": {
"jsx-a11y/no-autofocus": {
"count": 2
},
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/create-app-dialog/app-list/sidebar.tsx": {
"erasable-syntax-only/enums": {
"count": 1
@ -464,11 +448,6 @@
"count": 1
}
},
"web/app/components/app/overview/workflow-hidden-input-fields.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/text-generate/item/index.tsx": {
"typescript/no-explicit-any": {
"count": 3
@ -625,9 +604,6 @@
}
},
"web/app/components/base/chat/chat-with-history/inputs-form/content.tsx": {
"no-restricted-imports": {
"count": 1
},
"typescript/no-explicit-any": {
"count": 3
}
@ -715,9 +691,6 @@
}
},
"web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx": {
"no-restricted-imports": {
"count": 1
},
"typescript/no-explicit-any": {
"count": 3
}

View File

@ -11,37 +11,6 @@ vi.mock('use-context-selector', () => ({
createContext: vi.fn(() => ({})),
}))
vi.mock('@/app/components/base/input', () => ({
default: ({
value,
onChange,
placeholder,
autoFocus,
maxLength,
readOnly,
type,
}: {
value: string
onChange: (e: { target: { value: string } }) => void
placeholder?: string
autoFocus?: boolean
maxLength?: number
readOnly?: boolean
type?: string
}) => (
<input
data-testid={`input-${placeholder}`}
data-autofocus={autoFocus ? 'true' : undefined}
type={type || 'text'}
value={value}
onChange={onChange}
placeholder={placeholder}
maxLength={maxLength}
readOnly={readOnly}
/>
),
}))
vi.mock('@/app/components/workflow/nodes/_base/components/before-run-form/bool-input', () => ({
default: ({
name,
@ -172,7 +141,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Name')).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: 'Name' })).toBeInTheDocument()
})
it('should render paragraph input type', () => {
@ -204,7 +173,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
const select = screen.getByRole('combobox')
const select = screen.getByRole('combobox', { name: 'Choice' })
await user.click(select)
expect(await screen.findByRole('option', { name: 'A' })).toBeInTheDocument()
expect(screen.getByRole('option', { name: 'B' })).toBeInTheDocument()
@ -221,7 +190,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
const input = screen.getByTestId('input-Count')
const input = screen.getByRole('spinbutton', { name: 'Count' })
expect(input).toBeInTheDocument()
expect(input).toHaveAttribute('type', 'number')
})
@ -256,9 +225,9 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Name')).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: 'Name' })).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument()
expect(screen.getByRole('combobox')).toBeInTheDocument()
expect(screen.getByRole('combobox', { name: 'Choice' })).toBeInTheDocument()
})
it('should show optional label for non-required fields', () => {
@ -313,7 +282,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{ name: 'John' }} />)
expect(screen.getByTestId('input-Name')).toHaveValue('John')
expect(screen.getByRole('textbox', { name: 'Name' })).toHaveValue('John')
})
it('should display existing input values for paragraph type', () => {
@ -340,7 +309,7 @@ describe('ChatUserInput', () => {
render(<ChatUserInput inputs={{ count: 42 }} />)
// Number type input still uses string value internally
expect(screen.getByTestId('input-Count')).toHaveValue(42)
expect(screen.getByRole('spinbutton', { name: 'Count' })).toHaveValue(42)
})
it('should display checkbox as checked when value is truthy', () => {
@ -381,7 +350,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{ name: '' }} />)
expect(screen.getByTestId('input-Name')).toHaveValue('')
expect(screen.getByRole('textbox', { name: 'Name' })).toHaveValue('')
})
it('should handle undefined values', () => {
@ -394,7 +363,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Name')).toHaveValue('')
expect(screen.getByRole('textbox', { name: 'Name' })).toHaveValue('')
})
})
@ -409,7 +378,9 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
fireEvent.change(screen.getByTestId('input-Name'), { target: { value: 'New Value' } })
fireEvent.change(screen.getByRole('textbox', { name: 'Name' }), {
target: { value: 'New Value' },
})
expect(mockSetInputs).toHaveBeenCalledWith({ name: 'New Value' })
})
@ -447,7 +418,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{ choice: 'A' }} />)
await user.click(screen.getByRole('combobox'))
await user.click(screen.getByRole('combobox', { name: 'Choice' }))
await user.click(await screen.findByRole('option', { name: 'B' }))
expect(mockSetInputs).toHaveBeenCalledWith({ choice: 'B' })
@ -463,7 +434,9 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
fireEvent.change(screen.getByTestId('input-Count'), { target: { value: '100' } })
fireEvent.change(screen.getByRole('spinbutton', { name: 'Count' }), {
target: { value: '100' },
})
expect(mockSetInputs).toHaveBeenCalledWith({ count: '100' })
})
@ -508,7 +481,9 @@ describe('ChatUserInput', () => {
render(<ChatUserInput inputs={{}} />)
fireEvent.change(screen.getByTestId('input-Name'), { target: { value: 'Valid' } })
fireEvent.change(screen.getByRole('textbox', { name: 'Name' }), {
target: { value: 'Valid' },
})
expect(mockSetInputs).not.toHaveBeenCalled()
})
@ -527,7 +502,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Name')).not.toHaveAttribute('readonly')
expect(screen.getByRole('textbox', { name: 'Name' })).not.toHaveAttribute('readonly')
})
it('should set string input as readonly when test/run is denied even if configuration is editable', () => {
@ -542,7 +517,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Name')).toHaveAttribute('readonly')
expect(screen.getByRole('textbox', { name: 'Name' })).toHaveAttribute('readonly')
})
it('should set string input as readonly when configuration is readonly and test/run is denied', () => {
@ -557,7 +532,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Name')).toHaveAttribute('readonly')
expect(screen.getByRole('textbox', { name: 'Name' })).toHaveAttribute('readonly')
})
it('should set paragraph input as readonly when configuration is readonly and test/run is denied', () => {
@ -592,7 +567,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByRole('combobox')).toBeDisabled()
expect(screen.getByRole('combobox', { name: 'Choice' })).toBeDisabled()
})
it('should disable checkbox when configuration is readonly and test/run is denied', () => {
@ -718,39 +693,8 @@ describe('ChatUserInput', () => {
})
})
describe('AutoFocus', () => {
it('should set autoFocus on first string input', () => {
mockUseContext.mockReturnValue(
createContextValue({
modelConfig: createModelConfig([
createPromptVariable({ key: 'first', name: 'First', type: 'string' }),
createPromptVariable({ key: 'second', name: 'Second', type: 'string' }),
]),
}),
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-First')).toHaveAttribute('data-autofocus', 'true')
expect(screen.getByTestId('input-Second')).not.toHaveAttribute('data-autofocus')
})
it('should set autoFocus on first number input when it is the first field', () => {
mockUseContext.mockReturnValue(
createContextValue({
modelConfig: createModelConfig([
createPromptVariable({ key: 'count', name: 'Count', type: 'number' }),
createPromptVariable({ key: 'name', name: 'Name', type: 'string' }),
]),
}),
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Count')).toHaveAttribute('data-autofocus', 'true')
})
})
describe('MaxLength', () => {
it('should pass maxLength to string input', () => {
describe('Input constraints', () => {
it('preserves the text length constraint', () => {
mockUseContext.mockReturnValue(
createContextValue({
modelConfig: createModelConfig([
@ -760,20 +704,8 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Name')).toHaveAttribute('maxLength', '50')
})
it('should pass maxLength to number input', () => {
mockUseContext.mockReturnValue(
createContextValue({
modelConfig: createModelConfig([
createPromptVariable({ key: 'count', name: 'Count', type: 'number', max_length: 10 }),
]),
}),
)
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Count')).toHaveAttribute('maxLength', '10')
expect(screen.getByRole('textbox', { name: 'Name' })).toHaveAttribute('maxLength', '50')
})
})
@ -789,7 +721,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{}} />)
const select = screen.getByRole('combobox')
const select = screen.getByRole('combobox', { name: 'Choice' })
await user.click(select)
expect(screen.queryAllByRole('option')).toHaveLength(0)
})
@ -805,7 +737,9 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{ name: 'Existing', desc: 'Also Existing' }} />)
fireEvent.change(screen.getByTestId('input-Name'), { target: { value: 'Updated' } })
fireEvent.change(screen.getByRole('textbox', { name: 'Name' }), {
target: { value: 'Updated' },
})
expect(mockSetInputs).toHaveBeenCalledWith({
name: 'Updated',
@ -823,7 +757,7 @@ describe('ChatUserInput', () => {
)
render(<ChatUserInput inputs={{ value: 123 as unknown as string }} />)
expect(screen.getByTestId('input-Value')).toHaveValue('123')
expect(screen.getByRole('textbox', { name: 'Value' })).toHaveValue('123')
})
it('should not hide label for checkbox type', () => {

View File

@ -1,5 +1,6 @@
import type { Inputs } from '@/models/debug'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import {
Select,
SelectContent,
@ -11,10 +12,9 @@ import {
} from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useEffect } from 'react'
import { useEffect, useId } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import Input from '@/app/components/base/input'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import ConfigContext from '@/context/debug-configuration'
@ -24,6 +24,7 @@ type Props = Readonly<{
const ChatUserInput = ({ inputs }: Props) => {
const { t } = useTranslation()
const baseId = useId()
const { modelConfig, setInputs, canTestAndRun = false } = useContext(ConfigContext)
const debugInputReadonly = !canTestAndRun
@ -81,12 +82,14 @@ const ChatUserInput = ({ inputs }: Props) => {
)}
>
<div className="px-4 pt-3 pb-4">
{promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="mb-4 last-of-type:mb-0">
<div>
{type !== 'checkbox' && (
<div className="mb-1 flex h-6 items-center gap-1 system-sm-semibold text-text-secondary">
<div className="truncate">{name || key}</div>
<div id={`${baseId}-${key}-label`} className="truncate">
{name || key}
</div>
{!required && (
<span className="system-xs-regular text-text-tertiary">
{t(($) => $['panel.optional'], { ns: 'workflow' })}
@ -97,12 +100,10 @@ const ChatUserInput = ({ inputs }: Props) => {
<div className="grow">
{type === 'string' && (
<Input
aria-labelledby={`${baseId}-${key}-label`}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => {
handleInputValueChange(key, e.target.value)
}}
onValueChange={(value) => handleInputValueChange(key, value)}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length}
readOnly={debugInputReadonly}
/>
@ -110,7 +111,7 @@ const ChatUserInput = ({ inputs }: Props) => {
{type === 'paragraph' && (
<Textarea
className="h-30 grow"
aria-label={name || key}
aria-labelledby={`${baseId}-${key}-label`}
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
onValueChange={(value) => {
@ -130,7 +131,7 @@ const ChatUserInput = ({ inputs }: Props) => {
handleInputValueChange(key, nextValue)
}}
>
<SelectTrigger className="w-full">
<SelectTrigger aria-labelledby={`${baseId}-${key}-label`} className="w-full">
<SelectValue
placeholder={t(($) => $['placeholder.select'], { ns: 'common' })}
/>
@ -147,13 +148,11 @@ const ChatUserInput = ({ inputs }: Props) => {
)}
{type === 'number' && (
<Input
aria-labelledby={`${baseId}-${key}-label`}
type="number"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => {
handleInputValueChange(key, e.target.value)
}}
onValueChange={(value) => handleInputValueChange(key, value)}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length}
readOnly={debugInputReadonly}
/>

View File

@ -132,7 +132,7 @@ describe('PromptValuePanel', () => {
it('updates inputs, clears values, and triggers run when ready', async () => {
renderPanel()
const textInput = screen.getByPlaceholderText('Text Var')
const textInput = screen.getByRole('textbox', { name: 'Text Var' })
fireEvent.change(textInput, { target: { value: 'updated' } })
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({ textVar: 'updated' }))
@ -258,10 +258,10 @@ describe('PromptValuePanel', () => {
},
})
fireEvent.change(screen.getByPlaceholderText('Paragraph Var'), {
fireEvent.change(screen.getByRole('textbox', { name: 'Paragraph Var' }), {
target: { value: 'updated paragraph' },
})
await user.click(screen.getByRole('combobox'))
await user.click(screen.getByRole('combobox', { name: 'Select Var' }))
await user.click(await screen.findByRole('option', { name: 'b' }))
fireEvent.change(screen.getByDisplayValue('1'), { target: { value: '2' } })
fireEvent.click(screen.getByText('bool-input'))
@ -311,7 +311,9 @@ describe('PromptValuePanel', () => {
},
})
fireEvent.change(screen.getByPlaceholderText('Text Var'), { target: { value: 'ignored' } })
fireEvent.change(screen.getByRole('textbox', { name: 'Text Var' }), {
target: { value: 'ignored' },
})
expect(mockSetInputs).not.toHaveBeenCalled()
})
@ -338,7 +340,7 @@ describe('PromptValuePanel', () => {
})
expect(screen.getByText('common.placeholder.select')).toBeInTheDocument()
expect(screen.getByPlaceholderText('Number Var')).toHaveValue(null)
expect(screen.getByRole('spinbutton', { name: 'Number Var' })).toHaveValue(null)
expect(screen.queryAllByRole('option')).toHaveLength(0)
})
@ -400,7 +402,7 @@ describe('PromptValuePanel', () => {
},
})
expect(screen.getByPlaceholderText('Text Var')).toHaveAttribute('readonly')
expect(screen.getByRole('textbox', { name: 'Text Var' })).toHaveAttribute('readonly')
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toBeDisabled()
})
@ -413,7 +415,7 @@ describe('PromptValuePanel', () => {
},
})
expect(screen.getByPlaceholderText('Text Var')).toHaveAttribute('readonly')
expect(screen.getByRole('textbox', { name: 'Text Var' })).toHaveAttribute('readonly')
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toBeDisabled()
expect(screen.getByRole('button', { name: 'appDebug.inputs.run' })).toBeDisabled()
})

View File

@ -4,6 +4,7 @@ import type { Inputs } from '@/models/debug'
import type { VisionFile, VisionSettings } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import {
Select,
SelectContent,
@ -17,13 +18,12 @@ import { Textarea } from '@langgenius/dify-ui/textarea'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiArrowDownSLine, RiArrowRightSLine, RiPlayLargeFill } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useId, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useStore as useAppStore } from '@/app/components/app/store'
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import Input from '@/app/components/base/input'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import ConfigContext from '@/context/debug-configuration'
import { AppModeEnum, ModelModeType } from '@/types/app'
@ -44,6 +44,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
onVisionFilesChange,
}) => {
const { t } = useTranslation()
const baseId = useId()
const {
readonly,
canTestAndRun = false,
@ -159,12 +160,14 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
</div>
{!userInputFieldCollapse && promptVariables.length > 0 && (
<div className="px-4 pt-3 pb-4">
{promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
<div key={key} className="mb-4 last-of-type:mb-0">
<div>
{type !== 'checkbox' && (
<div className="mb-1 flex h-6 items-center gap-1 system-sm-semibold text-text-secondary">
<div className="truncate">{name || key}</div>
<div id={`${baseId}-${key}-label`} className="truncate">
{name || key}
</div>
{!required && (
<span className="system-xs-regular text-text-tertiary">
{t(($) => $['panel.optional'], { ns: 'workflow' })}
@ -175,19 +178,17 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
<div className="grow">
{type === 'string' && (
<Input
aria-labelledby={`${baseId}-${key}-label`}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => {
handleInputValueChange(key, e.target.value)
}}
onValueChange={(value) => handleInputValueChange(key, value)}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length}
readOnly={debugInputReadonly}
/>
)}
{type === 'paragraph' && (
<Textarea
aria-label={name}
aria-labelledby={`${baseId}-${key}-label`}
className="h-30 grow"
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
@ -208,7 +209,10 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
handleInputValueChange(key, nextValue)
}}
>
<SelectTrigger className="w-full bg-gray-50">
<SelectTrigger
aria-labelledby={`${baseId}-${key}-label`}
className="w-full bg-gray-50"
>
<SelectValue
placeholder={t(($) => $['placeholder.select'], { ns: 'common' })}
/>
@ -225,13 +229,11 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
)}
{type === 'number' && (
<Input
aria-labelledby={`${baseId}-${key}-label`}
type="number"
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => {
handleInputValueChange(key, e.target.value)
}}
onValueChange={(value) => handleInputValueChange(key, value)}
placeholder={name}
autoFocus={index === 0}
maxLength={max_length}
readOnly={debugInputReadonly}
/>

View File

@ -1,5 +1,6 @@
import type { ChangeEvent } from 'react'
import type { WorkflowHiddenStartVariable, WorkflowLaunchInputValue } from './app-card-utils'
import { Input } from '@langgenius/dify-ui/input'
import {
Select,
SelectContent,
@ -8,7 +9,6 @@ import {
SelectValue,
} from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import Input from '@/app/components/base/input'
import { InputVarType } from '@/app/components/workflow/types'
type WorkflowHiddenInputFieldsProps = {
@ -88,9 +88,7 @@ const WorkflowHiddenInputFields = ({
id={fieldId}
type={variable.type === InputVarType.number ? 'number' : 'text'}
value={typeof fieldValue === 'string' ? fieldValue : ''}
onChange={(event: ChangeEvent<HTMLInputElement>) =>
onValueChange(variable.variable, event.target.value)
}
onValueChange={(value) => onValueChange(variable.variable, value)}
placeholder={label}
maxLength={variable.max_length}
/>

View File

@ -216,7 +216,7 @@ describe('InputsFormContent', () => {
it('uses currentConversationInputs when currentConversationId is present', () => {
const context = createMockContext()
renderWithContext(<InputsFormContent />, context)
const input = screen.getByPlaceholderText('Text Label') as HTMLInputElement
const input = screen.getByRole('textbox', { name: 'Text Label' }) as HTMLInputElement
expect(input.value).toBe('current-value')
})
@ -227,7 +227,7 @@ describe('InputsFormContent', () => {
})
renderWithContext(<InputsFormContent />, context)
const input = screen.getByPlaceholderText('Text Label') as HTMLInputElement
const input = screen.getByRole('textbox', { name: 'Text Label' }) as HTMLInputElement
expect(input.value).toBe('new-value')
})
@ -235,7 +235,7 @@ describe('InputsFormContent', () => {
const user = userEvent.setup()
const context = createMockContext()
renderWithContext(<InputsFormContent />, context)
const input = screen.getByPlaceholderText('Text Label') as HTMLInputElement
const input = screen.getByRole('textbox', { name: 'Text Label' }) as HTMLInputElement
await user.clear(input)
await user.type(input, 'updated')
@ -256,7 +256,7 @@ describe('InputsFormContent', () => {
})
renderWithContext(<InputsFormContent />, context)
const input = screen.getByPlaceholderText('Num') as HTMLInputElement
const input = screen.getByRole('spinbutton', { name: 'Num' }) as HTMLInputElement
expect(input).toHaveAttribute('type', 'number')
await user.type(input, '123')
@ -274,7 +274,7 @@ describe('InputsFormContent', () => {
})
renderWithContext(<InputsFormContent />, context)
const textarea = screen.getByPlaceholderText('Para') as HTMLTextAreaElement
const textarea = screen.getByRole('textbox', { name: 'Para' }) as HTMLTextAreaElement
await user.type(textarea, 'hello')
expect(mockSetCurrentConversationInputs).toHaveBeenLastCalledWith(

View File

@ -1,3 +1,4 @@
import { Input } from '@langgenius/dify-ui/input'
import {
Select,
SelectContent,
@ -9,10 +10,9 @@ import {
} from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { memo, useCallback } from 'react'
import { memo, useCallback, useId } from 'react'
import { useTranslation } from 'react-i18next'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import Input from '@/app/components/base/input'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -25,6 +25,7 @@ type Props = Readonly<{
const InputsFormContent = ({ showTip }: Props) => {
const { t } = useTranslation()
const baseId = useId()
const {
appParams,
inputsForms,
@ -64,7 +65,12 @@ const InputsFormContent = ({ showTip }: Props) => {
<div key={form.variable} className="space-y-1">
{form.type !== InputVarType.checkbox && (
<div className="flex h-6 items-center gap-1">
<div className="system-md-semibold text-text-secondary">{form.label}</div>
<div
id={`${baseId}-${form.variable}-label`}
className="system-md-semibold text-text-secondary"
>
{form.label}
</div>
{!form.required && (
<div className="system-xs-regular text-text-tertiary">
{t(($) => $['panel.optional'], { ns: 'workflow' })}
@ -74,22 +80,24 @@ const InputsFormContent = ({ showTip }: Props) => {
)}
{form.type === InputVarType.textInput && (
<Input
aria-labelledby={`${baseId}-${form.variable}-label`}
value={inputsFormValue?.[form.variable] || ''}
onChange={(e) => handleFormChange(form.variable, e.target.value)}
onValueChange={(value) => handleFormChange(form.variable, value)}
placeholder={form.label}
/>
)}
{form.type === InputVarType.number && (
<Input
aria-labelledby={`${baseId}-${form.variable}-label`}
type="number"
value={inputsFormValue?.[form.variable] || ''}
onChange={(e) => handleFormChange(form.variable, e.target.value)}
onValueChange={(value) => handleFormChange(form.variable, value)}
placeholder={form.label}
/>
)}
{form.type === InputVarType.paragraph && (
<Textarea
aria-label={form.label}
aria-labelledby={`${baseId}-${form.variable}-label`}
value={inputsFormValue?.[form.variable] || ''}
onValueChange={(value) => handleFormChange(form.variable, value)}
placeholder={form.label}
@ -108,7 +116,10 @@ const InputsFormContent = ({ showTip }: Props) => {
value={(inputsFormValue?.[form.variable] ?? form.default ?? '') || null}
onValueChange={(value) => value && handleFormChange(form.variable, value)}
>
<SelectTrigger className="w-full">
<SelectTrigger
aria-labelledby={`${baseId}-${form.variable}-label`}
className="w-full"
>
<SelectValue placeholder={form.label} />
</SelectTrigger>
<SelectContent>

View File

@ -165,8 +165,8 @@ describe('InputsFormContent', () => {
it('should handle text input changes', async () => {
render(<InputsFormContent />)
const inputs = screen.getAllByPlaceholderText('Text Label')
await user.type(inputs[0]!, 'hello')
const input = screen.getByRole('textbox', { name: 'Text Label' })
await user.type(input, 'hello')
expect(mockContextValue.setCurrentConversationInputs).toHaveBeenCalled()
expect(mockContextValue.handleNewConversationInputsChange).toHaveBeenCalled()
@ -174,8 +174,8 @@ describe('InputsFormContent', () => {
it('should handle number input changes', async () => {
render(<InputsFormContent />)
const inputs = screen.getAllByPlaceholderText('Number Label')
await user.type(inputs[0]!, '123')
const input = screen.getByRole('spinbutton', { name: 'Number Label' })
await user.type(input, '123')
expect(mockContextValue.setCurrentConversationInputs).toHaveBeenCalled()
expect(mockContextValue.handleNewConversationInputsChange).toHaveBeenCalled()
@ -183,8 +183,8 @@ describe('InputsFormContent', () => {
it('should handle paragraph input changes', async () => {
render(<InputsFormContent />)
const inputs = screen.getAllByPlaceholderText('Paragraph Label')
await user.type(inputs[0]!, 'long text')
const input = screen.getByRole('textbox', { name: 'Paragraph Label' })
await user.type(input, 'long text')
expect(mockContextValue.setCurrentConversationInputs).toHaveBeenCalled()
expect(mockContextValue.handleNewConversationInputsChange).toHaveBeenCalled()

View File

@ -1,3 +1,4 @@
import { Input } from '@langgenius/dify-ui/input'
import {
Select,
SelectContent,
@ -9,10 +10,9 @@ import {
} from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { memo, useCallback } from 'react'
import { memo, useCallback, useId } from 'react'
import { useTranslation } from 'react-i18next'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import Input from '@/app/components/base/input'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -25,6 +25,7 @@ type Props = Readonly<{
const InputsFormContent = ({ showTip }: Props) => {
const { t } = useTranslation()
const baseId = useId()
const {
appParams,
inputsForms,
@ -68,7 +69,12 @@ const InputsFormContent = ({ showTip }: Props) => {
>
{form.type !== InputVarType.checkbox && (
<div className="flex h-6 items-center gap-1">
<div className="system-md-semibold text-text-secondary">{form.label}</div>
<div
id={`${baseId}-${form.variable}-label`}
className="system-md-semibold text-text-secondary"
>
{form.label}
</div>
{!form.required && (
<div className="system-xs-regular text-text-tertiary">
{t(($) => $['panel.optional'], { ns: 'workflow' })}
@ -78,22 +84,24 @@ const InputsFormContent = ({ showTip }: Props) => {
)}
{form.type === InputVarType.textInput && (
<Input
aria-labelledby={`${baseId}-${form.variable}-label`}
value={inputsFormValue?.[form.variable] || ''}
onChange={(e) => handleFormChange(form.variable, e.target.value)}
onValueChange={(value) => handleFormChange(form.variable, value)}
placeholder={form.label}
/>
)}
{form.type === InputVarType.number && (
<Input
aria-labelledby={`${baseId}-${form.variable}-label`}
type="number"
value={inputsFormValue?.[form.variable] || ''}
onChange={(e) => handleFormChange(form.variable, e.target.value)}
onValueChange={(value) => handleFormChange(form.variable, value)}
placeholder={form.label}
/>
)}
{form.type === InputVarType.paragraph && (
<Textarea
aria-label={form.label}
aria-labelledby={`${baseId}-${form.variable}-label`}
value={inputsFormValue?.[form.variable] || ''}
onValueChange={(value) => handleFormChange(form.variable, value)}
placeholder={form.label}
@ -112,7 +120,10 @@ const InputsFormContent = ({ showTip }: Props) => {
value={(inputsFormValue?.[form.variable] ?? form.default ?? '') || null}
onValueChange={(value) => value && handleFormChange(form.variable, value)}
>
<SelectTrigger className="w-full">
<SelectTrigger
aria-labelledby={`${baseId}-${form.variable}-label`}
className="w-full"
>
<SelectValue placeholder={form.label} />
</SelectTrigger>
<SelectContent>