diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json
index d451c140eea..a1d36a661e9 100644
--- a/oxlint-suppressions.json
+++ b/oxlint-suppressions.json
@@ -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": {
diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx
index 2ef09f39557..7b9068edcca 100644
--- a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx
+++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.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()
- 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()
-
- 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()
- 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', () => {
diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx
index 60ca1457a5b..64311a83ece 100644
--- a/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx
+++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx
@@ -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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ 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) => {
diff --git a/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx b/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx
index 84f51e9e95f..481d266da6f 100644
--- a/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx
+++ b/web/app/components/app/configuration/config-var/config-modal/form-fields.tsx
@@ -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 = ({
}) => {
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 = ({
>
= ({
>
onPayloadChange('label')(e.target.value)}
+ onValueChange={(value) => onPayloadChange('label')(value)}
placeholder={t(($) => $['variableConfig.inputPlaceholder'], { ns: 'appDebug' })}
/>
@@ -159,8 +167,9 @@ const ConfigModalFormFields: FC = ({
>
onPayloadChange('default')(e.target.value || undefined)}
+ onValueChange={(value) => onPayloadChange('default')(value || undefined)}
placeholder={t(($) => $['variableConfig.inputPlaceholder'], { ns: 'appDebug' })}
/>
@@ -185,17 +194,20 @@ const ConfigModalFormFields: FC = ({
htmlFor={`${fieldId}-default`}
title={t(($) => $['variableConfig.defaultValue'], { ns: 'appDebug' })}
>
- 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)}
+ >
+
+ $['variableConfig.inputPlaceholder'], { ns: 'appDebug' })}
+ />
+
+
)}
diff --git a/web/app/components/app/configuration/config-var/config-modal/utils.ts b/web/app/components/app/configuration/config-var/config-modal/utils.ts
index caa90475572..e2601f11802 100644
--- a/web/app/components/app/configuration/config-var/config-modal/utils.ts
+++ b/web/app/components/app/configuration/config-var/config-modal/utils.ts
@@ -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