@runtypelabs/react-flow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +289 -0
- package/example/.env.example +3 -0
- package/example/index.html +25 -0
- package/example/node_modules/.bin/browserslist +21 -0
- package/example/node_modules/.bin/terser +21 -0
- package/example/node_modules/.bin/tsc +21 -0
- package/example/node_modules/.bin/tsserver +21 -0
- package/example/node_modules/.bin/vite +21 -0
- package/example/package.json +26 -0
- package/example/src/App.tsx +1744 -0
- package/example/src/main.tsx +11 -0
- package/example/tsconfig.json +21 -0
- package/example/vite.config.ts +13 -0
- package/package.json +65 -0
- package/src/components/RuntypeFlowEditor.tsx +528 -0
- package/src/components/nodes/BaseNode.tsx +357 -0
- package/src/components/nodes/CodeNode.tsx +252 -0
- package/src/components/nodes/ConditionalNode.tsx +264 -0
- package/src/components/nodes/FetchUrlNode.tsx +299 -0
- package/src/components/nodes/PromptNode.tsx +270 -0
- package/src/components/nodes/SendEmailNode.tsx +311 -0
- package/src/hooks/useFlowValidation.ts +424 -0
- package/src/hooks/useRuntypeFlow.ts +414 -0
- package/src/index.ts +28 -0
- package/src/types/index.ts +332 -0
- package/src/utils/adapter.ts +544 -0
- package/src/utils/layout.ts +284 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React, { memo, useCallback, useState } from 'react'
|
|
2
|
+
import type { NodeProps } from '@xyflow/react'
|
|
3
|
+
import { Position } from '@xyflow/react'
|
|
4
|
+
import { BaseNode, GitBranchIcon, NODE_HEADER_COLORS } from './BaseNode'
|
|
5
|
+
import type { RuntypeNodeData, ConditionalStepConfig, FlowStep } from '../../types'
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Styles
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
const styles = {
|
|
12
|
+
field: {
|
|
13
|
+
marginBottom: '12px',
|
|
14
|
+
},
|
|
15
|
+
label: {
|
|
16
|
+
display: 'block',
|
|
17
|
+
fontSize: '11px',
|
|
18
|
+
fontWeight: 600,
|
|
19
|
+
color: '#6b7280',
|
|
20
|
+
marginBottom: '4px',
|
|
21
|
+
textTransform: 'uppercase' as const,
|
|
22
|
+
letterSpacing: '0.03em',
|
|
23
|
+
},
|
|
24
|
+
input: {
|
|
25
|
+
width: '100%',
|
|
26
|
+
padding: '8px 10px',
|
|
27
|
+
fontSize: '12px',
|
|
28
|
+
border: '1px solid #e5e7eb',
|
|
29
|
+
borderRadius: '6px',
|
|
30
|
+
backgroundColor: '#f9fafb',
|
|
31
|
+
color: '#1f2937',
|
|
32
|
+
outline: 'none',
|
|
33
|
+
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
|
|
34
|
+
},
|
|
35
|
+
conditionArea: {
|
|
36
|
+
width: '100%',
|
|
37
|
+
padding: '10px',
|
|
38
|
+
fontSize: '12px',
|
|
39
|
+
border: '1px solid #e5e7eb',
|
|
40
|
+
borderRadius: '6px',
|
|
41
|
+
backgroundColor: '#fef7ee',
|
|
42
|
+
color: '#92400e',
|
|
43
|
+
outline: 'none',
|
|
44
|
+
resize: 'vertical' as const,
|
|
45
|
+
minHeight: '60px',
|
|
46
|
+
fontFamily: '"Fira Code", "Monaco", "Consolas", monospace',
|
|
47
|
+
lineHeight: 1.5,
|
|
48
|
+
},
|
|
49
|
+
branchInfo: {
|
|
50
|
+
display: 'flex',
|
|
51
|
+
gap: '12px',
|
|
52
|
+
marginTop: '8px',
|
|
53
|
+
},
|
|
54
|
+
branchCard: (type: 'true' | 'false') => ({
|
|
55
|
+
flex: 1,
|
|
56
|
+
padding: '10px',
|
|
57
|
+
borderRadius: '8px',
|
|
58
|
+
backgroundColor: type === 'true' ? '#d1fae5' : '#fee2e2',
|
|
59
|
+
border: `1px solid ${type === 'true' ? '#a7f3d0' : '#fecaca'}`,
|
|
60
|
+
}),
|
|
61
|
+
branchLabel: (type: 'true' | 'false') => ({
|
|
62
|
+
fontSize: '10px',
|
|
63
|
+
fontWeight: 700,
|
|
64
|
+
textTransform: 'uppercase' as const,
|
|
65
|
+
letterSpacing: '0.05em',
|
|
66
|
+
color: type === 'true' ? '#059669' : '#dc2626',
|
|
67
|
+
marginBottom: '4px',
|
|
68
|
+
}),
|
|
69
|
+
branchCount: {
|
|
70
|
+
fontSize: '18px',
|
|
71
|
+
fontWeight: 700,
|
|
72
|
+
color: '#1f2937',
|
|
73
|
+
},
|
|
74
|
+
branchCountLabel: {
|
|
75
|
+
fontSize: '10px',
|
|
76
|
+
color: '#6b7280',
|
|
77
|
+
},
|
|
78
|
+
helpText: {
|
|
79
|
+
fontSize: '10px',
|
|
80
|
+
color: '#9ca3af',
|
|
81
|
+
marginTop: '4px',
|
|
82
|
+
fontStyle: 'italic' as const,
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Conditional Node Component
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
export const ConditionalNode = memo(function ConditionalNode(props: NodeProps) {
|
|
91
|
+
const { data, selected, id } = props as NodeProps & { data: RuntypeNodeData }
|
|
92
|
+
const { step, onChange } = data
|
|
93
|
+
const config = step.config as ConditionalStepConfig
|
|
94
|
+
|
|
95
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
96
|
+
|
|
97
|
+
const handleChange = useCallback(
|
|
98
|
+
(field: keyof ConditionalStepConfig, value: unknown) => {
|
|
99
|
+
onChange?.(id, {
|
|
100
|
+
config: {
|
|
101
|
+
...config,
|
|
102
|
+
[field]: value,
|
|
103
|
+
},
|
|
104
|
+
} as Partial<FlowStep>)
|
|
105
|
+
},
|
|
106
|
+
[id, config, onChange]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const handleNameChange = useCallback(
|
|
110
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
111
|
+
onChange?.(id, { name: e.target.value })
|
|
112
|
+
},
|
|
113
|
+
[id, onChange]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const trueStepsCount = config.trueSteps?.length || 0
|
|
117
|
+
const falseStepsCount = config.falseSteps?.length || 0
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<BaseNode
|
|
121
|
+
data={data}
|
|
122
|
+
selected={selected}
|
|
123
|
+
id={id}
|
|
124
|
+
typeLabel="Conditional"
|
|
125
|
+
icon={<GitBranchIcon />}
|
|
126
|
+
headerColor={NODE_HEADER_COLORS.conditional}
|
|
127
|
+
showSourceHandle={false}
|
|
128
|
+
additionalSourceHandles={[
|
|
129
|
+
{
|
|
130
|
+
id: 'true',
|
|
131
|
+
position: Position.Right,
|
|
132
|
+
label: 'True',
|
|
133
|
+
color: '#22c55e',
|
|
134
|
+
style: { top: '40%' },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'false',
|
|
138
|
+
position: Position.Right,
|
|
139
|
+
label: 'False',
|
|
140
|
+
color: '#ef4444',
|
|
141
|
+
style: { top: '60%' },
|
|
142
|
+
},
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
<div style={styles.field}>
|
|
146
|
+
<label style={styles.label}>Step Name</label>
|
|
147
|
+
<input
|
|
148
|
+
style={styles.input}
|
|
149
|
+
value={step.name}
|
|
150
|
+
onChange={handleNameChange}
|
|
151
|
+
placeholder="Enter step name"
|
|
152
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div style={styles.field}>
|
|
157
|
+
<label style={styles.label}>Condition (JavaScript)</label>
|
|
158
|
+
<textarea
|
|
159
|
+
style={styles.conditionArea}
|
|
160
|
+
value={config.condition || ''}
|
|
161
|
+
onChange={(e) => handleChange('condition', e.target.value)}
|
|
162
|
+
placeholder="user_type === 'premium'"
|
|
163
|
+
spellCheck={false}
|
|
164
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
165
|
+
/>
|
|
166
|
+
<div style={styles.helpText}>
|
|
167
|
+
Access variables directly: <code>variable_name</code>, <code>_record.metadata.field</code>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div style={styles.branchInfo}>
|
|
172
|
+
<div style={styles.branchCard('true')}>
|
|
173
|
+
<div style={styles.branchLabel('true')}>
|
|
174
|
+
<TrueIcon /> True Branch
|
|
175
|
+
</div>
|
|
176
|
+
<div style={styles.branchCount}>{trueStepsCount}</div>
|
|
177
|
+
<div style={styles.branchCountLabel}>
|
|
178
|
+
step{trueStepsCount !== 1 ? 's' : ''}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div style={styles.branchCard('false')}>
|
|
182
|
+
<div style={styles.branchLabel('false')}>
|
|
183
|
+
<FalseIcon /> False Branch
|
|
184
|
+
</div>
|
|
185
|
+
<div style={styles.branchCount}>{falseStepsCount}</div>
|
|
186
|
+
<div style={styles.branchCountLabel}>
|
|
187
|
+
step{falseStepsCount !== 1 ? 's' : ''}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{isExpanded && (
|
|
193
|
+
<div style={{ marginTop: '12px' }}>
|
|
194
|
+
<div style={styles.field}>
|
|
195
|
+
<label style={styles.label}>Condition Examples</label>
|
|
196
|
+
<div style={{ fontSize: '11px', color: '#6b7280', lineHeight: 1.6 }}>
|
|
197
|
+
<div><code>status === 'active'</code> - Check equality</div>
|
|
198
|
+
<div><code>count {'>'} 10</code> - Numeric comparison</div>
|
|
199
|
+
<div><code>data && data.length {'>'} 0</code> - Check array</div>
|
|
200
|
+
<div><code>_record.metadata.type === 'premium'</code> - API input</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
208
|
+
style={{
|
|
209
|
+
width: '100%',
|
|
210
|
+
padding: '6px',
|
|
211
|
+
fontSize: '11px',
|
|
212
|
+
color: '#6366f1',
|
|
213
|
+
backgroundColor: 'transparent',
|
|
214
|
+
border: '1px dashed #e5e7eb',
|
|
215
|
+
borderRadius: '6px',
|
|
216
|
+
cursor: 'pointer',
|
|
217
|
+
marginTop: '12px',
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
{isExpanded ? 'Hide Examples' : 'Show Examples'}
|
|
221
|
+
</button>
|
|
222
|
+
</BaseNode>
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// ============================================================================
|
|
227
|
+
// Icon Components
|
|
228
|
+
// ============================================================================
|
|
229
|
+
|
|
230
|
+
const TrueIcon = () => (
|
|
231
|
+
<svg
|
|
232
|
+
width="12"
|
|
233
|
+
height="12"
|
|
234
|
+
viewBox="0 0 24 24"
|
|
235
|
+
fill="none"
|
|
236
|
+
stroke="currentColor"
|
|
237
|
+
strokeWidth="2"
|
|
238
|
+
strokeLinecap="round"
|
|
239
|
+
strokeLinejoin="round"
|
|
240
|
+
style={{ display: 'inline', marginRight: '4px', verticalAlign: 'middle' }}
|
|
241
|
+
>
|
|
242
|
+
<polyline points="20 6 9 17 4 12" />
|
|
243
|
+
</svg>
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
const FalseIcon = () => (
|
|
247
|
+
<svg
|
|
248
|
+
width="12"
|
|
249
|
+
height="12"
|
|
250
|
+
viewBox="0 0 24 24"
|
|
251
|
+
fill="none"
|
|
252
|
+
stroke="currentColor"
|
|
253
|
+
strokeWidth="2"
|
|
254
|
+
strokeLinecap="round"
|
|
255
|
+
strokeLinejoin="round"
|
|
256
|
+
style={{ display: 'inline', marginRight: '4px', verticalAlign: 'middle' }}
|
|
257
|
+
>
|
|
258
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
259
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
260
|
+
</svg>
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
export default ConditionalNode
|
|
264
|
+
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import React, { memo, useCallback, useState } from 'react'
|
|
2
|
+
import type { NodeProps } from '@xyflow/react'
|
|
3
|
+
import { BaseNode, GlobeIcon, NODE_HEADER_COLORS } from './BaseNode'
|
|
4
|
+
import type { RuntypeNodeData, FetchUrlStepConfig, FlowStep } from '../../types'
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Styles
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
const styles = {
|
|
11
|
+
field: {
|
|
12
|
+
marginBottom: '12px',
|
|
13
|
+
},
|
|
14
|
+
label: {
|
|
15
|
+
display: 'block',
|
|
16
|
+
fontSize: '11px',
|
|
17
|
+
fontWeight: 600,
|
|
18
|
+
color: '#6b7280',
|
|
19
|
+
marginBottom: '4px',
|
|
20
|
+
textTransform: 'uppercase' as const,
|
|
21
|
+
letterSpacing: '0.03em',
|
|
22
|
+
},
|
|
23
|
+
input: {
|
|
24
|
+
width: '100%',
|
|
25
|
+
padding: '8px 10px',
|
|
26
|
+
fontSize: '12px',
|
|
27
|
+
border: '1px solid #e5e7eb',
|
|
28
|
+
borderRadius: '6px',
|
|
29
|
+
backgroundColor: '#f9fafb',
|
|
30
|
+
color: '#1f2937',
|
|
31
|
+
outline: 'none',
|
|
32
|
+
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
|
|
33
|
+
},
|
|
34
|
+
textarea: {
|
|
35
|
+
width: '100%',
|
|
36
|
+
padding: '8px 10px',
|
|
37
|
+
fontSize: '12px',
|
|
38
|
+
border: '1px solid #e5e7eb',
|
|
39
|
+
borderRadius: '6px',
|
|
40
|
+
backgroundColor: '#f9fafb',
|
|
41
|
+
color: '#1f2937',
|
|
42
|
+
outline: 'none',
|
|
43
|
+
resize: 'vertical' as const,
|
|
44
|
+
minHeight: '60px',
|
|
45
|
+
fontFamily: 'monospace',
|
|
46
|
+
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
|
|
47
|
+
},
|
|
48
|
+
select: {
|
|
49
|
+
width: '100%',
|
|
50
|
+
padding: '8px 10px',
|
|
51
|
+
fontSize: '12px',
|
|
52
|
+
border: '1px solid #e5e7eb',
|
|
53
|
+
borderRadius: '6px',
|
|
54
|
+
backgroundColor: '#f9fafb',
|
|
55
|
+
color: '#1f2937',
|
|
56
|
+
outline: 'none',
|
|
57
|
+
cursor: 'pointer',
|
|
58
|
+
},
|
|
59
|
+
row: {
|
|
60
|
+
display: 'flex',
|
|
61
|
+
gap: '8px',
|
|
62
|
+
},
|
|
63
|
+
methodBadge: (method: string) => {
|
|
64
|
+
const colors: Record<string, { bg: string; text: string }> = {
|
|
65
|
+
GET: { bg: '#d1fae5', text: '#059669' },
|
|
66
|
+
POST: { bg: '#dbeafe', text: '#1d4ed8' },
|
|
67
|
+
PUT: { bg: '#fef3c7', text: '#d97706' },
|
|
68
|
+
DELETE: { bg: '#fee2e2', text: '#dc2626' },
|
|
69
|
+
PATCH: { bg: '#e0e7ff', text: '#4f46e5' },
|
|
70
|
+
}
|
|
71
|
+
const color = colors[method] || colors.GET
|
|
72
|
+
return {
|
|
73
|
+
display: 'inline-flex',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
padding: '2px 8px',
|
|
76
|
+
borderRadius: '4px',
|
|
77
|
+
fontSize: '10px',
|
|
78
|
+
fontWeight: 700,
|
|
79
|
+
backgroundColor: color.bg,
|
|
80
|
+
color: color.text,
|
|
81
|
+
fontFamily: 'monospace',
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
urlPreview: {
|
|
85
|
+
fontSize: '11px',
|
|
86
|
+
color: '#6b7280',
|
|
87
|
+
backgroundColor: '#f3f4f6',
|
|
88
|
+
padding: '6px 8px',
|
|
89
|
+
borderRadius: '4px',
|
|
90
|
+
fontFamily: 'monospace',
|
|
91
|
+
wordBreak: 'break-all' as const,
|
|
92
|
+
marginTop: '4px',
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Fetch URL Node Component
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export const FetchUrlNode = memo(function FetchUrlNode(props: NodeProps) {
|
|
101
|
+
const { data, selected, id } = props as NodeProps & { data: RuntypeNodeData }
|
|
102
|
+
const { step, onChange } = data
|
|
103
|
+
const config = step.config as FetchUrlStepConfig
|
|
104
|
+
|
|
105
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
106
|
+
|
|
107
|
+
const handleChange = useCallback(
|
|
108
|
+
(field: string, value: unknown) => {
|
|
109
|
+
if (field.startsWith('http.')) {
|
|
110
|
+
const httpField = field.replace('http.', '')
|
|
111
|
+
onChange?.(id, {
|
|
112
|
+
config: {
|
|
113
|
+
...config,
|
|
114
|
+
http: {
|
|
115
|
+
...config.http,
|
|
116
|
+
[httpField]: value,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
} as Partial<FlowStep>)
|
|
120
|
+
} else {
|
|
121
|
+
onChange?.(id, {
|
|
122
|
+
config: {
|
|
123
|
+
...config,
|
|
124
|
+
[field]: value,
|
|
125
|
+
},
|
|
126
|
+
} as Partial<FlowStep>)
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[id, config, onChange]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const handleNameChange = useCallback(
|
|
133
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
134
|
+
// console.log('[FetchUrlNode] Name change:', e.target.value)
|
|
135
|
+
onChange?.(id, { name: e.target.value })
|
|
136
|
+
},
|
|
137
|
+
[id, onChange]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const method = config.http?.method || 'GET'
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<BaseNode
|
|
144
|
+
data={data}
|
|
145
|
+
selected={selected}
|
|
146
|
+
id={id}
|
|
147
|
+
typeLabel="Fetch URL"
|
|
148
|
+
icon={<GlobeIcon />}
|
|
149
|
+
headerColor={NODE_HEADER_COLORS['fetch-url']}
|
|
150
|
+
>
|
|
151
|
+
<div style={styles.field}>
|
|
152
|
+
<label style={styles.label}>Step Name</label>
|
|
153
|
+
<input
|
|
154
|
+
style={styles.input}
|
|
155
|
+
value={step.name}
|
|
156
|
+
onChange={handleNameChange}
|
|
157
|
+
placeholder="Enter step name"
|
|
158
|
+
onKeyDown={(e) => {
|
|
159
|
+
// console.log('[FetchUrlNode] KeyDown:', e.key)
|
|
160
|
+
e.stopPropagation()
|
|
161
|
+
}}
|
|
162
|
+
// onInput={(e) => console.log('[FetchUrlNode] Input event:', (e.target as HTMLInputElement).value)}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div style={styles.field}>
|
|
167
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
|
|
168
|
+
<label style={styles.label}>Method</label>
|
|
169
|
+
<span style={styles.methodBadge(method)}>{method}</span>
|
|
170
|
+
</div>
|
|
171
|
+
<select
|
|
172
|
+
style={styles.select}
|
|
173
|
+
value={method}
|
|
174
|
+
onChange={(e) => handleChange('http.method', e.target.value)}
|
|
175
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
176
|
+
>
|
|
177
|
+
<option value="GET">GET</option>
|
|
178
|
+
<option value="POST">POST</option>
|
|
179
|
+
<option value="PUT">PUT</option>
|
|
180
|
+
<option value="DELETE">DELETE</option>
|
|
181
|
+
<option value="PATCH">PATCH</option>
|
|
182
|
+
</select>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div style={styles.field}>
|
|
186
|
+
<label style={styles.label}>URL</label>
|
|
187
|
+
<input
|
|
188
|
+
style={styles.input}
|
|
189
|
+
value={config.http?.url || ''}
|
|
190
|
+
onChange={(e) => handleChange('http.url', e.target.value)}
|
|
191
|
+
placeholder="https://api.example.com/endpoint"
|
|
192
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
193
|
+
/>
|
|
194
|
+
{config.http?.url && (
|
|
195
|
+
<div style={styles.urlPreview}>
|
|
196
|
+
{config.http.url}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{isExpanded && (
|
|
202
|
+
<>
|
|
203
|
+
{(method === 'POST' || method === 'PUT' || method === 'PATCH') && (
|
|
204
|
+
<div style={styles.field}>
|
|
205
|
+
<label style={styles.label}>Request Body</label>
|
|
206
|
+
<textarea
|
|
207
|
+
style={styles.textarea}
|
|
208
|
+
value={config.http?.body || ''}
|
|
209
|
+
onChange={(e) => handleChange('http.body', e.target.value)}
|
|
210
|
+
placeholder='{"key": "value"}'
|
|
211
|
+
rows={3}
|
|
212
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
<div style={styles.field}>
|
|
218
|
+
<label style={styles.label}>Headers (JSON)</label>
|
|
219
|
+
<textarea
|
|
220
|
+
style={styles.textarea}
|
|
221
|
+
value={config.http?.headers ? JSON.stringify(config.http.headers, null, 2) : ''}
|
|
222
|
+
onChange={(e) => {
|
|
223
|
+
try {
|
|
224
|
+
const headers = JSON.parse(e.target.value)
|
|
225
|
+
handleChange('http.headers', headers)
|
|
226
|
+
} catch {
|
|
227
|
+
// Invalid JSON, don't update
|
|
228
|
+
}
|
|
229
|
+
}}
|
|
230
|
+
placeholder='{"Content-Type": "application/json"}'
|
|
231
|
+
rows={2}
|
|
232
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div style={styles.row}>
|
|
237
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
238
|
+
<label style={styles.label}>Response Type</label>
|
|
239
|
+
<select
|
|
240
|
+
style={styles.select}
|
|
241
|
+
value={config.responseType || 'json'}
|
|
242
|
+
onChange={(e) => handleChange('responseType', e.target.value)}
|
|
243
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
244
|
+
>
|
|
245
|
+
<option value="json">JSON</option>
|
|
246
|
+
<option value="text">Text</option>
|
|
247
|
+
<option value="xml">XML</option>
|
|
248
|
+
</select>
|
|
249
|
+
</div>
|
|
250
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
251
|
+
<label style={styles.label}>On Error</label>
|
|
252
|
+
<select
|
|
253
|
+
style={styles.select}
|
|
254
|
+
value={config.errorHandling || 'fail'}
|
|
255
|
+
onChange={(e) => handleChange('errorHandling', e.target.value)}
|
|
256
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
257
|
+
>
|
|
258
|
+
<option value="fail">Fail</option>
|
|
259
|
+
<option value="continue">Continue</option>
|
|
260
|
+
<option value="default">Use Default</option>
|
|
261
|
+
</select>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div style={styles.field}>
|
|
266
|
+
<label style={styles.label}>Output Variable</label>
|
|
267
|
+
<input
|
|
268
|
+
style={styles.input}
|
|
269
|
+
value={config.outputVariable || ''}
|
|
270
|
+
onChange={(e) => handleChange('outputVariable', e.target.value)}
|
|
271
|
+
placeholder="api_response"
|
|
272
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
</>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
<button
|
|
279
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
280
|
+
style={{
|
|
281
|
+
width: '100%',
|
|
282
|
+
padding: '6px',
|
|
283
|
+
fontSize: '11px',
|
|
284
|
+
color: '#6366f1',
|
|
285
|
+
backgroundColor: 'transparent',
|
|
286
|
+
border: '1px dashed #e5e7eb',
|
|
287
|
+
borderRadius: '6px',
|
|
288
|
+
cursor: 'pointer',
|
|
289
|
+
marginTop: '4px',
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
{isExpanded ? 'Show Less' : 'Show More Options'}
|
|
293
|
+
</button>
|
|
294
|
+
</BaseNode>
|
|
295
|
+
)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
export default FetchUrlNode
|
|
299
|
+
|