@particle-academy/fancy-flow 0.5.4 → 0.7.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 +98 -0
- package/dist/engine.d.cts +2 -2
- package/dist/engine.d.ts +2 -2
- package/dist/index.cjs +283 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -13
- package/dist/index.d.ts +136 -13
- package/dist/index.js +288 -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 +68 -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,104 @@ 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
|
+
- **right-click a node** → Delete / Duplicate,
|
|
112
|
+
- the **Delete** toolbar button (enabled when a node is selected),
|
|
113
|
+
- the <kbd>Delete</kbd> or <kbd>Backspace</kbd> key on the canvas,
|
|
114
|
+
- `api.deleteSelected()` / `api.deleteNodes(ids)` from code.
|
|
115
|
+
|
|
116
|
+
Swap the menu for your own with `slots.contextMenu`, or turn it off with
|
|
117
|
+
`builtins={{ contextMenu: false }}` (passing your own
|
|
118
|
+
`canvasProps.onNodeContextMenu` also takes over):
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
<FlowEditor
|
|
122
|
+
slots={{
|
|
123
|
+
contextMenu: (api, nodeId, close) => (
|
|
124
|
+
<>
|
|
125
|
+
<button onClick={() => { api.duplicateNode(nodeId); close(); }}>Duplicate</button>
|
|
126
|
+
<button onClick={() => { pinNode(nodeId); close(); }}>Pin</button>
|
|
127
|
+
</>
|
|
128
|
+
),
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`onDelete(ids)` fires after either path, so a host can sync its own store.
|
|
134
|
+
|
|
135
|
+
If you want none of the above chrome, skip `<FlowEditor>` entirely and compose
|
|
136
|
+
`useFlowState()` + `<FlowCanvas>` + `<NodePalette>` + `<NodeConfigPanel>`
|
|
137
|
+
yourself — they are all exported.
|
|
138
|
+
|
|
41
139
|
## Quick start
|
|
42
140
|
|
|
43
141
|
```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,247 @@ 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);
|
|
10431
|
+
const [menu, setMenu] = ReactExports.useState(null);
|
|
10432
|
+
const closeMenu = ReactExports.useCallback(() => setMenu(null), []);
|
|
10433
|
+
const handleNodeContextMenu = (event, node) => {
|
|
10434
|
+
event.preventDefault();
|
|
10435
|
+
setSelectedId(node.id);
|
|
10436
|
+
setMenu({ x: event.clientX, y: event.clientY, nodeId: node.id });
|
|
10437
|
+
};
|
|
10438
|
+
ReactExports.useEffect(() => {
|
|
10439
|
+
if (menu === null) return;
|
|
10440
|
+
const close = () => setMenu(null);
|
|
10441
|
+
const onKey = (e) => e.key === "Escape" && setMenu(null);
|
|
10442
|
+
window.addEventListener("click", close);
|
|
10443
|
+
window.addEventListener("scroll", close, true);
|
|
10444
|
+
window.addEventListener("keydown", onKey);
|
|
10445
|
+
return () => {
|
|
10446
|
+
window.removeEventListener("click", close);
|
|
10447
|
+
window.removeEventListener("scroll", close, true);
|
|
10448
|
+
window.removeEventListener("keydown", onKey);
|
|
10449
|
+
};
|
|
10450
|
+
}, [menu]);
|
|
10386
10451
|
ReactExports.useEffect(() => {
|
|
10387
10452
|
if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
|
|
10388
10453
|
}, [flow.nodes, flow.edges, onChange, controlled]);
|
|
10454
|
+
const addNode = ReactExports.useCallback(
|
|
10455
|
+
(kindName, position) => {
|
|
10456
|
+
const kind = getNodeKind(kindName);
|
|
10457
|
+
if (!kind) return null;
|
|
10458
|
+
const at = position ?? { x: 80, y: 80 };
|
|
10459
|
+
const id2 = newNodeId();
|
|
10460
|
+
flow.setNodes((all) => [
|
|
10461
|
+
...all,
|
|
10462
|
+
{
|
|
10463
|
+
id: id2,
|
|
10464
|
+
type: kind.name,
|
|
10465
|
+
position: at,
|
|
10466
|
+
data: { kind: kind.name, label: kind.label, config: defaultConfigFor(kind) }
|
|
10467
|
+
}
|
|
10468
|
+
]);
|
|
10469
|
+
setSelectedId(id2);
|
|
10470
|
+
return id2;
|
|
10471
|
+
},
|
|
10472
|
+
[flow]
|
|
10473
|
+
);
|
|
10474
|
+
const deleteNodes = ReactExports.useCallback(
|
|
10475
|
+
(ids) => {
|
|
10476
|
+
if (ids.length === 0) return;
|
|
10477
|
+
const doomed = new Set(ids);
|
|
10478
|
+
flow.setNodes((all) => removeNodes({ nodes: all, edges: [] }, ids).nodes);
|
|
10479
|
+
flow.setEdges((all) => removeNodes({ nodes: [], edges: all }, ids).edges);
|
|
10480
|
+
setSelectedId((cur) => cur !== null && doomed.has(cur) ? null : cur);
|
|
10481
|
+
onDelete?.(ids);
|
|
10482
|
+
},
|
|
10483
|
+
[flow, onDelete]
|
|
10484
|
+
);
|
|
10485
|
+
const api = ReactExports.useMemo(() => {
|
|
10486
|
+
const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
|
|
10487
|
+
return {
|
|
10488
|
+
graph: { nodes: flow.nodes, edges: flow.edges },
|
|
10489
|
+
nodes: flow.nodes,
|
|
10490
|
+
edges: flow.edges,
|
|
10491
|
+
selectedId,
|
|
10492
|
+
selected: selected2,
|
|
10493
|
+
running: runner.running,
|
|
10494
|
+
statuses: runner.statuses,
|
|
10495
|
+
select: setSelectedId,
|
|
10496
|
+
addNode,
|
|
10497
|
+
updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
|
|
10498
|
+
deleteNodes,
|
|
10499
|
+
deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
|
|
10500
|
+
deleteEdges: (ids) => flow.setEdges((all) => removeEdges(all, ids)),
|
|
10501
|
+
duplicateNode: (id2) => {
|
|
10502
|
+
const src = flow.nodes.find((n) => n.id === id2);
|
|
10503
|
+
if (!src) return null;
|
|
10504
|
+
const copy = duplicateNode(src, newNodeId());
|
|
10505
|
+
flow.setNodes((all) => [...all, copy]);
|
|
10506
|
+
setSelectedId(copy.id);
|
|
10507
|
+
return copy.id;
|
|
10508
|
+
},
|
|
10509
|
+
setGraph: (graph) => {
|
|
10510
|
+
flow.setNodes(graph.nodes);
|
|
10511
|
+
flow.setEdges(graph.edges);
|
|
10512
|
+
},
|
|
10513
|
+
run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
|
|
10514
|
+
cancel: runner.cancel,
|
|
10515
|
+
reset: runner.reset,
|
|
10516
|
+
toWorkflow,
|
|
10517
|
+
exportWorkflow: () => downloadWorkflow(toWorkflow(), metadata),
|
|
10518
|
+
importWorkflow: () => pickWorkflow((graph) => {
|
|
10519
|
+
flow.setNodes(graph.nodes);
|
|
10520
|
+
flow.setEdges(graph.edges);
|
|
10521
|
+
}),
|
|
10522
|
+
fitView: () => rf.fitView({ padding: 0.2 })
|
|
10523
|
+
};
|
|
10524
|
+
}, [flow, selectedId, selected2, runner, executors, metadata, addNode, deleteNodes, rf]);
|
|
10525
|
+
ReactExports.useImperativeHandle(apiRef, () => api, [api]);
|
|
10526
|
+
const dropHandlers = paletteDropHandlers((kindName, evt) => {
|
|
10527
|
+
const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
|
|
10528
|
+
addNode(kindName, { x: point.x - 100, y: point.y - 30 });
|
|
10529
|
+
});
|
|
10530
|
+
const startActions = actions.filter((a) => a.placement === "start");
|
|
10531
|
+
const endActions = actions.filter((a) => a.placement !== "start");
|
|
10532
|
+
const toolbar = slots.toolbar ? slots.toolbar(api) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10533
|
+
startActions.map((a) => renderAction(a, api)),
|
|
10534
|
+
builtins.run !== false && /* @__PURE__ */ jsxRuntime.jsx(FlowRunControls, { running: api.running, onRun: api.run, onCancel: api.cancel, onReset: api.reset }),
|
|
10535
|
+
(builtins.delete !== false || builtins.export !== false || builtins.import !== false) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
|
|
10536
|
+
builtins.delete !== false && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10537
|
+
"button",
|
|
10538
|
+
{
|
|
10539
|
+
className: "ff-editor__btn",
|
|
10540
|
+
"data-action": "delete",
|
|
10541
|
+
onClick: api.deleteSelected,
|
|
10542
|
+
disabled: api.selected === null,
|
|
10543
|
+
title: "Delete the selected node (Del / Backspace)",
|
|
10544
|
+
children: "\u2715 Delete"
|
|
10545
|
+
}
|
|
10546
|
+
),
|
|
10547
|
+
builtins.export !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "export", onClick: api.exportWorkflow, children: "\u2193 Export" }),
|
|
10548
|
+
builtins.import !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "import", onClick: api.importWorkflow, children: "\u2191 Import" }),
|
|
10549
|
+
endActions.map((a) => renderAction(a, api)),
|
|
10550
|
+
extraToolbar,
|
|
10551
|
+
builtins.count !== false && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ff-editor__count", children: [
|
|
10552
|
+
api.nodes.length,
|
|
10553
|
+
" nodes \xB7 ",
|
|
10554
|
+
api.edges.length,
|
|
10555
|
+
" edges"
|
|
10556
|
+
] })
|
|
10557
|
+
] });
|
|
10558
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(FlowEditorProvider, { value: api, children: [
|
|
10559
|
+
showPalette && (slots.palette ? slots.palette(api) : /* @__PURE__ */ jsxRuntime.jsx(NodePalette, { className: "ff-editor__palette" })),
|
|
10560
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__main", ...dropHandlers, children: [
|
|
10561
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10562
|
+
FlowCanvas,
|
|
10563
|
+
{
|
|
10564
|
+
nodes: renderedNodes,
|
|
10565
|
+
edges: flow.edges,
|
|
10566
|
+
nodeTypes,
|
|
10567
|
+
onNodesChange: flow.onNodesChange,
|
|
10568
|
+
onEdgesChange: flow.onEdgesChange,
|
|
10569
|
+
onConnect: flow.onConnect,
|
|
10570
|
+
onNodeClick: handleNodeClick2,
|
|
10571
|
+
onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
|
|
10572
|
+
onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
|
|
10573
|
+
deleteKeyCode: ["Delete", "Backspace"],
|
|
10574
|
+
height: "100%",
|
|
10575
|
+
toolbar,
|
|
10576
|
+
...canvasProps
|
|
10577
|
+
}
|
|
10578
|
+
),
|
|
10579
|
+
slots.empty && api.nodes.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-editor__empty", children: slots.empty(api) }),
|
|
10580
|
+
menu !== null && builtins.contextMenu !== false && /* @__PURE__ */ jsxRuntime.jsx(
|
|
10581
|
+
"div",
|
|
10582
|
+
{
|
|
10583
|
+
className: "ff-editor__ctx",
|
|
10584
|
+
style: { top: menu.y, left: menu.x },
|
|
10585
|
+
role: "menu",
|
|
10586
|
+
onClick: (e) => e.stopPropagation(),
|
|
10587
|
+
children: slots.contextMenu ? slots.contextMenu(api, menu.nodeId, closeMenu) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
10588
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10589
|
+
"button",
|
|
10590
|
+
{
|
|
10591
|
+
type: "button",
|
|
10592
|
+
role: "menuitem",
|
|
10593
|
+
className: "ff-editor__ctx-item",
|
|
10594
|
+
"data-action": "ctx-duplicate",
|
|
10595
|
+
onClick: () => {
|
|
10596
|
+
api.duplicateNode(menu.nodeId);
|
|
10597
|
+
closeMenu();
|
|
10598
|
+
},
|
|
10599
|
+
children: "Duplicate"
|
|
10600
|
+
}
|
|
10601
|
+
),
|
|
10602
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10603
|
+
"button",
|
|
10604
|
+
{
|
|
10605
|
+
type: "button",
|
|
10606
|
+
role: "menuitem",
|
|
10607
|
+
className: "ff-editor__ctx-item ff-editor__ctx-item--danger",
|
|
10608
|
+
"data-action": "ctx-delete",
|
|
10609
|
+
onClick: () => {
|
|
10610
|
+
api.deleteNodes([menu.nodeId]);
|
|
10611
|
+
closeMenu();
|
|
10612
|
+
},
|
|
10613
|
+
children: "Delete"
|
|
10614
|
+
}
|
|
10615
|
+
)
|
|
10616
|
+
] })
|
|
10617
|
+
}
|
|
10618
|
+
),
|
|
10619
|
+
showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsxRuntime.jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" }))
|
|
10620
|
+
] }),
|
|
10621
|
+
showPanel && (slots.panel ? slots.panel(api) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ff-editor__panel-wrap", children: [
|
|
10622
|
+
/* @__PURE__ */ jsxRuntime.jsx(NodeConfigPanel, { className: "ff-editor__panel", node: api.selected, onChange: api.updateNode }),
|
|
10623
|
+
slots.panelFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ff-editor__panel-footer", children: slots.panelFooter(api) })
|
|
10624
|
+
] }))
|
|
10625
|
+
] });
|
|
10626
|
+
}
|
|
10627
|
+
function renderAction(action, api) {
|
|
10628
|
+
if (action.visible && !action.visible(api)) return null;
|
|
10629
|
+
const disabled = action.disabled ? action.disabled(api) : action.requiresSelection === true && api.selected === null;
|
|
10389
10630
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
10390
|
-
|
|
10631
|
+
"button",
|
|
10391
10632
|
{
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
renderedNodes,
|
|
10401
|
-
selected: selected2,
|
|
10402
|
-
setSelectedId,
|
|
10403
|
-
handleNodeClick: handleNodeClick2,
|
|
10404
|
-
extraToolbar
|
|
10405
|
-
}
|
|
10633
|
+
className: "ff-editor__btn",
|
|
10634
|
+
"data-action": action.id,
|
|
10635
|
+
title: action.title,
|
|
10636
|
+
disabled,
|
|
10637
|
+
onClick: () => action.onSelect(api),
|
|
10638
|
+
children: action.label
|
|
10639
|
+
},
|
|
10640
|
+
action.id
|
|
10406
10641
|
);
|
|
10407
10642
|
}
|
|
10643
|
+
function newNodeId() {
|
|
10644
|
+
return `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
10645
|
+
}
|
|
10646
|
+
function downloadWorkflow(schema, metadata) {
|
|
10647
|
+
const url = URL.createObjectURL(workflowToBlob(schema));
|
|
10648
|
+
const a = document.createElement("a");
|
|
10649
|
+
a.href = url;
|
|
10650
|
+
a.download = `${metadata?.id ?? "workflow"}.json`;
|
|
10651
|
+
a.click();
|
|
10652
|
+
URL.revokeObjectURL(url);
|
|
10653
|
+
}
|
|
10654
|
+
function pickWorkflow(onLoad) {
|
|
10655
|
+
const input = document.createElement("input");
|
|
10656
|
+
input.type = "file";
|
|
10657
|
+
input.accept = "application/json";
|
|
10658
|
+
input.onchange = async () => {
|
|
10659
|
+
const file = input.files?.[0];
|
|
10660
|
+
if (!file) return;
|
|
10661
|
+
try {
|
|
10662
|
+
const result = importWorkflow(JSON.parse(await file.text()), { lenient: true });
|
|
10663
|
+
onLoad(result.graph);
|
|
10664
|
+
} catch (e) {
|
|
10665
|
+
console.error("import failed", e);
|
|
10666
|
+
}
|
|
10667
|
+
};
|
|
10668
|
+
input.click();
|
|
10669
|
+
}
|
|
10408
10670
|
function makeControlledFlowAdapter(value, onChange) {
|
|
10409
10671
|
const apply = (next) => onChange?.(next);
|
|
10410
10672
|
return {
|
|
@@ -10430,114 +10692,6 @@ function makeControlledFlowAdapter(value, onChange) {
|
|
|
10430
10692
|
toGraph: () => value
|
|
10431
10693
|
};
|
|
10432
10694
|
}
|
|
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
10695
|
function defineNode(render) {
|
|
10542
10696
|
function Wrapped(props) {
|
|
10543
10697
|
return render({
|
|
@@ -10606,6 +10760,8 @@ exports.paletteDropHandlers = paletteDropHandlers;
|
|
|
10606
10760
|
exports.registerBuiltinKinds = registerBuiltinKinds;
|
|
10607
10761
|
exports.registerNodeKind = registerNodeKind;
|
|
10608
10762
|
exports.runFlow = runFlow;
|
|
10763
|
+
exports.useFlowEditor = useFlowEditor;
|
|
10764
|
+
exports.useFlowEditorOptional = useFlowEditorOptional;
|
|
10609
10765
|
exports.useFlowRun = useFlowRun;
|
|
10610
10766
|
exports.useFlowState = useFlowState;
|
|
10611
10767
|
exports.validateConfig = validateConfig;
|