Skip to content

Commit 03a0cf6

Browse files
authored
Feat/agentflow Add JSON, code, and SelectVariable input types to NodeInputHandler (#6016)
* Initial flow * enhance JSON input handling with dialog support and improve variable selection UI * Fix syntax highlighting bug * Added test cases * Fix gemini comments * lock file fix * Build error * Test fix * Remove 'inputs' folder under atoms and move all components directly under atoms/
1 parent fff4b80 commit 03a0cf6

20 files changed

Lines changed: 1490 additions & 293 deletions

packages/agentflow/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,19 @@
6868
"reactflow": "^11.5.0"
6969
},
7070
"dependencies": {
71+
"@codemirror/lang-javascript": "^6.2.0",
72+
"@codemirror/lang-json": "^6.0.0",
73+
"@codemirror/lang-python": "^6.1.0",
7174
"@tabler/icons-react": "^3.7.0",
7275
"@tiptap/extension-code-block-lowlight": "^3.4.3",
7376
"@tiptap/extension-placeholder": "^2.11.5",
7477
"@tiptap/react": "^2.11.5",
7578
"@tiptap/starter-kit": "^2.11.5",
79+
"@uiw/codemirror-theme-sublime": "^4.21.0",
80+
"@uiw/codemirror-theme-vscode": "^4.21.0",
81+
"@uiw/react-codemirror": "^4.21.0",
7682
"axios": "^1.7.2",
83+
"flowise-react-json-view": "^1.21.7",
7784
"lodash": "^4.17.21",
7885
"lowlight": "^3.3.0",
7986
"uuid": "^10.0.0"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { render, screen } from '@testing-library/react'
2+
3+
import { CodeInput } from './CodeInput'
4+
5+
// Mock CodeMirror — jsdom doesn't support it
6+
jest.mock('@uiw/react-codemirror', () => {
7+
const MockCodeMirror = ({ value, readOnly, height }: { value: string; readOnly?: boolean; height?: string }) => (
8+
<textarea data-testid='codemirror' value={value} readOnly={readOnly} data-height={height} onChange={() => {}} />
9+
)
10+
MockCodeMirror.displayName = 'MockCodeMirror'
11+
return { __esModule: true, default: MockCodeMirror }
12+
})
13+
14+
jest.mock('@uiw/codemirror-theme-vscode', () => ({ vscodeDark: 'vscodeDark' }))
15+
jest.mock('@uiw/codemirror-theme-sublime', () => ({ sublime: 'sublime' }))
16+
jest.mock('@codemirror/lang-javascript', () => ({ javascript: () => [] }))
17+
jest.mock('@codemirror/lang-json', () => ({ json: () => [] }))
18+
jest.mock('@codemirror/lang-python', () => ({ python: () => [] }))
19+
20+
describe('CodeInput', () => {
21+
it('renders CodeMirror with the provided value', () => {
22+
render(<CodeInput value='const x = 1' onChange={jest.fn()} />)
23+
24+
const editor = screen.getByTestId('codemirror')
25+
expect(editor).toHaveValue('const x = 1')
26+
})
27+
28+
it('renders with default height of 200px', () => {
29+
render(<CodeInput value='' onChange={jest.fn()} />)
30+
31+
expect(screen.getByTestId('codemirror')).toHaveAttribute('data-height', '200px')
32+
})
33+
34+
it('renders with custom height', () => {
35+
render(<CodeInput value='' onChange={jest.fn()} height='400px' />)
36+
37+
expect(screen.getByTestId('codemirror')).toHaveAttribute('data-height', '400px')
38+
})
39+
40+
it('sets readOnly when disabled', () => {
41+
render(<CodeInput value='code' onChange={jest.fn()} disabled />)
42+
43+
expect(screen.getByTestId('codemirror')).toHaveAttribute('readonly')
44+
})
45+
46+
it('is not readOnly when enabled', () => {
47+
render(<CodeInput value='code' onChange={jest.fn()} />)
48+
49+
expect(screen.getByTestId('codemirror')).not.toHaveAttribute('readonly')
50+
})
51+
52+
it('renders empty string when value is empty', () => {
53+
render(<CodeInput value='' onChange={jest.fn()} />)
54+
55+
expect(screen.getByTestId('codemirror')).toHaveValue('')
56+
})
57+
58+
it('renders inside a bordered container', () => {
59+
const { container } = render(<CodeInput value='' onChange={jest.fn()} />)
60+
61+
const box = container.firstChild as HTMLElement
62+
expect(box).toBeTruthy()
63+
})
64+
})
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useMemo } from 'react'
2+
3+
import { javascript } from '@codemirror/lang-javascript'
4+
import { json } from '@codemirror/lang-json'
5+
import { python } from '@codemirror/lang-python'
6+
import { Box } from '@mui/material'
7+
import { useTheme } from '@mui/material/styles'
8+
import { sublime } from '@uiw/codemirror-theme-sublime'
9+
import { vscodeDark } from '@uiw/codemirror-theme-vscode'
10+
import CodeMirror from '@uiw/react-codemirror'
11+
12+
export interface CodeInputProps {
13+
value: string
14+
onChange: (code: string) => void
15+
language?: string
16+
disabled?: boolean
17+
height?: string
18+
}
19+
20+
/**
21+
* CodeMirror-based code editor atom.
22+
*
23+
* Supports javascript (default), python, and json syntax highlighting.
24+
* Theme switches automatically based on dark mode.
25+
*/
26+
export function CodeInput({ value, onChange, language = 'javascript', disabled = false, height = '200px' }: CodeInputProps) {
27+
const theme = useTheme()
28+
const isDarkMode = theme.palette.mode === 'dark'
29+
30+
const extensions = useMemo(() => {
31+
switch (language) {
32+
case 'python':
33+
return [python()]
34+
case 'json':
35+
return [json()]
36+
case 'typescript':
37+
return [javascript({ typescript: true })]
38+
default:
39+
return [javascript()]
40+
}
41+
}, [language])
42+
43+
return (
44+
<Box
45+
sx={{
46+
mt: 1,
47+
border: '1px solid',
48+
borderColor: 'grey.400',
49+
borderRadius: '6px',
50+
overflow: 'hidden'
51+
}}
52+
>
53+
<CodeMirror
54+
value={value || ''}
55+
height={height}
56+
theme={isDarkMode ? (language === 'json' ? sublime : vscodeDark) : 'light'}
57+
extensions={extensions}
58+
onChange={onChange}
59+
readOnly={disabled}
60+
basicSetup={{ lineNumbers: true, foldGutter: true }}
61+
/>
62+
</Box>
63+
)
64+
}

