refactor(web): migrate variable configuration inputs (#42179)

This commit is contained in:
yyh authored and GitHub committed 2026-09-11 06:16:12 +00:00
1 parent 10658e428b
commit 2de83835cd
5 files changed
+186 -40

No files matched your search

-3
View File
@@ -308,9 +308,6 @@
"web/app/components/app/configuration/config-var/config-modal/form-fields.tsx": {
"jsx-a11y/anchor-has-content": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/configuration/config-var/config-modal/index.tsx": {
@@ -188,7 +188,7 @@ describe('ConfigModalFormFields', () => {
},
)
it('should update paragraph, number, checkbox, and select defaults', async () => {
it('should update paragraph, checkbox, and select defaults', async () => {
const user = userEvent.setup()
const paragraphProps = createBaseProps()
paragraphProps.tempPayload = {
@@ -200,16 +200,6 @@ describe('ConfigModalFormFields', () => {
fireEvent.change(screen.getByDisplayValue('hello'), { target: { value: 'updated paragraph' } })
expect(paragraphProps.payloadChangeHandlers.default).toHaveBeenCalledWith('updated paragraph')
const numberProps = createBaseProps()
numberProps.tempPayload = {
...numberProps.tempPayload,
type: InputVarType.number,
default: '1',
}
render(<ConfigModalFormFields {...numberProps} />)
fireEvent.change(screen.getByDisplayValue('1'), { target: { value: '2' } })
expect(numberProps.payloadChangeHandlers.default).toHaveBeenCalledWith('2')
const checkboxProps = createBaseProps()
checkboxProps.tempPayload = {
...checkboxProps.tempPayload,
@@ -390,18 +380,7 @@ describe('ConfigModalFormFields', () => {
expect(multiFallbackProps.payloadChangeHandlers.default).toHaveBeenCalledWith(undefined)
})
it('should clear number defaults and skip rendering the default selector when options are missing', () => {
const numberProps = createBaseProps()
numberProps.tempPayload = {
...numberProps.tempPayload,
type: InputVarType.number,
default: '9',
}
render(<ConfigModalFormFields {...numberProps} />)
fireEvent.change(screen.getByDisplayValue('9'), { target: { value: '' } })
expect(numberProps.payloadChangeHandlers.default).toHaveBeenCalledWith(undefined)
it('should skip rendering the default selector when options are missing', () => {
const selectWithoutOptionsProps = createBaseProps()
selectWithoutOptionsProps.tempPayload = {
...selectWithoutOptionsProps.tempPayload,
@@ -486,7 +465,7 @@ describe('ConfigModalFormFields', () => {
}
render(<ConfigModalFormFields {...numberProps} />)
expect(screen.getByRole('spinbutton')).toHaveValue(null)
expect(screen.getByRole('textbox', { name: 'variableConfig.defaultValue' })).toHaveValue('')
})
it('should disable hide checkbox when required is true and disable required when hide is true', () => {
@@ -130,6 +130,155 @@ describe('ConfigModal', () => {
expect(onConfirm).toHaveBeenCalledWith(expect.objectContaining({ default: 'hello' }), undefined)
})
it('should normalize spaces in the variable name and fill an empty label on blur', async () => {
const user = userEvent.setup()
render(
<ConfigModal
isShow
payload={createPayload({ variable: '', label: '' })}
onClose={vi.fn()}
onConfirm={vi.fn()}
/>,
)
const variable = screen.getByRole('textbox', { name: 'appDebug.variableConfig.varName' })
await user.type(variable, 'search query')
await user.tab()
expect(variable).toHaveValue('search_query')
expect(screen.getByRole('textbox', { name: 'appDebug.variableConfig.labelName' })).toHaveValue(
'search_query',
)
})
it.each(['0', 0])(
'should edit a numeric default initialized from %s without losing decimal precision',
async (initialDefault) => {
const user = userEvent.setup()
const onConfirm = vi.fn()
render(
<ConfigModal
isShow
payload={createPayload({
type: InputVarType.number,
label: 'Amount',
default: initialDefault,
})}
onClose={vi.fn()}
onConfirm={onConfirm}
/>,
)
const input = screen.getByRole('textbox', { name: 'appDebug.variableConfig.defaultValue' })
expect(input).toHaveValue('0')
await user.click(screen.getByText('appDebug.variableConfig.defaultValue'))
expect(input).toHaveFocus()
await user.clear(input)
await user.type(input, '-7.123456')
await user.tab()
expect(input).toHaveValue('-7.123456')
await user.click(input)
await user.keyboard('{Enter}')
expect(onConfirm).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ default: -7.123456 }),
undefined,
)
},
)
it.each([
{ option: 'appDebug.variableConfig.text-input string', type: InputVarType.textInput },
{ option: 'appDebug.variableConfig.paragraph string', type: InputVarType.paragraph },
])('should preserve a numeric default when switching to $type', async ({ option, type }) => {
const user = userEvent.setup()
const onConfirm = vi.fn()
render(
<ConfigModal
isShow
payload={createPayload({ type: InputVarType.number, label: 'Amount', default: -7.123456 })}
onClose={vi.fn()}
onConfirm={onConfirm}
/>,
)
await user.click(screen.getByRole('combobox', { name: 'appDebug.variableConfig.fieldType' }))
await user.click(await screen.findByRole('option', { name: option }))
expect(
screen.getByRole('textbox', { name: 'appDebug.variableConfig.defaultValue' }),
).toHaveValue('-7.123456')
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(onConfirm).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ type, default: '-7.123456' }),
undefined,
)
})
it.each([
{ initialDefault: '0', expected: 0, displayed: '0' },
{ initialDefault: 'hello', expected: undefined, displayed: '' },
])(
'should use a numeric or absent default when switching from "$initialDefault" to number',
async ({ initialDefault, expected, displayed }) => {
const user = userEvent.setup()
const onConfirm = vi.fn()
render(
<ConfigModal
isShow
payload={createPayload({ label: 'Question', default: initialDefault })}
onClose={vi.fn()}
onConfirm={onConfirm}
/>,
)
await user.click(screen.getByRole('combobox', { name: 'appDebug.variableConfig.fieldType' }))
await user.click(
await screen.findByRole('option', { name: 'appDebug.variableConfig.number number' }),
)
expect(
screen.getByRole('textbox', { name: 'appDebug.variableConfig.defaultValue' }),
).toHaveValue(displayed)
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(onConfirm).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ type: InputVarType.number, default: expected }),
undefined,
)
},
)
it.each([InputVarType.textInput, InputVarType.number])(
'should save an absent %s default after clearing it',
async (type) => {
const user = userEvent.setup()
const onConfirm = vi.fn()
render(
<ConfigModal
isShow
payload={createPayload({
type,
label: 'Question',
default: type === InputVarType.number ? 9 : 'hello',
})}
onClose={vi.fn()}
onConfirm={onConfirm}
/>,
)
const input = screen.getByRole('textbox', { name: 'appDebug.variableConfig.defaultValue' })
await user.clear(input)
await user.tab()
expect(input).toHaveValue('')
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
expect(onConfirm).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ default: undefined }),
undefined,
)
},
)
it.each([InputVarType.singleFile, InputVarType.multiFiles])(
'should keep the %s configuration open when Enter adds custom extensions',
async (type) => {
@@ -6,6 +6,8 @@ import type { SelectorTranslate } from '@/app/components/app/configuration/utils
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { InputVar, UploadFileSetting } from '@/app/components/workflow/types'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { Input } from '@langgenius/dify-ui/input'
import { NumberField, NumberFieldGroup, NumberFieldInput } from '@langgenius/dify-ui/number-field'
import {
Select,
SelectItem,
@@ -25,7 +27,6 @@ import { Trans } from 'react-i18next'
import { getStringSelectorTranslate } from '@/app/components/app/configuration/utils'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { Infotip } from '@/app/components/base/infotip'
import Input from '@/app/components/base/input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@@ -82,6 +83,11 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
}) => {
const t = getStringSelectorTranslate(rawTranslate)
const { type, label, variable } = tempPayload
const numberDefault =
typeof tempPayload.default === 'number' ||
(typeof tempPayload.default === 'string' && tempPayload.default.trim() !== '')
? Number(tempPayload.default)
: Number.NaN
const isFileInput = [InputVarType.singleFile, InputVarType.multiFiles].includes(type)
const docLink = useDocLink()
const fieldId = React.useId()
@@ -115,6 +121,7 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
>
<Input
id={`${fieldId}-variable`}
name="variable"
{...getErrorProps('variable')}
value={variable}
onChange={onVarNameChange}
@@ -130,9 +137,10 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
>
<Input
id={`${fieldId}-label`}
name="label"
{...getErrorProps('label')}
value={label as string}
onChange={(e) => onPayloadChange('label')(e.target.value)}
onValueChange={(value) => onPayloadChange('label')(value)}
placeholder={t(($) => $['variableConfig.inputPlaceholder'], { ns: 'appDebug' })}
/>
</Field>
@@ -159,8 +167,9 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
>
<Input
id={`${fieldId}-default`}
name="default"
value={typeof tempPayload.default === 'string' ? tempPayload.default : ''}
onChange={(e) => onPayloadChange('default')(e.target.value || undefined)}
onValueChange={(value) => onPayloadChange('default')(value || undefined)}
placeholder={t(($) => $['variableConfig.inputPlaceholder'], { ns: 'appDebug' })}
/>
</Field>
@@ -185,17 +194,20 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
htmlFor={`${fieldId}-default`}
title={t(($) => $['variableConfig.defaultValue'], { ns: 'appDebug' })}
>
<Input
<NumberField
id={`${fieldId}-default`}
type="number"
value={
typeof tempPayload.default === 'number' || typeof tempPayload.default === 'string'
? tempPayload.default
: ''
}
onChange={(e) => onPayloadChange('default')(e.target.value || undefined)}
placeholder={t(($) => $['variableConfig.inputPlaceholder'], { ns: 'appDebug' })}
/>
name="default"
value={Number.isFinite(numberDefault) ? numberDefault : null}
format={{ maximumSignificantDigits: 21, useGrouping: false }}
onValueChange={(value) => onPayloadChange('default')(value ?? undefined)}
>
<NumberFieldGroup>
<NumberFieldInput
inputMode="decimal"
placeholder={t(($) => $['variableConfig.inputPlaceholder'], { ns: 'appDebug' })}
/>
</NumberFieldGroup>
</NumberField>
</Field>
)}
@@ -93,6 +93,15 @@ export const createPayloadForType = (payload: InputVar, type: InputVarType) => {
return produce(payload, (draft) => {
draft.type = type
if (type === InputVarType.select) draft.default = undefined
if (isStringInputType(type) && typeof draft.default === 'number')
draft.default = String(draft.default)
if (type === InputVarType.number && typeof draft.default !== 'number') {
const value =
typeof draft.default === 'string' && draft.default.trim() !== ''
? Number(draft.default)
: Number.NaN
draft.default = Number.isFinite(value) ? value : undefined
}
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
draft.hide = false