mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
refactor(web): migrate variable configuration inputs (#42179)
This commit is contained in:
5 files changed
+186
-40
No files matched your search
@@ -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": {
|
||||
|
||||
+3
-24
@@ -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', () => {
|
||||
|
||||
+149
@@ -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
|
||||
|
||||
Reference in new issue
Block a user