packages/agentflow/src/atoms/ExpandTextDialog.test.tsx

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import { ExpandTextDialog } from './ExpandTextDialog'
44

55
// TipTap modules are auto-mocked via moduleNameMapper in jest.config.js
66

7+
jest.mock('./CodeInput', () => ({
8+
CodeInput: ({ value, language }: { value: string; language?: string }) => (
9+
<textarea data-testid='code-input' data-language={language} value={value} onChange={() => {}} />
10+
)
11+
}))
12+
713
const mockOnConfirm = jest.fn()
814
const mockOnCancel = jest.fn()
915

@@ -13,32 +19,34 @@ beforeEach(() => {
1319

1420
describe('ExpandTextDialog', () => {
1521
it('should not render content when closed', () => {
16-
render(<ExpandTextDialog open={false} value='' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
22+
render(<ExpandTextDialog open={false} value='' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
1723

1824
expect(screen.queryByTestId('expand-content-input')).not.toBeInTheDocument()
1925
})
2026

2127
it('should render with the provided value when open', () => {
22-
render(<ExpandTextDialog open={true} value='Hello world' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
28+
render(<ExpandTextDialog open={true} value='Hello world' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
2329

2430
const textarea = screen.getByTestId('expand-content-input').querySelector('textarea')!
2531
expect(textarea).toHaveValue('Hello world')
2632
})
2733

2834
it('should render title when provided', () => {
29-
render(<ExpandTextDialog open={true} value='' title='Content' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
35+
render(
36+
<ExpandTextDialog open={true} value='' title='Content' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />
37+
)
3038

3139
expect(screen.getByText('Content')).toBeInTheDocument()
3240
})
3341

3442
it('should not render title when not provided', () => {
35-
render(<ExpandTextDialog open={true} value='' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
43+
render(<ExpandTextDialog open={true} value='' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
3644

3745
expect(screen.queryByRole('heading')).not.toBeInTheDocument()
3846
})
3947

4048
it('should call onConfirm with edited value when Save is clicked', () => {
41-
render(<ExpandTextDialog open={true} value='Original' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
49+
render(<ExpandTextDialog open={true} value='Original' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
4250

4351
const textarea = screen.getByTestId('expand-content-input').querySelector('textarea')!
4452
fireEvent.change(textarea, { target: { value: 'Updated' } })
@@ -48,7 +56,7 @@ describe('ExpandTextDialog', () => {
4856
})
4957

5058
it('should call onCancel when Cancel is clicked', () => {
51-
render(<ExpandTextDialog open={true} value='Original' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
59+
render(<ExpandTextDialog open={true} value='Original' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
5260

5361
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
5462

@@ -58,7 +66,14 @@ describe('ExpandTextDialog', () => {
5866

5967
it('should disable textarea and Save button when disabled', () => {
6068
render(
61-
<ExpandTextDialog open={true} value='test' inputType='code' disabled={true} onConfirm={mockOnConfirm} onCancel={mockOnCancel} />
69+
<ExpandTextDialog
70+
open={true}
71+
value='test'
72+
inputType='number'
73+
disabled={true}
74+
onConfirm={mockOnConfirm}
75+
onCancel={mockOnCancel}
76+
/>
6277
)
6378

6479
const textarea = screen.getByTestId('expand-content-input').querySelector('textarea')!
@@ -71,7 +86,7 @@ describe('ExpandTextDialog', () => {
7186
<ExpandTextDialog
7287
open={true}
7388
value=''
74-
inputType='code'
89+
inputType='number'
7590
placeholder='Type here...'
7691
onConfirm={mockOnConfirm}
7792
onCancel={mockOnCancel}
@@ -84,22 +99,24 @@ describe('ExpandTextDialog', () => {
8499

85100
it('should show current value when opened after value changed while closed', () => {
86101
const { rerender } = render(
87-
<ExpandTextDialog open={false} value='' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />
102+
<ExpandTextDialog open={false} value='' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />
88103
)
89104

90105
// Simulate value changing while dialog is closed (user typing in inline editor)
91-
rerender(<ExpandTextDialog open={false} value='Updated text' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
106+
rerender(
107+
<ExpandTextDialog open={false} value='Updated text' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />
108+
)
92109

93110
// Open the dialog — it should show the updated value, not the initial empty value
94-
rerender(<ExpandTextDialog open={true} value='Updated text' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
111+
rerender(<ExpandTextDialog open={true} value='Updated text' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
95112

96113
const textarea = screen.getByTestId('expand-content-input').querySelector('textarea')!
97114
expect(textarea).toHaveValue('Updated text')
98115
})
99116

100117
it('should reset to current value when re-opened after cancel', () => {
101118
const { rerender } = render(
102-
<ExpandTextDialog open={true} value='Original' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />
119+
<ExpandTextDialog open={true} value='Original' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />
103120
)
104121

105122
// User types in the dialog then cancels
@@ -108,15 +125,58 @@ describe('ExpandTextDialog', () => {
108125
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
109126

110127
// Close the dialog
111-
rerender(<ExpandTextDialog open={false} value='Original' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
128+
rerender(<ExpandTextDialog open={false} value='Original' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
112129

113130
// Re-open — should show the original value, not the unsaved edits
114-
rerender(<ExpandTextDialog open={true} value='Original' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
131+
rerender(<ExpandTextDialog open={true} value='Original' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
115132

116133
const textarea2 = screen.getByTestId('expand-content-input').querySelector('textarea')!
117134
expect(textarea2).toHaveValue('Original')
118135
})
119136

137+
// --- Code mode ---
138+
139+
describe('inputType="code"', () => {
140+
it('should render CodeInput instead of TextField', () => {
141+
render(
142+
<ExpandTextDialog
143+
open={true}
144+
value='const x = 1'
145+
inputType='code'
146+
language='javascript'
147+
onConfirm={mockOnConfirm}
148+
onCancel={mockOnCancel}
149+
/>
150+
)
151+
152+
expect(screen.getByTestId('code-input')).toBeInTheDocument()
153+
expect(screen.queryByTestId('expand-content-input')).not.toBeInTheDocument()
154+
expect(screen.queryByTestId('rich-text-editor')).not.toBeInTheDocument()
155+
})
156+
157+
it('should pass language prop to CodeInput', () => {
158+
render(
159+
<ExpandTextDialog
160+
open={true}
161+
value='{}'
162+
inputType='code'
163+
language='json'
164+
onConfirm={mockOnConfirm}
165+
onCancel={mockOnCancel}
166+
/>
167+
)
168+
169+
expect(screen.getByTestId('code-input')).toHaveAttribute('data-language', 'json')
170+
})
171+
172+
it('should show Save and Cancel buttons in code mode', () => {
173+
render(<ExpandTextDialog open={true} value='' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
174+
175+
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument()
176+
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument()
177+
})
178+
})
179+
120180
// --- Rich text mode ---
121181

122182
describe('inputType="string" (richtext)', () => {
@@ -133,11 +193,12 @@ describe('ExpandTextDialog', () => {
133193
expect(screen.queryByTestId('expand-content-input')).not.toBeInTheDocument()
134194
})
135195

136-
it('should render plain TextField for non-string input types', () => {
137-
render(<ExpandTextDialog open={true} value='Hello' inputType='code' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
196+
it('should render plain TextField for non-string, non-code input types', () => {
197+
render(<ExpandTextDialog open={true} value='Hello' inputType='number' onConfirm={mockOnConfirm} onCancel={mockOnCancel} />)
138198

139199
expect(screen.getByTestId('expand-content-input')).toBeInTheDocument()
140200
expect(screen.queryByTestId('rich-text-editor')).not.toBeInTheDocument()
201+
expect(screen.queryByTestId('code-input')).not.toBeInTheDocument()
141202
})
142203

143204
it('should still show Save and Cancel buttons in richtext mode', () => {

packages/agentflow/src/atoms/ExpandTextDialog.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useCallback, useState } from 'react'
22

33
import { Box, Button, Dialog, DialogActions, DialogContent, TextField, Typography } from '@mui/material'
44

5+
import { CodeInput } from './CodeInput'
56
import { RichTextEditor } from './RichTextEditor.lazy'
67

78
export interface ExpandTextDialogProps {
@@ -10,9 +11,10 @@ export interface ExpandTextDialogProps {
1011
title?: string
1112
placeholder?: string
1213
disabled?: boolean
13-
/** The input param type — determines which editor to render. 'string' uses the TipTap RichTextEditor; others fall back to a plain TextField. */
14-
// TODO: handle 'code' type separately with a dedicated CodeMirror editor
14+
/** The input param type — determines which editor to render. 'string' uses the TipTap RichTextEditor, 'code' renders CodeInput; others fall back to a plain TextField. */
1515
inputType?: string
16+
/** Language hint for 'code' mode (e.g. 'javascript', 'python', 'json'). */
17+
language?: string
1618
onConfirm: (value: string) => void
1719
onCancel: () => void
1820
}
@@ -28,6 +30,7 @@ export function ExpandTextDialog({
2830
placeholder,
2931
disabled = false,
3032
inputType = 'string',
33+
language,
3134
onConfirm,
3235
onCancel
3336
}: ExpandTextDialogProps) {
@@ -56,7 +59,15 @@ export function ExpandTextDialog({
5659
{title}
5760
</Typography>
5861
)}
59-
{inputType === 'string' ? (
62+
{inputType === 'code' ? (
63+
<CodeInput
64+
value={localValue}
65+
onChange={setLocalValue}
66+
language={language}
67+
disabled={disabled}
68+
height='calc(100vh - 220px)'
69+
/>
70+
) : inputType === 'string' ? (
6071
<Box
6172
sx={{
6273
borderRadius: '12px',

0 commit comments

Comments
 (0)