From 2de83835cd481cc7e4c4c126345d9aff68903136 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 11 Sep 2026 06:16:12 +0000 Subject: [PATCH] refactor(web): migrate variable configuration inputs (#42179) --- oxlint-suppressions.json | 3 - .../__tests__/form-fields.spec.tsx | 27 +--- .../config-modal/__tests__/index.spec.tsx | 149 ++++++++++++++++++ .../config-var/config-modal/form-fields.tsx | 38 +++-- .../config-var/config-modal/utils.ts | 9 ++ 5 files changed, 186 insertions(+), 40 deletions(-) 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