@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,357 @@
|
|
|
1
|
+
import React, { memo } from 'react'
|
|
2
|
+
import { Handle, Position } from '@xyflow/react'
|
|
3
|
+
import type { RuntypeNodeData, FlowStep } from '../../types'
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export interface BaseNodeProps {
|
|
10
|
+
/** Node data containing the step */
|
|
11
|
+
data: RuntypeNodeData
|
|
12
|
+
/** Whether the node is selected */
|
|
13
|
+
selected?: boolean
|
|
14
|
+
/** Node ID */
|
|
15
|
+
id: string
|
|
16
|
+
/** Node type label */
|
|
17
|
+
typeLabel: string
|
|
18
|
+
/** Icon component to display */
|
|
19
|
+
icon: React.ReactNode
|
|
20
|
+
/** Header color (Tailwind class or CSS color) */
|
|
21
|
+
headerColor?: string
|
|
22
|
+
/** Whether to show source handle */
|
|
23
|
+
showSourceHandle?: boolean
|
|
24
|
+
/** Whether to show target handle */
|
|
25
|
+
showTargetHandle?: boolean
|
|
26
|
+
/** Additional source handles for conditional branches */
|
|
27
|
+
additionalSourceHandles?: Array<{
|
|
28
|
+
id: string
|
|
29
|
+
position: Position
|
|
30
|
+
label?: string
|
|
31
|
+
color?: string
|
|
32
|
+
style?: React.CSSProperties
|
|
33
|
+
}>
|
|
34
|
+
/** Children to render in the node body */
|
|
35
|
+
children: React.ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Styles
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
const styles = {
|
|
43
|
+
container: (selected: boolean) => ({
|
|
44
|
+
minWidth: '280px',
|
|
45
|
+
maxWidth: '320px',
|
|
46
|
+
backgroundColor: '#ffffff',
|
|
47
|
+
borderRadius: '12px',
|
|
48
|
+
border: selected ? '2px solid #6366f1' : '1px solid #e5e7eb',
|
|
49
|
+
boxShadow: selected
|
|
50
|
+
? '0 0 0 2px rgba(99, 102, 241, 0.2), 0 4px 6px -1px rgba(0, 0, 0, 0.1)'
|
|
51
|
+
: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
|
|
52
|
+
overflow: 'hidden',
|
|
53
|
+
transition: 'border-color 0.15s ease, box-shadow 0.15s ease',
|
|
54
|
+
}),
|
|
55
|
+
header: (color: string) => ({
|
|
56
|
+
display: 'flex',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
gap: '8px',
|
|
59
|
+
padding: '10px 12px',
|
|
60
|
+
backgroundColor: color,
|
|
61
|
+
borderBottom: '1px solid rgba(0, 0, 0, 0.05)',
|
|
62
|
+
}),
|
|
63
|
+
iconWrapper: {
|
|
64
|
+
display: 'flex',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
width: '28px',
|
|
68
|
+
height: '28px',
|
|
69
|
+
borderRadius: '6px',
|
|
70
|
+
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
|
71
|
+
color: '#374151',
|
|
72
|
+
},
|
|
73
|
+
headerContent: {
|
|
74
|
+
flex: 1,
|
|
75
|
+
minWidth: 0,
|
|
76
|
+
},
|
|
77
|
+
typeLabel: {
|
|
78
|
+
fontSize: '10px',
|
|
79
|
+
fontWeight: 600,
|
|
80
|
+
textTransform: 'uppercase' as const,
|
|
81
|
+
letterSpacing: '0.05em',
|
|
82
|
+
color: 'rgba(0, 0, 0, 0.5)',
|
|
83
|
+
marginBottom: '2px',
|
|
84
|
+
},
|
|
85
|
+
stepName: {
|
|
86
|
+
fontSize: '13px',
|
|
87
|
+
fontWeight: 600,
|
|
88
|
+
color: '#1f2937',
|
|
89
|
+
overflow: 'hidden',
|
|
90
|
+
textOverflow: 'ellipsis',
|
|
91
|
+
whiteSpace: 'nowrap' as const,
|
|
92
|
+
},
|
|
93
|
+
body: {
|
|
94
|
+
padding: '12px',
|
|
95
|
+
},
|
|
96
|
+
handle: {
|
|
97
|
+
width: '12px',
|
|
98
|
+
height: '12px',
|
|
99
|
+
borderRadius: '50%',
|
|
100
|
+
border: '2px solid #ffffff',
|
|
101
|
+
},
|
|
102
|
+
targetHandle: {
|
|
103
|
+
backgroundColor: '#6366f1',
|
|
104
|
+
},
|
|
105
|
+
sourceHandle: {
|
|
106
|
+
backgroundColor: '#10b981',
|
|
107
|
+
},
|
|
108
|
+
deleteButton: {
|
|
109
|
+
padding: '4px',
|
|
110
|
+
borderRadius: '4px',
|
|
111
|
+
backgroundColor: 'transparent',
|
|
112
|
+
border: 'none',
|
|
113
|
+
cursor: 'pointer',
|
|
114
|
+
color: '#9ca3af',
|
|
115
|
+
display: 'flex',
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
justifyContent: 'center',
|
|
118
|
+
transition: 'color 0.15s ease, background-color 0.15s ease',
|
|
119
|
+
},
|
|
120
|
+
enabledBadge: (enabled: boolean) => ({
|
|
121
|
+
display: 'inline-flex',
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
padding: '2px 6px',
|
|
124
|
+
borderRadius: '4px',
|
|
125
|
+
fontSize: '10px',
|
|
126
|
+
fontWeight: 500,
|
|
127
|
+
backgroundColor: enabled ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
|
|
128
|
+
color: enabled ? '#059669' : '#dc2626',
|
|
129
|
+
}),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Header Color Presets
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
export const NODE_HEADER_COLORS: Record<string, string> = {
|
|
137
|
+
prompt: '#f3e8ff', // Purple tint
|
|
138
|
+
'fetch-url': '#dbeafe', // Blue tint
|
|
139
|
+
'transform-data': '#fef3c7', // Amber tint
|
|
140
|
+
conditional: '#fce7f3', // Pink tint
|
|
141
|
+
'send-email': '#d1fae5', // Green tint
|
|
142
|
+
default: '#f3f4f6', // Gray tint
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Base Node Component
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
export const BaseNode = memo(function BaseNode({
|
|
150
|
+
data,
|
|
151
|
+
selected = false,
|
|
152
|
+
id,
|
|
153
|
+
typeLabel,
|
|
154
|
+
icon,
|
|
155
|
+
headerColor,
|
|
156
|
+
showSourceHandle = true,
|
|
157
|
+
showTargetHandle = true,
|
|
158
|
+
additionalSourceHandles,
|
|
159
|
+
children,
|
|
160
|
+
}: BaseNodeProps) {
|
|
161
|
+
const { step, onChange, onDelete } = data
|
|
162
|
+
const color = headerColor || NODE_HEADER_COLORS[step.type] || NODE_HEADER_COLORS.default
|
|
163
|
+
|
|
164
|
+
const handleDelete = (e: React.MouseEvent) => {
|
|
165
|
+
e.stopPropagation()
|
|
166
|
+
onDelete?.(id)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div style={styles.container(selected)}>
|
|
171
|
+
{/* Target Handle (input) - on left for horizontal flow */}
|
|
172
|
+
{showTargetHandle && (
|
|
173
|
+
<Handle
|
|
174
|
+
type="target"
|
|
175
|
+
position={Position.Left}
|
|
176
|
+
style={{ ...styles.handle, ...styles.targetHandle }}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
{/* Header */}
|
|
181
|
+
<div style={styles.header(color)}>
|
|
182
|
+
<div style={styles.iconWrapper}>{icon}</div>
|
|
183
|
+
<div style={styles.headerContent}>
|
|
184
|
+
<div style={styles.typeLabel}>{typeLabel}</div>
|
|
185
|
+
<div style={styles.stepName} title={step.name}>
|
|
186
|
+
{step.name || 'Untitled Step'}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
190
|
+
<span style={styles.enabledBadge(step.enabled)}>
|
|
191
|
+
{step.enabled ? 'Active' : 'Disabled'}
|
|
192
|
+
</span>
|
|
193
|
+
{onDelete && (
|
|
194
|
+
<button
|
|
195
|
+
style={styles.deleteButton}
|
|
196
|
+
onClick={handleDelete}
|
|
197
|
+
title="Delete step"
|
|
198
|
+
onMouseEnter={(e) => {
|
|
199
|
+
e.currentTarget.style.backgroundColor = 'rgba(239, 68, 68, 0.1)'
|
|
200
|
+
e.currentTarget.style.color = '#dc2626'
|
|
201
|
+
}}
|
|
202
|
+
onMouseLeave={(e) => {
|
|
203
|
+
e.currentTarget.style.backgroundColor = 'transparent'
|
|
204
|
+
e.currentTarget.style.color = '#9ca3af'
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<DeleteIcon />
|
|
208
|
+
</button>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Body - nodrag class prevents React Flow from intercepting events */}
|
|
214
|
+
<div style={styles.body} className="nodrag">{children}</div>
|
|
215
|
+
|
|
216
|
+
{/* Source Handle (output) - on right for horizontal flow */}
|
|
217
|
+
{showSourceHandle && (
|
|
218
|
+
<Handle
|
|
219
|
+
type="source"
|
|
220
|
+
position={Position.Right}
|
|
221
|
+
id="output"
|
|
222
|
+
style={{ ...styles.handle, ...styles.sourceHandle }}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* Additional source handles for conditional branches */}
|
|
227
|
+
{additionalSourceHandles?.map((handle) => (
|
|
228
|
+
<Handle
|
|
229
|
+
key={handle.id}
|
|
230
|
+
type="source"
|
|
231
|
+
position={handle.position}
|
|
232
|
+
id={handle.id}
|
|
233
|
+
style={{
|
|
234
|
+
...styles.handle,
|
|
235
|
+
backgroundColor: handle.color || '#10b981',
|
|
236
|
+
...handle.style,
|
|
237
|
+
}}
|
|
238
|
+
/>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Icon Components
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
const DeleteIcon = () => (
|
|
249
|
+
<svg
|
|
250
|
+
width="14"
|
|
251
|
+
height="14"
|
|
252
|
+
viewBox="0 0 24 24"
|
|
253
|
+
fill="none"
|
|
254
|
+
stroke="currentColor"
|
|
255
|
+
strokeWidth="2"
|
|
256
|
+
strokeLinecap="round"
|
|
257
|
+
strokeLinejoin="round"
|
|
258
|
+
>
|
|
259
|
+
<path d="M3 6h18" />
|
|
260
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
261
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
262
|
+
</svg>
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
// Export common icons for use in step nodes
|
|
266
|
+
export const BrainIcon = () => (
|
|
267
|
+
<svg
|
|
268
|
+
width="16"
|
|
269
|
+
height="16"
|
|
270
|
+
viewBox="0 0 24 24"
|
|
271
|
+
fill="none"
|
|
272
|
+
stroke="currentColor"
|
|
273
|
+
strokeWidth="2"
|
|
274
|
+
strokeLinecap="round"
|
|
275
|
+
strokeLinejoin="round"
|
|
276
|
+
>
|
|
277
|
+
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
|
|
278
|
+
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
|
|
279
|
+
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
|
|
280
|
+
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
|
|
281
|
+
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
|
|
282
|
+
<path d="M3.477 10.896a4 4 0 0 1 .585-.396" />
|
|
283
|
+
<path d="M19.938 10.5a4 4 0 0 1 .585.396" />
|
|
284
|
+
<path d="M6 18a4 4 0 0 1-1.967-.516" />
|
|
285
|
+
<path d="M19.967 17.484A4 4 0 0 1 18 18" />
|
|
286
|
+
</svg>
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
export const GlobeIcon = () => (
|
|
290
|
+
<svg
|
|
291
|
+
width="16"
|
|
292
|
+
height="16"
|
|
293
|
+
viewBox="0 0 24 24"
|
|
294
|
+
fill="none"
|
|
295
|
+
stroke="currentColor"
|
|
296
|
+
strokeWidth="2"
|
|
297
|
+
strokeLinecap="round"
|
|
298
|
+
strokeLinejoin="round"
|
|
299
|
+
>
|
|
300
|
+
<circle cx="12" cy="12" r="10" />
|
|
301
|
+
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
|
302
|
+
<path d="M2 12h20" />
|
|
303
|
+
</svg>
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
export const CodeIcon = () => (
|
|
307
|
+
<svg
|
|
308
|
+
width="16"
|
|
309
|
+
height="16"
|
|
310
|
+
viewBox="0 0 24 24"
|
|
311
|
+
fill="none"
|
|
312
|
+
stroke="currentColor"
|
|
313
|
+
strokeWidth="2"
|
|
314
|
+
strokeLinecap="round"
|
|
315
|
+
strokeLinejoin="round"
|
|
316
|
+
>
|
|
317
|
+
<polyline points="16 18 22 12 16 6" />
|
|
318
|
+
<polyline points="8 6 2 12 8 18" />
|
|
319
|
+
</svg>
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
export const GitBranchIcon = () => (
|
|
323
|
+
<svg
|
|
324
|
+
width="16"
|
|
325
|
+
height="16"
|
|
326
|
+
viewBox="0 0 24 24"
|
|
327
|
+
fill="none"
|
|
328
|
+
stroke="currentColor"
|
|
329
|
+
strokeWidth="2"
|
|
330
|
+
strokeLinecap="round"
|
|
331
|
+
strokeLinejoin="round"
|
|
332
|
+
>
|
|
333
|
+
<line x1="6" y1="3" x2="6" y2="15" />
|
|
334
|
+
<circle cx="18" cy="6" r="3" />
|
|
335
|
+
<circle cx="6" cy="18" r="3" />
|
|
336
|
+
<path d="M18 9a9 9 0 0 1-9 9" />
|
|
337
|
+
</svg>
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
export const MailIcon = () => (
|
|
341
|
+
<svg
|
|
342
|
+
width="16"
|
|
343
|
+
height="16"
|
|
344
|
+
viewBox="0 0 24 24"
|
|
345
|
+
fill="none"
|
|
346
|
+
stroke="currentColor"
|
|
347
|
+
strokeWidth="2"
|
|
348
|
+
strokeLinecap="round"
|
|
349
|
+
strokeLinejoin="round"
|
|
350
|
+
>
|
|
351
|
+
<rect width="20" height="16" x="2" y="4" rx="2" />
|
|
352
|
+
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
|
353
|
+
</svg>
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
export default BaseNode
|
|
357
|
+
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import React, { memo, useCallback, useState } from 'react'
|
|
2
|
+
import type { NodeProps } from '@xyflow/react'
|
|
3
|
+
import { BaseNode, CodeIcon, NODE_HEADER_COLORS } from './BaseNode'
|
|
4
|
+
import type { RuntypeNodeData, TransformDataStepConfig, 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
|
+
codeArea: {
|
|
35
|
+
width: '100%',
|
|
36
|
+
padding: '10px',
|
|
37
|
+
fontSize: '11px',
|
|
38
|
+
border: '1px solid #e5e7eb',
|
|
39
|
+
borderRadius: '6px',
|
|
40
|
+
backgroundColor: '#1e1e1e',
|
|
41
|
+
color: '#d4d4d4',
|
|
42
|
+
outline: 'none',
|
|
43
|
+
resize: 'vertical' as const,
|
|
44
|
+
minHeight: '100px',
|
|
45
|
+
fontFamily: '"Fira Code", "Monaco", "Consolas", monospace',
|
|
46
|
+
lineHeight: 1.5,
|
|
47
|
+
tabSize: 2,
|
|
48
|
+
},
|
|
49
|
+
select: {
|
|
50
|
+
width: '100%',
|
|
51
|
+
padding: '8px 10px',
|
|
52
|
+
fontSize: '12px',
|
|
53
|
+
border: '1px solid #e5e7eb',
|
|
54
|
+
borderRadius: '6px',
|
|
55
|
+
backgroundColor: '#f9fafb',
|
|
56
|
+
color: '#1f2937',
|
|
57
|
+
outline: 'none',
|
|
58
|
+
cursor: 'pointer',
|
|
59
|
+
},
|
|
60
|
+
row: {
|
|
61
|
+
display: 'flex',
|
|
62
|
+
gap: '8px',
|
|
63
|
+
},
|
|
64
|
+
languageBadge: (language: string) => {
|
|
65
|
+
const colors: Record<string, { bg: string; text: string }> = {
|
|
66
|
+
javascript: { bg: '#fef3c7', text: '#d97706' },
|
|
67
|
+
typescript: { bg: '#dbeafe', text: '#1d4ed8' },
|
|
68
|
+
python: { bg: '#d1fae5', text: '#059669' },
|
|
69
|
+
}
|
|
70
|
+
const color = colors[language] || colors.javascript
|
|
71
|
+
return {
|
|
72
|
+
display: 'inline-flex',
|
|
73
|
+
alignItems: 'center',
|
|
74
|
+
padding: '2px 8px',
|
|
75
|
+
borderRadius: '4px',
|
|
76
|
+
fontSize: '10px',
|
|
77
|
+
fontWeight: 600,
|
|
78
|
+
backgroundColor: color.bg,
|
|
79
|
+
color: color.text,
|
|
80
|
+
textTransform: 'capitalize' as const,
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
lineNumbers: {
|
|
84
|
+
display: 'flex',
|
|
85
|
+
flexDirection: 'column' as const,
|
|
86
|
+
alignItems: 'flex-end',
|
|
87
|
+
paddingRight: '8px',
|
|
88
|
+
marginRight: '8px',
|
|
89
|
+
borderRight: '1px solid #3f3f46',
|
|
90
|
+
color: '#6b7280',
|
|
91
|
+
fontSize: '11px',
|
|
92
|
+
fontFamily: '"Fira Code", "Monaco", "Consolas", monospace',
|
|
93
|
+
lineHeight: 1.5,
|
|
94
|
+
userSelect: 'none' as const,
|
|
95
|
+
},
|
|
96
|
+
codePreview: {
|
|
97
|
+
fontSize: '10px',
|
|
98
|
+
color: '#9ca3af',
|
|
99
|
+
marginTop: '4px',
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Code Node Component
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
export const CodeNode = memo(function CodeNode(props: NodeProps) {
|
|
108
|
+
const { data, selected, id } = props as NodeProps & { data: RuntypeNodeData }
|
|
109
|
+
const { step, onChange } = data
|
|
110
|
+
const config = step.config as TransformDataStepConfig
|
|
111
|
+
|
|
112
|
+
const [isExpanded, setIsExpanded] = useState(false)
|
|
113
|
+
|
|
114
|
+
const handleChange = useCallback(
|
|
115
|
+
(field: keyof TransformDataStepConfig, value: unknown) => {
|
|
116
|
+
onChange?.(id, {
|
|
117
|
+
config: {
|
|
118
|
+
...config,
|
|
119
|
+
[field]: value,
|
|
120
|
+
},
|
|
121
|
+
} as Partial<FlowStep>)
|
|
122
|
+
},
|
|
123
|
+
[id, config, onChange]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const handleNameChange = useCallback(
|
|
127
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
128
|
+
onChange?.(id, { name: e.target.value })
|
|
129
|
+
},
|
|
130
|
+
[id, onChange]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const language = config.language || 'javascript'
|
|
134
|
+
const lineCount = (config.script || '').split('\n').length
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<BaseNode
|
|
138
|
+
data={data}
|
|
139
|
+
selected={selected}
|
|
140
|
+
id={id}
|
|
141
|
+
typeLabel="Run Code"
|
|
142
|
+
icon={<CodeIcon />}
|
|
143
|
+
headerColor={NODE_HEADER_COLORS['transform-data']}
|
|
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
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
|
|
158
|
+
<label style={styles.label}>Code</label>
|
|
159
|
+
<span style={styles.languageBadge(language)}>{language}</span>
|
|
160
|
+
</div>
|
|
161
|
+
<textarea
|
|
162
|
+
style={styles.codeArea}
|
|
163
|
+
value={config.script || ''}
|
|
164
|
+
onChange={(e) => handleChange('script', e.target.value)}
|
|
165
|
+
placeholder={`// Write your ${language} code here\nreturn { result: input }`}
|
|
166
|
+
spellCheck={false}
|
|
167
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
168
|
+
/>
|
|
169
|
+
<div style={styles.codePreview}>
|
|
170
|
+
{lineCount} line{lineCount !== 1 ? 's' : ''}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{isExpanded && (
|
|
175
|
+
<>
|
|
176
|
+
<div style={styles.row}>
|
|
177
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
178
|
+
<label style={styles.label}>Language</label>
|
|
179
|
+
<select
|
|
180
|
+
style={styles.select}
|
|
181
|
+
value={language}
|
|
182
|
+
onChange={(e) => handleChange('language', e.target.value)}
|
|
183
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
184
|
+
>
|
|
185
|
+
<option value="javascript">JavaScript</option>
|
|
186
|
+
<option value="typescript">TypeScript</option>
|
|
187
|
+
<option value="python">Python</option>
|
|
188
|
+
</select>
|
|
189
|
+
</div>
|
|
190
|
+
<div style={{ ...styles.field, flex: 1 }}>
|
|
191
|
+
<label style={styles.label}>Sandbox</label>
|
|
192
|
+
<select
|
|
193
|
+
style={styles.select}
|
|
194
|
+
value={config.sandboxProvider || 'quickjs'}
|
|
195
|
+
onChange={(e) => handleChange('sandboxProvider', e.target.value)}
|
|
196
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
197
|
+
>
|
|
198
|
+
<option value="quickjs">QuickJS (Fast)</option>
|
|
199
|
+
<option value="daytona">Daytona (Full)</option>
|
|
200
|
+
</select>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div style={styles.field}>
|
|
205
|
+
<label style={styles.label}>On Error</label>
|
|
206
|
+
<select
|
|
207
|
+
style={styles.select}
|
|
208
|
+
value={config.errorHandling || 'fail'}
|
|
209
|
+
onChange={(e) => handleChange('errorHandling', e.target.value)}
|
|
210
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
211
|
+
>
|
|
212
|
+
<option value="fail">Fail</option>
|
|
213
|
+
<option value="continue">Continue</option>
|
|
214
|
+
<option value="default">Use Default</option>
|
|
215
|
+
</select>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div style={styles.field}>
|
|
219
|
+
<label style={styles.label}>Output Variable</label>
|
|
220
|
+
<input
|
|
221
|
+
style={styles.input}
|
|
222
|
+
value={config.outputVariable || ''}
|
|
223
|
+
onChange={(e) => handleChange('outputVariable', e.target.value)}
|
|
224
|
+
placeholder="transform_result"
|
|
225
|
+
onKeyDown={(e) => e.stopPropagation()}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
<button
|
|
232
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
233
|
+
style={{
|
|
234
|
+
width: '100%',
|
|
235
|
+
padding: '6px',
|
|
236
|
+
fontSize: '11px',
|
|
237
|
+
color: '#6366f1',
|
|
238
|
+
backgroundColor: 'transparent',
|
|
239
|
+
border: '1px dashed #e5e7eb',
|
|
240
|
+
borderRadius: '6px',
|
|
241
|
+
cursor: 'pointer',
|
|
242
|
+
marginTop: '4px',
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{isExpanded ? 'Show Less' : 'Show More Options'}
|
|
246
|
+
</button>
|
|
247
|
+
</BaseNode>
|
|
248
|
+
)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
export default CodeNode
|
|
252
|
+
|