diff --git a/web/__tests__/check-i18n.test.ts b/web/__tests__/check-i18n.test.ts
index 9e9b3d7168..de78ae997e 100644
--- a/web/__tests__/check-i18n.test.ts
+++ b/web/__tests__/check-i18n.test.ts
@@ -774,7 +774,7 @@ export default translation`
const endTime = Date.now()
expect(keys.length).toBe(1000)
- expect(endTime - startTime).toBeLessThan(10000)
+ expect(endTime - startTime).toBeLessThan(1000) // Should complete in under 1 second
})
it('should handle multiple translation files concurrently', async () => {
@@ -796,7 +796,7 @@ export default translation`
const endTime = Date.now()
expect(keys.length).toBe(20) // 10 files * 2 keys each
- expect(endTime - startTime).toBeLessThan(10000)
+ expect(endTime - startTime).toBeLessThan(500)
})
})
diff --git a/web/app/components/workflow/nodes/_base/__tests__/use-node-resize-observer.spec.tsx b/web/app/components/workflow/nodes/_base/__tests__/use-node-resize-observer.spec.tsx
index 9ee377be4d..a098ca997f 100644
--- a/web/app/components/workflow/nodes/_base/__tests__/use-node-resize-observer.spec.tsx
+++ b/web/app/components/workflow/nodes/_base/__tests__/use-node-resize-observer.spec.tsx
@@ -2,6 +2,10 @@ import { renderHook } from '@testing-library/react'
import useNodeResizeObserver from '../use-node-resize-observer'
describe('useNodeResizeObserver', () => {
+ afterEach(() => {
+ vi.unstubAllGlobals()
+ })
+
it('should observe and disconnect when enabled with a mounted node ref', () => {
const observe = vi.fn()
const disconnect = vi.fn()
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx
index 5eef8d3fa4..2929d0e47e 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/helpers.spec.tsx
@@ -75,16 +75,12 @@ describe('workflow-panel helpers', () => {
})
describe('custom run form fallback', () => {
- it('should return a fallback message for unsupported custom run form nodes', () => {
+ it('should return null for unsupported custom run form nodes', () => {
const form = getCustomRunForm({
...createCustomRunFormProps({ type: BlockEnum.Tool }),
})
- expect(form).toMatchObject({
- props: {
- children: expect.arrayContaining(['Custom Run Form:', ' ', 'not found']),
- },
- })
+ expect(form).toBeNull()
})
})
})
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/helpers.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/helpers.tsx
index c303bdc7f0..2e8e75d2a9 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/helpers.tsx
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/helpers.tsx
@@ -39,14 +39,7 @@ export const getCustomRunForm = (params: CustomRunFormProps): ReactNode => {
case BlockEnum.DataSource:
return
default:
- return (
-
- Custom Run Form:
- {nodeType}
- {' '}
- not found
-
- )
+ return null
}
}
diff --git a/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx b/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx
index 388ca255c8..4a4d94d3c3 100644
--- a/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx
+++ b/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx
@@ -50,6 +50,7 @@ const advancedColumns = [
describe('GenericTable', () => {
beforeEach(() => {
vi.clearAllMocks()
+ vi.useRealTimers()
})
it('should render an empty editable row and append a configured row when typing into the virtual row', async () => {
@@ -144,10 +145,11 @@ describe('GenericTable', () => {
)
await user.click(screen.getByRole('button', { name: 'Choose method' }))
- await user.click(await screen.findByRole('option', { name: 'POST' }))
+ await user.click(await screen.findByText('POST'))
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith([{ method: 'post', preview: '' }])
+ expect(screen.getByRole('button', { name: 'POST' })).toBeInTheDocument()
})
onChange.mockClear()
diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts
index 61ad609e50..47aeb57ae7 100644
--- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts
+++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts
@@ -90,6 +90,23 @@ describe('useVariableModalState', () => {
])
})
+ it('should keep valid object rows when switching to json mode from form mode', () => {
+ const { result } = renderHook(() => useVariableModalState(createOptions()))
+
+ act(() => {
+ result.current.handleTypeChange(ChatVarType.Object)
+ result.current.setObjectValue([
+ { key: '', type: ChatVarType.String, value: undefined },
+ { key: 'timeout', type: ChatVarType.Number, value: 30 },
+ ])
+ result.current.handleEditorChange(true)
+ })
+
+ expect(result.current.editInJSON).toBe(true)
+ expect(result.current.value).toEqual({ timeout: 30 })
+ expect(result.current.editorContent).toBe(JSON.stringify({ timeout: 30 }))
+ })
+
it('should reset object form values when leaving empty json mode', () => {
const { result } = renderHook(() => useVariableModalState(createOptions({
chatVar: {
@@ -161,7 +178,7 @@ describe('useVariableModalState', () => {
result.current.handleSave()
})
- expect(notify).toHaveBeenCalledWith({ type: 'error', message: 'object key can not be empty' })
+ expect(notify).toHaveBeenCalledWith({ type: 'error', message: 'chatVariable.modal.objectKeyRequired' })
expect(onSave).not.toHaveBeenCalled()
expect(onClose).not.toHaveBeenCalled()
})
diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts
index 9e082265d6..2caaf9b90d 100644
--- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts
+++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts
@@ -33,6 +33,11 @@ describe('variable-modal helpers', () => {
{ key: '', type: ChatVarType.Number, value: 1 },
])).toEqual({ apiKey: 'secret' })
+ expect(formatObjectValueFromList([
+ { key: 'count', type: ChatVarType.Number, value: 0 },
+ { key: 'label', type: ChatVarType.String, value: '' },
+ ])).toEqual({ count: 0, label: null })
+
expect(formatChatVariableValue({
editInJSON: false,
objectValue: [{ key: 'enabled', type: ChatVarType.String, value: 'true' }],
diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx
index 596383cb87..e61a6bd085 100644
--- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx
+++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx
@@ -80,7 +80,7 @@ describe('variable-modal', () => {
await user.type(screen.getByPlaceholderText('workflow.chatVariable.modal.namePlaceholder'), 'existing_name')
await user.click(screen.getByText('common.operation.save'))
- expect(mockToastError.mock.calls.at(-1)?.[0]).toBe('name is existed')
+ expect(mockToastError.mock.calls.at(-1)?.[0]).toBe('appDebug.varKeyError.keyAlreadyExists:{"key":"workflow.chatVariable.modal.name"}')
expect(onSave).not.toHaveBeenCalled()
})
@@ -195,4 +195,22 @@ describe('variable-modal', () => {
description: '',
})
})
+
+ it('should keep the number input empty while editing after the user clears it', async () => {
+ const user = userEvent.setup()
+ renderVariableModal({
+ chatVar: {
+ id: 'var-4',
+ name: 'timeout',
+ description: '',
+ value_type: ChatVarType.Number,
+ value: 3,
+ },
+ })
+
+ const input = screen.getByDisplayValue('3') as HTMLInputElement
+ await user.clear(input)
+
+ expect(input.value).toBe('')
+ })
})
diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts b/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts
index ecc8af6432..648fdc8eaf 100644
--- a/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts
+++ b/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts
@@ -108,7 +108,7 @@ export const useVariableModalState = ({
if (prev.type === ChatVarType.Object) {
if (nextEditInJSON) {
- const nextValue = !prev.objectValue[0].key ? undefined : formatObjectValueFromList(prev.objectValue)
+ const nextValue = prev.objectValue.some(item => item.key) ? formatObjectValueFromList(prev.objectValue) : undefined
nextState.value = nextValue
nextState.editorContent = JSON.stringify(nextValue)
return nextState
@@ -181,12 +181,15 @@ export const useVariableModalState = ({
return
if (!chatVar && conversationVariables.some(item => item.name === state.name)) {
- notify({ type: 'error', message: 'name is existed' })
+ notify({
+ type: 'error',
+ message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: t('chatVariable.modal.name', { ns: 'workflow' }) }),
+ })
return
}
- if (state.type === ChatVarType.Object && state.objectValue.some(item => !item.key && !!item.value)) {
- notify({ type: 'error', message: 'object key can not be empty' })
+ if (state.type === ChatVarType.Object && state.objectValue.some(item => !item.key && item.value !== undefined && item.value !== '')) {
+ notify({ type: 'error', message: t('chatVariable.modal.objectKeyRequired', { ns: 'workflow' }) })
return
}
diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts
index 944b197e19..07369ed1fe 100644
--- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts
+++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts
@@ -72,7 +72,7 @@ export const buildObjectValueItems = (chatVar?: ConversationVariable): ObjectVal
export const formatObjectValueFromList = (list: ObjectValueItem[]) => {
return list.reduce>((acc, curr) => {
if (curr.key)
- acc[curr.key] = curr.value || null
+ acc[curr.key] = curr.value === '' || curr.value === undefined ? null : curr.value
return acc
}, {})
}
diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx
index dd7e69bb34..6b31e024b4 100644
--- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx
+++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.sections.tsx
@@ -138,7 +138,10 @@ export const ValueSection = ({
onArrayChange([Number(e.target.value)])}
+ onChange={(e) => {
+ const rawValue = e.target.value
+ onArrayChange([rawValue === '' ? undefined : Number(rawValue)])
+ }}
type="number"
/>
)}