Skip to content

Commit c28067c

Browse files
authored
Merge branch 'main' into fix/stripe-email
2 parents eb1d443 + 106f211 commit c28067c

65 files changed

Lines changed: 3179 additions & 547 deletions

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ Flowise support different environment variables to configure your instance. You
157157
| Variable | Description | Type | Default |
158158
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------- |
159159
| PORT | The HTTP port Flowise runs on | Number | 3000 |
160+
| CORS_ALLOW_CREDENTIALS | Enables CORS `Access-Control-Allow-Credentials` when `true` | Boolean | false |
160161
| CORS_ORIGINS | The allowed origins for all cross-origin HTTP calls | String | |
161162
| IFRAME_ORIGINS | The allowed origins for iframe src embedding | String | |
162163
| FLOWISE_FILE_SIZE_LIMIT | Upload File Size Limit | String | 50mb |

docker/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ PORT=3000
7474
############################################################################################################
7575

7676
# NUMBER_OF_PROXIES= 1
77+
# CORS_ALLOW_CREDENTIALS=false
7778
# CORS_ORIGINS=*
7879
# IFRAME_ORIGINS=*
7980
# FLOWISE_FILE_SIZE_LIMIT=50mb

docker/worker/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ WORKER_PORT=5566
7474
############################################################################################################
7575

7676
# NUMBER_OF_PROXIES= 1
77+
# CORS_ALLOW_CREDENTIALS=false
7778
# CORS_ORIGINS=*
7879
# IFRAME_ORIGINS=*
7980
# FLOWISE_FILE_SIZE_LIMIT=50mb

