mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
refactor(web): migrate runtime variable inputs (#41739)
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
parent
aee0062cb2
commit
4beffa97e0
@ -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
|
||||
}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user