Skip to content

Commit b4e47a6

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/audit-telemetry-gdpr-deletion
2 parents 9b249ff + 7176dcb commit b4e47a6

71 files changed

Lines changed: 3793 additions & 553 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,40 @@ Flowise has 3 different modules in a single mono repository.
116116

117117
11. Commit code and submit Pull Request from forked branch pointing to [Flowise main](https://github.com/FlowiseAI/Flowise/tree/main).
118118

119+
### Testing
120+
121+
- Unit tests are **co-located** with their source files — a test for `Foo.ts` lives in `Foo.test.ts` in the same directory. This is the standard used across all packages in this repo.
122+
123+
- Run tests per package:
124+
125+
```bash
126+
cd packages/server && pnpm test
127+
cd packages/components && pnpm test
128+
cd packages/agentflow && pnpm test
129+
```
130+
131+
Or from the repo root using `--filter`:
132+
133+
```bash
134+
pnpm --filter flowise-components test
135+
pnpm --filter @flowiseai/agentflow test
136+
pnpm --filter "./packages/server" test # root and server share the same package name.
137+
```
138+
139+
- Or run all tests from the repo root:
140+
141+
```bash
142+
pnpm test
143+
```
144+
145+
- When adding new functionality, place your test file next to the source file it tests:
146+
147+
```
148+
packages/components/nodes/tools/MyTool/
149+
├── MyTool.ts
150+
└── MyTool.test.ts ← co-located test
151+
```
152+
119153
## 🌱 Env Variables
120154

121155
Flowise support different environment variables to configure your instance. You can specify the following variables in the `.env` file inside `packages/server` folder. Read [more](https://docs.flowiseai.com/environment-variables)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "flowise",
3-
"version": "3.1.0",
3+
"version": "3.1.1",
44
"private": true,
55
"homepage": "https://flowiseai.com",
66
"workspaces": [

packages/agentflow/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module.exports = {
4747
'./src/atoms/ArrayInput.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
4848
'./src/atoms/ExpandTextDialog.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
4949
'./src/atoms/MessagesInput.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
50+
'./src/atoms/ScenariosInput.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
5051
// Tier 3 UI atom — only the onChange/disabled/sync logic is tested, not styled internals
5152
'./src/atoms/RichTextEditor.tsx': { branches: 30, functions: 50, lines: 50, statements: 50 },
5253
'./src/core/': { branches: 80, functions: 80, lines: 80, statements: 80 },

packages/agentflow/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@flowiseai/agentflow",
3-
"version": "0.0.0-dev.5",
3+
"version": "0.0.0-dev.6",
44
"description": "Embeddable React component for building and visualizing AI agent workflows",
55
"license": "Apache-2.0",
66
"repository": {
@@ -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"

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

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,140 @@ describe('ArrayInput', () => {
306306
})
307307
})
308308

309-
// Test 12: itemParameters prop overrides inputParam.array display flags
309+
// Test 12: Nested array sub-fields render recursively
310+
it('should render nested array sub-fields (e.g., addOptions inside formInputTypes)', () => {
311+
const nestedArrayParam: InputParam = {
312+
id: 'formInputTypes',
313+
name: 'formInputTypes',
314+
label: 'Form Input Type',
315+
type: 'array',
316+
array: [
317+
{ id: 'type', name: 'type', label: 'Type', type: 'options', default: 'string' } as InputParam,
318+
{ id: 'label', name: 'label', label: 'Label', type: 'string' } as InputParam,
319+
{
320+
id: 'addOptions',
321+
name: 'addOptions',
322+
label: 'Add Options',
323+
type: 'array',
324+
display: true,
325+
array: [{ id: 'option', name: 'option', label: 'Option', type: 'string' } as InputParam]
326+
} as InputParam
327+
]
328+
}
329+
330+
const dataWithNestedArray: NodeData = {
331+
...mockNodeData,
332+
inputValues: {
333+
formInputTypes: [
334+
{
335+
type: 'options',
336+
label: 'Color',
337+
addOptions: [{ option: 'Red' }, { option: 'Blue' }]
338+
}
339+
]
340+
}
341+
} as NodeData
342+
343+
render(<ArrayInput inputParam={nestedArrayParam} data={dataWithNestedArray} onDataChange={mockOnDataChange} />)
344+
345+
// The parent array item renders
346+
expect(screen.getByText('0')).toBeInTheDocument()
347+
348+
// The nested array sub-field (addOptions) is rendered via NodeInputHandler
349+
// Since our mock NodeInputHandler renders a div with data-testid, the addOptions field should appear
350+
expect(screen.getByTestId('input-handler-addOptions')).toBeInTheDocument()
351+
})
352+
353+
it('should hide nested array sub-fields when display is false', () => {
354+
const nestedArrayParam: InputParam = {
355+
id: 'formInputTypes',
356+
name: 'formInputTypes',
357+
label: 'Form Input Type',
358+
type: 'array',
359+
array: [
360+
{ id: 'type', name: 'type', label: 'Type', type: 'options' } as InputParam,
361+
{
362+
id: 'addOptions',
363+
name: 'addOptions',
364+
label: 'Add Options',
365+
type: 'array',
366+
display: false,
367+
array: [{ id: 'option', name: 'option', label: 'Option', type: 'string' } as InputParam]
368+
} as InputParam
369+
]
370+
}
371+
372+
const dataWithNestedArray: NodeData = {
373+
...mockNodeData,
374+
inputValues: {
375+
formInputTypes: [{ type: 'string', label: 'Name' }]
376+
}
377+
} as NodeData
378+
379+
render(<ArrayInput inputParam={nestedArrayParam} data={dataWithNestedArray} onDataChange={mockOnDataChange} />)
380+
381+
expect(screen.getByTestId('input-handler-type')).toBeInTheDocument()
382+
expect(screen.queryByTestId('input-handler-addOptions')).not.toBeInTheDocument()
383+
})
384+
385+
it('should use itemParameters to control nested array visibility per row', () => {
386+
const nestedArrayParam: InputParam = {
387+
id: 'formInputTypes',
388+
name: 'formInputTypes',
389+
label: 'Form Input Type',
390+
type: 'array',
391+
array: [
392+
{ id: 'type', name: 'type', label: 'Type', type: 'options' } as InputParam,
393+
{
394+
id: 'addOptions',
395+
name: 'addOptions',
396+
label: 'Add Options',
397+
type: 'array',
398+
array: [{ id: 'option', name: 'option', label: 'Option', type: 'string' } as InputParam]
399+
} as InputParam
400+
]
401+
}
402+
403+
const dataWithTwoRows: NodeData = {
404+
...mockNodeData,
405+
inputValues: {
406+
formInputTypes: [
407+
{ type: 'options', label: 'Color', addOptions: [{ option: 'Red' }] },
408+
{ type: 'string', label: 'Name' }
409+
]
410+
}
411+
} as NodeData
412+
413+
// Row 0: addOptions visible (type = options)
414+
// Row 1: addOptions hidden (type = string)
415+
const itemParameters: InputParam[][] = [
416+
[
417+
{ id: 'type', name: 'type', label: 'Type', type: 'options', display: true } as InputParam,
418+
{ id: 'addOptions', name: 'addOptions', label: 'Add Options', type: 'array', display: true } as InputParam
419+
],
420+
[
421+
{ id: 'type', name: 'type', label: 'Type', type: 'options', display: true } as InputParam,
422+
{ id: 'addOptions', name: 'addOptions', label: 'Add Options', type: 'array', display: false } as InputParam
423+
]
424+
]
425+
426+
render(
427+
<ArrayInput
428+
inputParam={nestedArrayParam}
429+
data={dataWithTwoRows}
430+
onDataChange={mockOnDataChange}
431+
itemParameters={itemParameters}
432+
/>
433+
)
434+
435+
// Both rows show their Type field
436+
expect(screen.getAllByTestId('input-handler-type')).toHaveLength(2)
437+
438+
// Only row 0 shows addOptions (row 1 has display: false)
439+
expect(screen.getAllByTestId('input-handler-addOptions')).toHaveLength(1)
440+
})
441+
442+
// Test 13: itemParameters prop overrides inputParam.array display flags
310443
it('should use itemParameters prop for field visibility when provided, ignoring inputParam.array display flags', () => {
311444
// inputParam.array has both fields with no display flag (both would show)
312445
const dataWithItem: NodeData = {
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+
}

0 commit comments

Comments
 (0)