@particle-academy/fancy-flow 0.5.4 → 0.6.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 +80 -0
- package/dist/engine.d.cts +2 -2
- package/dist/engine.d.ts +2 -2
- package/dist/index.cjs +223 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -13
- package/dist/index.d.ts +131 -13
- package/dist/index.js +228 -134
- package/dist/index.js.map +1 -1
- package/dist/registry/index.d.cts +3 -3
- package/dist/registry/index.d.ts +3 -3
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/schema/index.d.cts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/styles.css +30 -1
- package/dist/styles.css.map +1 -1
- package/dist/{types-TemTtb04.d.cts → types-BS3Gwnkq.d.cts} +1 -1
- package/dist/{types-TemTtb04.d.ts → types-BS3Gwnkq.d.ts} +1 -1
- package/dist/{types-D5RERHIP.d.cts → types-C0wdN6QX.d.cts} +1 -1
- package/dist/{types-BodwZiST.d.ts → types-DnMe9Vsf.d.ts} +1 -1
- package/dist/ux.d.cts +2 -2
- package/dist/ux.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,86 @@ export const ThresholdNode = defineNode<MyData>(({ data, selected }) => (
|
|
|
38
38
|
|
|
39
39
|
`defineNode` returns a memoized component compatible with the underlying engine; `<NodePort>` renders a connection handle. Together they cover what the typical node author needs — multiple ports, source vs target, position per side — without ever importing from `@xyflow/react`.
|
|
40
40
|
|
|
41
|
+
## Extending the editor
|
|
42
|
+
|
|
43
|
+
`<FlowEditor>` is batteries-included but not a black box. Four escape hatches,
|
|
44
|
+
smallest first — reach for the first one that fits.
|
|
45
|
+
|
|
46
|
+
**1. Custom toolbar buttons — declarative, so an agent can emit them too.**
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<FlowEditor
|
|
50
|
+
actions={[
|
|
51
|
+
{
|
|
52
|
+
id: "save",
|
|
53
|
+
label: "Save",
|
|
54
|
+
placement: "start",
|
|
55
|
+
onSelect: (api) => persist(api.toWorkflow()),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "duplicate",
|
|
59
|
+
label: "Duplicate",
|
|
60
|
+
requiresSelection: true, // auto-disabled with no selection
|
|
61
|
+
onSelect: (api) => api.duplicateNode(api.selectedId!),
|
|
62
|
+
},
|
|
63
|
+
]}
|
|
64
|
+
builtins={{ import: false }} // drop built-ins you don't want
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Each button renders with `data-action="<id>"`, so an agent gets a stable handle
|
|
69
|
+
instead of guessing DOM.
|
|
70
|
+
|
|
71
|
+
**2. Replace a whole region with `slots`.** Every slot receives the editor API.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<FlowEditor
|
|
75
|
+
slots={{
|
|
76
|
+
panel: (api) => <MyInspector node={api.selected} onChange={api.updateNode} />,
|
|
77
|
+
panelFooter: (api) => <button onClick={api.deleteSelected}>Delete node</button>,
|
|
78
|
+
empty: () => <p>Drag a node from the palette to start.</p>,
|
|
79
|
+
toolbar: (api) => <MyToolbar api={api} />, // replaces built-ins entirely
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**3. Drive it from outside** — `ref` for imperative control, `useFlowEditor()`
|
|
85
|
+
inside any child:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const editor = useRef<FlowEditorApi>(null);
|
|
89
|
+
editor.current?.addNode("llm_call", { x: 120, y: 80 });
|
|
90
|
+
editor.current?.deleteSelected();
|
|
91
|
+
editor.current?.run();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`FlowEditorApi` carries the graph, selection, run state, and every mutation:
|
|
95
|
+
`addNode` · `updateNode` · `deleteNodes` · `deleteSelected` · `deleteEdges` ·
|
|
96
|
+
`duplicateNode` · `setGraph` · `select` · `run` / `cancel` / `reset` ·
|
|
97
|
+
`toWorkflow` / `exportWorkflow` / `importWorkflow` · `fitView`.
|
|
98
|
+
|
|
99
|
+
**4. Reach React Flow directly** with `canvasProps` — context menus,
|
|
100
|
+
`snapToGrid`, minimap options, edge types, anything xyflow accepts:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<FlowEditor canvasProps={{ snapToGrid: true, showMinimap: true, onNodeContextMenu: openMenu }} />
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Deleting nodes
|
|
107
|
+
|
|
108
|
+
Three ways, all of which prune the edges attached to the node (a dangling edge
|
|
109
|
+
would survive into the schema and break the runner):
|
|
110
|
+
|
|
111
|
+
- the **Delete** toolbar button (enabled when a node is selected),
|
|
112
|
+
- the <kbd>Delete</kbd> or <kbd>Backspace</kbd> key on the canvas,
|
|
113
|
+
- `api.deleteSelected()` / `api.deleteNodes(ids)` from code.
|
|
114
|
+
|
|
115
|
+
`onDelete(ids)` fires after either path, so a host can sync its own store.
|
|
116
|
+
|
|
117
|
+
If you want none of the above chrome, skip `<FlowEditor>` entirely and compose
|
|
118
|
+
`useFlowState()` + `<FlowCanvas>` + `<NodePalette>` + `<NodeConfigPanel>`
|
|
119
|
+
yourself — they are all exported.
|
|
120
|
+
|
|
41
121
|
## Quick start
|
|
42
122
|
|
|
43
123
|
```tsx
|
package/dist/engine.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-
|
|
2
|
-
export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData,
|
|
1
|
+
import { a as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-BS3Gwnkq.cjs';
|
|
2
|
+
export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, c as FlowEdge, F as FlowNode, d as FlowNodeData, e as FlowNodeKind, N as NodeExecutor, b as NodeRunStatus, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, S as SubgraphNodeData, T as TriggerNodeData } from './types-BS3Gwnkq.cjs';
|
|
3
3
|
import '@xyflow/react';
|
|
4
4
|
|
|
5
5
|
type RunOptions = {
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-
|
|
2
|
-
export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData,
|
|
1
|
+
import { a as FlowGraph, E as ExecutorRegistry, R as RunEvent } from './types-BS3Gwnkq.js';
|
|
2
|
+
export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, c as FlowEdge, F as FlowNode, d as FlowNodeData, e as FlowNodeKind, N as NodeExecutor, b as NodeRunStatus, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, S as SubgraphNodeData, T as TriggerNodeData } from './types-BS3Gwnkq.js';
|
|
3
3
|
import '@xyflow/react';
|
|
4
4
|
|
|
5
5
|
type RunOptions = {
|
package/dist/index.cjs
CHANGED
|
@@ -10348,16 +10348,52 @@ function buildNodeTypes() {
|
|
|
10348
10348
|
for (const k of listNodeKinds()) map[k.name] = RegistryNode;
|
|
10349
10349
|
return map;
|
|
10350
10350
|
}
|
|
10351
|
-
|
|
10351
|
+
|
|
10352
|
+
// src/components/FlowEditor/graph-ops.ts
|
|
10353
|
+
function removeNodes(graph, ids) {
|
|
10354
|
+
if (ids.length === 0) return graph;
|
|
10355
|
+
const doomed = new Set(ids);
|
|
10356
|
+
return {
|
|
10357
|
+
nodes: graph.nodes.filter((n) => !doomed.has(n.id)),
|
|
10358
|
+
edges: graph.edges.filter((e) => !doomed.has(e.source) && !doomed.has(e.target))
|
|
10359
|
+
};
|
|
10360
|
+
}
|
|
10361
|
+
function removeEdges(edges, ids) {
|
|
10362
|
+
if (ids.length === 0) return edges;
|
|
10363
|
+
const doomed = new Set(ids);
|
|
10364
|
+
return edges.filter((e) => !doomed.has(e.id));
|
|
10365
|
+
}
|
|
10366
|
+
function duplicateNode(node, id2, offset = 40) {
|
|
10367
|
+
return {
|
|
10368
|
+
...node,
|
|
10369
|
+
id: id2,
|
|
10370
|
+
position: { x: node.position.x + offset, y: node.position.y + offset },
|
|
10371
|
+
// Deep copy so the clone's config edits don't mutate the original.
|
|
10372
|
+
data: JSON.parse(JSON.stringify(node.data ?? {}))
|
|
10373
|
+
};
|
|
10374
|
+
}
|
|
10375
|
+
var FlowEditorContext = ReactExports.createContext(null);
|
|
10376
|
+
var FlowEditorProvider = FlowEditorContext.Provider;
|
|
10377
|
+
function useFlowEditor() {
|
|
10378
|
+
const api = ReactExports.useContext(FlowEditorContext);
|
|
10379
|
+
if (api === null) {
|
|
10380
|
+
throw new Error("useFlowEditor() must be called inside <FlowEditor>.");
|
|
10381
|
+
}
|
|
10382
|
+
return api;
|
|
10383
|
+
}
|
|
10384
|
+
function useFlowEditorOptional() {
|
|
10385
|
+
return ReactExports.useContext(FlowEditorContext);
|
|
10386
|
+
}
|
|
10387
|
+
var FlowEditor = ReactExports.forwardRef(function FlowEditor2(props, ref) {
|
|
10352
10388
|
return /* @__PURE__ */ jsxRuntime.jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
10353
10389
|
"div",
|
|
10354
10390
|
{
|
|
10355
10391
|
className: ["ff-editor", props.className ?? ""].filter(Boolean).join(" "),
|
|
10356
10392
|
style: { height: props.height ?? 720, ...props.style },
|
|
10357
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(FlowEditorInner, { ...props })
|
|
10393
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(FlowEditorInner, { ...props, apiRef: ref })
|
|
10358
10394
|
}
|
|
10359
10395
|
) });
|
|
10360
|
-
}
|
|
10396
|
+
});
|
|
10361
10397
|
function FlowEditorInner({
|
|
10362
10398
|
initial = { nodes: [], edges: [] },
|
|
10363
10399
|
value,
|
|
@@ -10367,10 +10403,18 @@ function FlowEditorInner({
|
|
|
10367
10403
|
showPanel = true,
|
|
10368
10404
|
showFeed = true,
|
|
10369
10405
|
extraToolbar,
|
|
10370
|
-
|
|
10406
|
+
actions = [],
|
|
10407
|
+
builtins = {},
|
|
10408
|
+
slots = {},
|
|
10409
|
+
canvasProps = {},
|
|
10410
|
+
onChange,
|
|
10411
|
+
onSelectionChange,
|
|
10412
|
+
onDelete,
|
|
10413
|
+
apiRef
|
|
10371
10414
|
}) {
|
|
10372
10415
|
const internal = useFlowState(initial);
|
|
10373
10416
|
const runner = useFlowRun();
|
|
10417
|
+
const rf = useReactFlow();
|
|
10374
10418
|
const controlled = value !== void 0;
|
|
10375
10419
|
const flow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
|
|
10376
10420
|
const [, force] = ReactExports.useState(0);
|
|
@@ -10382,29 +10426,187 @@ function FlowEditorInner({
|
|
|
10382
10426
|
);
|
|
10383
10427
|
const [selectedId, setSelectedId] = ReactExports.useState(null);
|
|
10384
10428
|
const selected2 = ReactExports.useMemo(() => flow.nodes.find((n) => n.id === selectedId) ?? null, [flow.nodes, selectedId]);
|
|
10429
|
+
ReactExports.useEffect(() => onSelectionChange?.(selected2), [selected2, onSelectionChange]);
|
|
10385
10430
|
const handleNodeClick2 = (_e, node) => setSelectedId(node.id);
|
|
10386
10431
|
ReactExports.useEffect(() => {
|
|
10387
10432
|
if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
|
|
10388
10433
|
}, [flow.nodes, flow.edges, onChange, controlled]);
|
|
10434
|
+
const addNode = ReactExports.useCallback(
|
|
10435
|
+
(kindName, position) => {
|
|
10436
|
+
const kind = getNodeKind(kindName);
|
|
10437
|
+
if (!kind) return null;
|
|
10438
|
+
const at = position ?? { x: 80, y: 80 };
|
|
10439
|
+
const id2 = newNodeId();
|
|
10440
|
+
flow.setNodes((all) => [
|
|
10441
|
+
...all,
|
|
10442
|
+
{
|
|
10443
|
+
id: id2,
|
|
10444
|
+
type: kind.name,
|
|
10445
|
+
position: at,
|
|
10446
|
+
data: { kind: kind.name, label: kind.label, config: defaultConfigFor(kind) }
|
|
10447
|
+
}
|
|
10448
|
+
]);
|
|
10449
|
+
setSelectedId(id2);
|
|
10450
|
+
return id2;
|
|
10451
|
+
},
|
|
10452
|
+
[flow]
|
|
10453
|
+
);
|
|
10454
|
+
const deleteNodes = ReactExports.useCallback(
|
|
10455
|
+
(ids) => {
|
|
10456
|
+
if (ids.length === 0) return;
|
|
10457
|
+
const doomed = new Set(ids);
|
|
10458
|
+
flow.setNodes((all) => removeNodes({ nodes: all, edges: [] }, ids).nodes);
|
|
10459
|
+
flow.setEdges((all) => removeNodes({ nodes: [], edges: all }, ids).edges);
|
|
10460
|
+
setSelectedId((cur) => cur !== null && doomed.has(cur) ? null : cur);
|
|
10461
|
+
onDelete?.(ids);
|
|
10462
|
+
},
|
|
10463
|
+
[flow, onDelete]
|
|
10464
|
+
);
|
|
10465
|
+
const api = ReactExports.useMemo(() => {
|
|
10466
|
+
const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
|
|
10467
|
+
return {
|
|
10468
|
+
graph: { nodes: flow.nodes, edges: flow.edges },
|
|
10469
|
+
nodes: flow.nodes,
|
|
10470
|
+
edges: flow.edges,
|
|
10471
|
+
selectedId,
|
|
10472
|
+
selected: selected2,
|
|
10473
|
+
running: runner.running,
|
|
10474
|
+
statuses: runner.statuses,
|
|
10475
|
+
select: setSelectedId,
|
|
10476
|
+
addNode,
|
|
10477
|
+
updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
|
|
10478
|
+
deleteNodes,
|
|
10479
|
+
deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
|
|
10480
|
+
deleteEdges: (ids) => flow.setEdges((all) => removeEdges(all, ids)),
|
|
10481
|
+
duplicateNode: (id2) => {
|
|
10482
|
+
const src = flow.nodes.find((n) => n.id === id2);
|
|
10483
|
+
if (!src) return null;
|
|
10484
|
+
const copy = duplicateNode(src, newNodeId());
|
|
10485
|
+
flow.setNodes((all) => [...all, copy]);
|
|
10486
|
+
setSelectedId(copy.id);
|
|
10487
|
+
return copy.id;
|
|
10488
|
+
},
|
|
10489
|
+
setGraph: (graph) => {
|
|
10490
|
+
flow.setNodes(graph.nodes);
|
|
10491
|
+
flow.setEdges(graph.edges);
|
|
10492
|
+
},
|
|
10493
|
+
run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
|
|
10494
|
+
cancel: runner.cancel,
|
|
10495
|
+
reset: runner.reset,
|
|
10496
|
+
toWorkflow,
|
|
10497
|
+
exportWorkflow: () => downloadWorkflow(toWorkflow(), metadata),
|
|
10498
|
+
importWorkflow: () => pickWorkflow((graph) => {
|
|
10499
|
+
flow.setNodes(graph.nodes);
|
|
10500
|
+
flow.setEdges(graph.edges);
|
|
10501
|
+
}),
|
|
10502
|
+
fitView: () => rf.fitView({ padding: 0.2 })
|
|
10503
|
+
};
|
|
10504
|
+
}, [flow, selectedId, selected2, runner, executors, metadata, addNode, deleteNodes, rf]);
|
|
10505
|
+
ReactExports.useImperativeHandle(apiRef, () => api, [api]);
|
|
10506
|
+
const dropHandlers = paletteDropHandlers((kindName, evt) => {
|
|
10507
|
+
const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
|
|
10508
|
+
addNode(kindName, { x: point.x - 100, y: point.y - 30 });
|
|
10509
|
+
});
|
|
10510
|
+
const startActions = actions.filter((a) => a.placement === "start");
|
|
10511
|
+
const endActions = actions.filter((a) => a.placement !== "start");
|
|
10512
|
+
const toolbar = slots.toolbar ? slots.toolbar(api) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10513
|
+
startActions.map((a) => renderAction(a, api)),
|
|
10514
|
+
builtins.run !== false && /* @__PURE__ */ jsxRuntime.jsx(FlowRunControls, { running: api.running, onRun: api.run, onCancel: api.cancel, onReset: api.reset }),
|
|
10515
|
+
(builtins.delete !== false || builtins.export !== false || builtins.import !== false) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
|
|
10516
|
+
builtins.delete !== false && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10517
|
+
"button",
|
|
10518
|
+
{
|
|
10519
|
+
className: "ff-editor__btn",
|
|
10520
|
+
"data-action": "delete",
|
|
10521
|
+
onClick: api.deleteSelected,
|
|
10522
|
+
disabled: api.selected === null,
|
|
10523
|
+
title: "Delete the selected node (Del / Backspace)",
|
|
10524
|
+
children: "\u2715 Delete"
|
|
10525
|
+
}
|
|
10526
|
+
),
|
|
10527
|
+
builtins.export !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "export", onClick: api.exportWorkflow, children: "\u2193 Export" }),
|
|
10528
|
+
builtins.import !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "import", onClick: api.importWorkflow, children: "\u2191 Import" }),
|
|
10529
|
+
endActions.map((a) => renderAction(a, api)),
|
|
10530
|
+
extraToolbar,
|
|
10531
|
+
builtins.count !== false && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-editor__count", children: [
|
|
10532
|
+
api.nodes.length,
|
|
10533
|
+
" nodes \xB7 ",
|
|
10534
|
+
api.edges.length,
|
|
10535
|
+
" edges"
|
|
10536
|
+
] })
|
|
10537
|
+
] });
|
|
10538
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(FlowEditorProvider, { value: api, children: [
|
|
10539
|
+
showPalette && (slots.palette ? slots.palette(api) : /* @__PURE__ */ jsxRuntime.jsx(NodePalette, { className: "ff-editor__palette" })),
|
|
10540
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__main", ...dropHandlers, children: [
|
|
10541
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10542
|
+
FlowCanvas,
|
|
10543
|
+
{
|
|
10544
|
+
nodes: renderedNodes,
|
|
10545
|
+
edges: flow.edges,
|
|
10546
|
+
nodeTypes,
|
|
10547
|
+
onNodesChange: flow.onNodesChange,
|
|
10548
|
+
onEdgesChange: flow.onEdgesChange,
|
|
10549
|
+
onConnect: flow.onConnect,
|
|
10550
|
+
onNodeClick: handleNodeClick2,
|
|
10551
|
+
onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
|
|
10552
|
+
deleteKeyCode: ["Delete", "Backspace"],
|
|
10553
|
+
height: "100%",
|
|
10554
|
+
toolbar,
|
|
10555
|
+
...canvasProps
|
|
10556
|
+
}
|
|
10557
|
+
),
|
|
10558
|
+
slots.empty && api.nodes.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-editor__empty", children: slots.empty(api) }),
|
|
10559
|
+
showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsxRuntime.jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" }))
|
|
10560
|
+
] }),
|
|
10561
|
+
showPanel && (slots.panel ? slots.panel(api) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__panel-wrap", children: [
|
|
10562
|
+
/* @__PURE__ */ jsxRuntime.jsx(NodeConfigPanel, { className: "ff-editor__panel", node: api.selected, onChange: api.updateNode }),
|
|
10563
|
+
slots.panelFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-editor__panel-footer", children: slots.panelFooter(api) })
|
|
10564
|
+
] }))
|
|
10565
|
+
] });
|
|
10566
|
+
}
|
|
10567
|
+
function renderAction(action, api) {
|
|
10568
|
+
if (action.visible && !action.visible(api)) return null;
|
|
10569
|
+
const disabled = action.disabled ? action.disabled(api) : action.requiresSelection === true && api.selected === null;
|
|
10389
10570
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
10390
|
-
|
|
10571
|
+
"button",
|
|
10391
10572
|
{
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
renderedNodes,
|
|
10401
|
-
selected: selected2,
|
|
10402
|
-
setSelectedId,
|
|
10403
|
-
handleNodeClick: handleNodeClick2,
|
|
10404
|
-
extraToolbar
|
|
10405
|
-
}
|
|
10573
|
+
className: "ff-editor__btn",
|
|
10574
|
+
"data-action": action.id,
|
|
10575
|
+
title: action.title,
|
|
10576
|
+
disabled,
|
|
10577
|
+
onClick: () => action.onSelect(api),
|
|
10578
|
+
children: action.label
|
|
10579
|
+
},
|
|
10580
|
+
action.id
|
|
10406
10581
|
);
|
|
10407
10582
|
}
|
|
10583
|
+
function newNodeId() {
|
|
10584
|
+
return `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
10585
|
+
}
|
|
10586
|
+
function downloadWorkflow(schema, metadata) {
|
|
10587
|
+
const url = URL.createObjectURL(workflowToBlob(schema));
|
|
10588
|
+
const a = document.createElement("a");
|
|
10589
|
+
a.href = url;
|
|
10590
|
+
a.download = `${metadata?.id ?? "workflow"}.json`;
|
|
10591
|
+
a.click();
|
|
10592
|
+
URL.revokeObjectURL(url);
|
|
10593
|
+
}
|
|
10594
|
+
function pickWorkflow(onLoad) {
|
|
10595
|
+
const input = document.createElement("input");
|
|
10596
|
+
input.type = "file";
|
|
10597
|
+
input.accept = "application/json";
|
|
10598
|
+
input.onchange = async () => {
|
|
10599
|
+
const file = input.files?.[0];
|
|
10600
|
+
if (!file) return;
|
|
10601
|
+
try {
|
|
10602
|
+
const result = importWorkflow(JSON.parse(await file.text()), { lenient: true });
|
|
10603
|
+
onLoad(result.graph);
|
|
10604
|
+
} catch (e) {
|
|
10605
|
+
console.error("import failed", e);
|
|
10606
|
+
}
|
|
10607
|
+
};
|
|
10608
|
+
input.click();
|
|
10609
|
+
}
|
|
10408
10610
|
function makeControlledFlowAdapter(value, onChange) {
|
|
10409
10611
|
const apply = (next) => onChange?.(next);
|
|
10410
10612
|
return {
|
|
@@ -10430,114 +10632,6 @@ function makeControlledFlowAdapter(value, onChange) {
|
|
|
10430
10632
|
toGraph: () => value
|
|
10431
10633
|
};
|
|
10432
10634
|
}
|
|
10433
|
-
function FlowEditorBody({
|
|
10434
|
-
showPalette,
|
|
10435
|
-
showPanel,
|
|
10436
|
-
showFeed,
|
|
10437
|
-
flow,
|
|
10438
|
-
runner,
|
|
10439
|
-
executors,
|
|
10440
|
-
metadata,
|
|
10441
|
-
nodeTypes,
|
|
10442
|
-
renderedNodes,
|
|
10443
|
-
selected: selected2,
|
|
10444
|
-
setSelectedId,
|
|
10445
|
-
handleNodeClick: handleNodeClick2,
|
|
10446
|
-
extraToolbar
|
|
10447
|
-
}) {
|
|
10448
|
-
const rf = useReactFlow();
|
|
10449
|
-
const dropHandlers = paletteDropHandlers((kindName, evt) => {
|
|
10450
|
-
const kind = getNodeKind(kindName);
|
|
10451
|
-
if (!kind) return;
|
|
10452
|
-
const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
|
|
10453
|
-
const id2 = `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
10454
|
-
flow.setNodes((all) => [
|
|
10455
|
-
...all,
|
|
10456
|
-
{
|
|
10457
|
-
id: id2,
|
|
10458
|
-
type: kind.name,
|
|
10459
|
-
position: { x: point.x - 100, y: point.y - 30 },
|
|
10460
|
-
data: { kind: kind.name, label: kind.label, config: defaultConfigFor(kind) }
|
|
10461
|
-
}
|
|
10462
|
-
]);
|
|
10463
|
-
setSelectedId(id2);
|
|
10464
|
-
});
|
|
10465
|
-
const doExport = () => {
|
|
10466
|
-
const schema = exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
|
|
10467
|
-
const url = URL.createObjectURL(workflowToBlob(schema));
|
|
10468
|
-
const a = document.createElement("a");
|
|
10469
|
-
a.href = url;
|
|
10470
|
-
a.download = `${metadata?.id ?? "workflow"}.json`;
|
|
10471
|
-
a.click();
|
|
10472
|
-
URL.revokeObjectURL(url);
|
|
10473
|
-
};
|
|
10474
|
-
const doImport = () => {
|
|
10475
|
-
const input = document.createElement("input");
|
|
10476
|
-
input.type = "file";
|
|
10477
|
-
input.accept = "application/json";
|
|
10478
|
-
input.onchange = async () => {
|
|
10479
|
-
const file = input.files?.[0];
|
|
10480
|
-
if (!file) return;
|
|
10481
|
-
const text = await file.text();
|
|
10482
|
-
try {
|
|
10483
|
-
const result = importWorkflow(JSON.parse(text), { lenient: true });
|
|
10484
|
-
flow.setNodes(result.graph.nodes);
|
|
10485
|
-
flow.setEdges(result.graph.edges);
|
|
10486
|
-
} catch (e) {
|
|
10487
|
-
console.error("import failed", e);
|
|
10488
|
-
}
|
|
10489
|
-
};
|
|
10490
|
-
input.click();
|
|
10491
|
-
};
|
|
10492
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10493
|
-
showPalette && /* @__PURE__ */ jsxRuntime.jsx(NodePalette, { className: "ff-editor__palette" }),
|
|
10494
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__main", ...dropHandlers, children: [
|
|
10495
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10496
|
-
FlowCanvas,
|
|
10497
|
-
{
|
|
10498
|
-
nodes: renderedNodes,
|
|
10499
|
-
edges: flow.edges,
|
|
10500
|
-
nodeTypes,
|
|
10501
|
-
onNodesChange: flow.onNodesChange,
|
|
10502
|
-
onEdgesChange: flow.onEdgesChange,
|
|
10503
|
-
onConnect: flow.onConnect,
|
|
10504
|
-
onNodeClick: handleNodeClick2,
|
|
10505
|
-
height: "100%",
|
|
10506
|
-
toolbar: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10507
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10508
|
-
FlowRunControls,
|
|
10509
|
-
{
|
|
10510
|
-
running: runner.running,
|
|
10511
|
-
onRun: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
|
|
10512
|
-
onCancel: runner.cancel,
|
|
10513
|
-
onReset: runner.reset
|
|
10514
|
-
}
|
|
10515
|
-
),
|
|
10516
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
|
|
10517
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", onClick: doExport, children: "\u2193 Export" }),
|
|
10518
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", onClick: doImport, children: "\u2191 Import" }),
|
|
10519
|
-
extraToolbar,
|
|
10520
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-editor__count", children: [
|
|
10521
|
-
flow.nodes.length,
|
|
10522
|
-
" nodes \xB7 ",
|
|
10523
|
-
flow.edges.length,
|
|
10524
|
-
" edges"
|
|
10525
|
-
] })
|
|
10526
|
-
] })
|
|
10527
|
-
}
|
|
10528
|
-
),
|
|
10529
|
-
showFeed && /* @__PURE__ */ jsxRuntime.jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" })
|
|
10530
|
-
] }),
|
|
10531
|
-
showPanel && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10532
|
-
NodeConfigPanel,
|
|
10533
|
-
{
|
|
10534
|
-
className: "ff-editor__panel",
|
|
10535
|
-
node: selected2,
|
|
10536
|
-
onChange: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x))
|
|
10537
|
-
}
|
|
10538
|
-
)
|
|
10539
|
-
] });
|
|
10540
|
-
}
|
|
10541
10635
|
function defineNode(render) {
|
|
10542
10636
|
function Wrapped(props) {
|
|
10543
10637
|
return render({
|
|
@@ -10606,6 +10700,8 @@ exports.paletteDropHandlers = paletteDropHandlers;
|
|
|
10606
10700
|
exports.registerBuiltinKinds = registerBuiltinKinds;
|
|
10607
10701
|
exports.registerNodeKind = registerNodeKind;
|
|
10608
10702
|
exports.runFlow = runFlow;
|
|
10703
|
+
exports.useFlowEditor = useFlowEditor;
|
|
10704
|
+
exports.useFlowEditorOptional = useFlowEditorOptional;
|
|
10609
10705
|
exports.useFlowRun = useFlowRun;
|
|
10610
10706
|
exports.useFlowState = useFlowState;
|
|
10611
10707
|
exports.validateConfig = validateConfig;
|