packages/agentflow/.eslintrc.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ module.exports = {
119119
{
120120
target: './src/atoms',
121121
from: './src/core',
122-
except: ['./types', './theme'],
123-
message: 'Atoms can only import from core/types and core/theme, not utilities or business logic.'
122+
except: ['./types', './theme', './primitives'],
123+
message: 'Atoms can only import from core/types, core/theme, and core/primitives.'
124124
},
125125
// core/ cannot import from anything (leaf node)
126126
{

packages/agentflow/ARCHITECTURE.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ atoms/
4242
- No API calls
4343
- Stateless or minimal local state
4444
- Imported by features, never the reverse
45-
- **Forbidden**: Importing from `features/` or `infrastructure/` (except types from `core/types` for prop definitions and design tokens from `core/theme`)
45+
- **Forbidden**: Importing from `features/` or `infrastructure/` (except types from `core/types` for prop definitions, design tokens from `core/theme`, and primitives from `core/primitives`)
4646

4747
**Goal:** 100% visual consistency.
4848

@@ -110,6 +110,9 @@ features/
110110
core/
111111
├── types/ # Global interfaces (Node, Edge, Flow)
112112
│ └── index.ts
113+
├── primitives/ # Domain-free utilities (safe for atoms)
114+
│ ├── inputDefaults.ts # getDefaultValueForType
115+
│ └── index.ts
113116
├── node-config/ # Node configuration (icons, colors, default types)
114117
│ ├── nodeIcons.ts # AGENTFLOW_ICONS, DEFAULT_AGENTFLOW_NODES
115118
│ └── ...
@@ -124,7 +127,7 @@ core/
124127
│ ├── flowValidation.ts # validateFlow, validateNode
125128
│ ├── connectionValidation.ts # isValidConnectionAgentflowV2
126129
│ └── ...
127-
├── utils/ # Generic utilities
130+
├── utils/ # Domain-aware utilities (NOT importable by atoms)
128131
│ ├── nodeFactory.ts # initNode, getUniqueNodeId
129132
│ └── ...
130133
└── index.ts # Barrel export (use sparingly)
@@ -138,6 +141,15 @@ core/
138141
- Pure functions where possible
139142
- Can be tested in isolation
140143

144+
#### `core/primitives/` vs `core/utils/`
145+
146+
`core/` contains two utility directories with different import permissions:
147+
148+
- **`primitives/`** — Domain-free, general-purpose functions with no knowledge of nodes, flows, or any business concept. These are pure data transformations (e.g., computing a default value from a type string). **Safe to import from `atoms/`.**
149+
- **`utils/`** — Domain-aware utilities that understand node structures, flow data, or validation logic (e.g., `initNode`, `buildDynamicOutputAnchors`). **Only importable by `features/` and `infrastructure/`.**
150+
151+
When adding a new utility, ask: _"Does this function need to know what a Node or Flow is?"_ If no → `primitives/`. If yes → `utils/`.
152+
141153
**Goal:** To be the framework-agnostic source of truth.
142154

143155
---
@@ -210,7 +222,7 @@ infrastructure/
210222

211223
- `features``atoms`, `infrastructure`, `core`
212224
- `infrastructure``core`
213-
- `atoms``core/types` and `core/theme` only (for type definitions and design tokens)
225+
- `atoms``core/types`, `core/theme`, and `core/primitives` only
214226
- `core` → nothing (leaf node) ✅
215227
- **Atoms and Core are "leaf" nodes** - they cannot import from `features/` or `infrastructure/`
216228

packages/agentflow/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = {
5454
'./src/features/canvas/components/ConnectionLine.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
5555
// Only getMinimumNodeHeight() is tested; the component is Tier 3 UI with no business logic
5656
'./src/features/canvas/components/NodeOutputHandles.tsx': { branches: 0, functions: 10, lines: 30, statements: 30 },
57+
'./src/features/canvas/containers/NodeInfoDialog.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
5758
'./src/features/canvas/hooks/': { branches: 80, functions: 80, lines: 80, statements: 80 },
5859
'./src/features/generator/GenerateFlowDialog.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
5960
'./src/features/node-editor/': { branches: 80, functions: 80, lines: 80, statements: 80 },

packages/agentflow/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@
8080
"@uiw/codemirror-theme-vscode": "^4.21.0",
8181
"@uiw/react-codemirror": "^4.21.0",
8282
"axios": "^1.7.2",
83+
"dompurify": "^3.2.6",
8384
"flowise-react-json-view": "^1.21.7",
85+
"html-react-parser": "^3.0.16",
8486
"lodash": "^4.17.21",
8587
"lowlight": "^3.3.0",
8688
"uuid": "^10.0.0"

packages/agentflow/src/atoms/ArrayInput.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Box, Button, Chip, IconButton } from '@mui/material'
44
import { useTheme } from '@mui/material/styles'
55
import { IconPlus, IconTrash } from '@tabler/icons-react'
66

7+
import { getDefaultValueForType } from '@/core/primitives'
78
import type { InputParam, NodeData } from '@/core/types'
89

910
import { type AsyncInputProps, type ConfigInputComponentProps, NodeInputHandler } from './NodeInputHandler'
@@ -75,23 +76,7 @@ export function ArrayInput({
7576

7677
if (inputParam.array) {
7778
for (const field of inputParam.array) {
78-
if (field.default !== undefined) {
79-
newItem[field.name] = field.default
80-
} else {
81-
switch (field.type) {
82-
case 'number':
83-
newItem[field.name] = 0
84-
break
85-
case 'boolean':
86-
newItem[field.name] = false
87-
break
88-
case 'array':
89-
newItem[field.name] = []
90-
break
91-
default:
92-
newItem[field.name] = ''
93-
}
94-
}
79+
newItem[field.name] = getDefaultValueForType(field)
9580
}
9681
}
9782

packages/agentflow/src/atoms/ConditionBuilder.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Box, Button, Chip, IconButton, Typography } from '@mui/material'
44
import { useTheme } from '@mui/material/styles'
55
import { IconPlus, IconTrash } from '@tabler/icons-react'
66

7+
import { getDefaultValueForType } from '@/core/primitives'
78
import type { InputParam, NodeData } from '@/core/types'
89

910
import { NodeInputHandler } from './NodeInputHandler'
@@ -58,20 +59,7 @@ export function ConditionBuilder({
5859
const newItem: Record<string, unknown> = {}
5960
if (inputParam.array) {
6061
for (const field of inputParam.array) {
61-
if (field.default != null) {
62-
newItem[field.name] = field.default
63-
} else {
64-
switch (field.type) {
65-
case 'number':
66-
newItem[field.name] = 0
67-
break
68-
case 'boolean':
69-
newItem[field.name] = false
70-
break
71-
default:
72-
newItem[field.name] = ''
73-
}
74-
}
62+
newItem[field.name] = getDefaultValueForType(field)
7563
}
7664
}
7765
onDataChange?.({ inputParam, newValue: [...arrayItems, newItem] })
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { fireEvent, render, screen } from '@testing-library/react'
2+
3+
import type { ComponentCredentialSchema } from '@/core/types'
4+
5+
import { CredentialTypeSelector } from './CredentialTypeSelector'
6+
7+
// ─── Mocks ────────────────────────────────────────────────────────────────────
8+
9+
jest.mock('@tabler/icons-react', () => ({
10+
IconKey: () => <span data-testid='icon-key-fallback' />,
11+
IconSearch: () => <span data-testid='icon-search' />,
12+
IconX: (props: { onClick?: () => void }) => <button data-testid='icon-x' onClick={props.onClick} />
13+
}))
14+
15+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
16+
17+
const schemas: ComponentCredentialSchema[] = [
18+
{ label: 'HTTP Basic Auth', name: 'httpBasicAuth', inputs: [] },
19+
{ label: 'HTTP Bearer Token', name: 'httpBearerToken', inputs: [] },
20+
{ label: 'HTTP Api Key', name: 'httpApiKey', inputs: [] }
21+
]
22+
23+
const apiBaseUrl = 'http://localhost:3000'
24+
25+
// ─── Tests ────────────────────────────────────────────────────────────────────
26+
27+
describe('CredentialTypeSelector', () => {
28+
it('renders search input with placeholder', () => {
29+
render(<CredentialTypeSelector schemas={schemas} apiBaseUrl={apiBaseUrl} onSelect={jest.fn()} />)
30+
31+
expect(screen.getByPlaceholderText('Search credential')).toBeInTheDocument()
32+
})
33+
34+
it('renders all credential cards with labels and icons', () => {
35+
render(<CredentialTypeSelector schemas={schemas} apiBaseUrl={apiBaseUrl} onSelect={jest.fn()} />)
36+
37+
expect(screen.getByText('HTTP Basic Auth')).toBeInTheDocument()
38+
expect(screen.getByText('HTTP Bearer Token')).toBeInTheDocument()
39+
expect(screen.getByText('HTTP Api Key')).toBeInTheDocument()
40+
41+
const images = screen.getAllByRole('img')
42+
expect(images).toHaveLength(3)
43+
expect(images[0]).toHaveAttribute('src', 'http://localhost:3000/api/v1/components-credentials-icon/httpBasicAuth')
44+
expect(images[0]).toHaveAttribute('alt', 'httpBasicAuth')
45+
})
46+
47+
it('calls onSelect with the clicked schema', () => {
48+
const onSelect = jest.fn()
49+
render(<CredentialTypeSelector schemas={schemas} apiBaseUrl={apiBaseUrl} onSelect={onSelect} />)
50+
51+
fireEvent.click(screen.getByText('HTTP Bearer Token'))
52+
53+
expect(onSelect).toHaveBeenCalledTimes(1)
54+
expect(onSelect).toHaveBeenCalledWith(schemas[1])
55+
})
56+
57+
it('filters schemas by search input', () => {
58+
render(<CredentialTypeSelector schemas={schemas} apiBaseUrl={apiBaseUrl} onSelect={jest.fn()} />)
59+
60+
const searchInput = screen.getByPlaceholderText('Search credential')
61+
fireEvent.change(searchInput, { target: { value: 'bearer' } })
62+
63+
expect(screen.getByText('HTTP Bearer Token')).toBeInTheDocument()
64+
expect(screen.queryByText('HTTP Basic Auth')).not.toBeInTheDocument()
65+
expect(screen.queryByText('HTTP Api Key')).not.toBeInTheDocument()
66+
})
67+
68+
it('search is case-insensitive', () => {
69+
render(<CredentialTypeSelector schemas={schemas} apiBaseUrl={apiBaseUrl} onSelect={jest.fn()} />)
70+
71+
fireEvent.change(screen.getByPlaceholderText('Search credential'), { target: { value: 'API' } })
72+
73+
expect(screen.getByText('HTTP Api Key')).toBeInTheDocument()
74+
expect(screen.queryByText('HTTP Basic Auth')).not.toBeInTheDocument()
75+
})
76+
77+
it('clears search when clear button is clicked', () => {
78+
render(<CredentialTypeSelector schemas={schemas} apiBaseUrl={apiBaseUrl} onSelect={jest.fn()} />)
79+
80+
const searchInput = screen.getByPlaceholderText('Search credential')
81+
fireEvent.change(searchInput, { target: { value: 'bearer' } })
82+
83+
expect(screen.queryByText('HTTP Basic Auth')).not.toBeInTheDocument()
84+
85+
fireEvent.click(screen.getByTestId('icon-x'))
86+
87+
expect(screen.getByText('HTTP Basic Auth')).toBeInTheDocument()
88+
expect(screen.getByText('HTTP Bearer Token')).toBeInTheDocument()
89+
expect(screen.getByText('HTTP Api Key')).toBeInTheDocument()
90+
})
91+
92+
it('shows no cards when search matches nothing', () => {
93+
render(<CredentialTypeSelector schemas={schemas} apiBaseUrl={apiBaseUrl} onSelect={jest.fn()} />)
94+
95+
fireEvent.change(screen.getByPlaceholderText('Search credential'), { target: { value: 'nonexistent' } })
96+
97+
expect(screen.queryByText('HTTP Basic Auth')).not.toBeInTheDocument()
98+
expect(screen.queryByText('HTTP Bearer Token')).not.toBeInTheDocument()
99+
expect(screen.queryByText('HTTP Api Key')).not.toBeInTheDocument()
100+
expect(screen.queryAllByRole('img')).toHaveLength(0)
101+
})
102+
103+
it('renders empty list when schemas is empty', () => {
104+
render(<CredentialTypeSelector schemas={[]} apiBaseUrl={apiBaseUrl} onSelect={jest.fn()} />)
105+
106+
expect(screen.getByPlaceholderText('Search credential')).toBeInTheDocument()
107+
expect(screen.queryAllByRole('img')).toHaveLength(0)
108+
})
109+
110+
it('shows fallback key icon when credential icon fails to load', () => {
111+
render(<CredentialTypeSelector schemas={[schemas[0]]} apiBaseUrl={apiBaseUrl} onSelect={jest.fn()} />)
112+
113+
const img = screen.getByAltText('httpBasicAuth')
114+
fireEvent.error(img)
115+
116+
expect(screen.queryByAltText('httpBasicAuth')).not.toBeInTheDocument()
117+
expect(screen.getByTestId('icon-key-fallback')).toBeInTheDocument()
118+
})
119+
})

0 commit comments

Comments
 (0)