@particle-academy/fancy-flow 0.18.0 → 0.19.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.
@@ -1,6 +1,6 @@
1
1
  import { applyNodeChanges, applyEdgeChanges, addEdge } from './chunk-NF6NPY5N.js';
2
2
  import { runFlow } from './chunk-L4AX73Q6.js';
3
- import { useState, useRef, useCallback } from 'react';
3
+ import { useState, useRef, useCallback, useMemo } from 'react';
4
4
 
5
5
  function useFlowRun({ maxFeed = 200 } = {}) {
6
6
  const [statuses, setStatuses] = useState({});
@@ -105,10 +105,119 @@ function useFlowState(initial) {
105
105
  const onConnect = useCallback((connection) => {
106
106
  setEdges((es) => addEdge(connection, es));
107
107
  }, []);
108
+ const setGraph = useCallback((graph) => {
109
+ setNodes(graph.nodes);
110
+ setEdges(graph.edges);
111
+ }, []);
108
112
  const toGraph = useCallback(() => ({ nodes, edges }), [nodes, edges]);
109
- return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
113
+ return { nodes, edges, setNodes, setEdges, setGraph, onNodesChange, onEdgesChange, onConnect, toGraph };
114
+ }
115
+
116
+ // src/runtime/history.ts
117
+ function createHistory(limit = 100) {
118
+ let past = [];
119
+ let future = [];
120
+ return {
121
+ push(graph) {
122
+ past.push(graph);
123
+ if (past.length > limit) past.shift();
124
+ future = [];
125
+ },
126
+ undo(current) {
127
+ const prev = past.pop();
128
+ if (prev === void 0) return null;
129
+ future.push(current);
130
+ return prev;
131
+ },
132
+ redo(current) {
133
+ const next = future.pop();
134
+ if (next === void 0) return null;
135
+ past.push(current);
136
+ return next;
137
+ },
138
+ canUndo: () => past.length > 0,
139
+ canRedo: () => future.length > 0,
140
+ clear() {
141
+ past = [];
142
+ future = [];
143
+ },
144
+ size: () => ({ past: past.length, future: future.length })
145
+ };
146
+ }
147
+
148
+ // src/runtime/use-flow-history.ts
149
+ function useFlowHistory(flow) {
150
+ const history = useRef(createHistory()).current;
151
+ const restoring = useRef(false);
152
+ const coalesced = useRef(false);
153
+ const [, bump] = useState(0);
154
+ const rerender = useCallback(() => bump((n) => n + 1), []);
155
+ const graphRef = useRef({ nodes: flow.nodes, edges: flow.edges });
156
+ graphRef.current = { nodes: flow.nodes, edges: flow.edges };
157
+ const capture = useCallback(() => {
158
+ if (restoring.current || coalesced.current) return;
159
+ coalesced.current = true;
160
+ queueMicrotask(() => {
161
+ coalesced.current = false;
162
+ });
163
+ history.push(graphRef.current);
164
+ rerender();
165
+ }, [history, rerender]);
166
+ const restore = useCallback(
167
+ (g) => {
168
+ if (!g) return;
169
+ restoring.current = true;
170
+ flow.setGraph(g);
171
+ queueMicrotask(() => {
172
+ restoring.current = false;
173
+ });
174
+ rerender();
175
+ },
176
+ [flow, rerender]
177
+ );
178
+ const undo = useCallback(() => restore(history.undo(graphRef.current)), [history, restore]);
179
+ const redo = useCallback(() => restore(history.redo(graphRef.current)), [history, restore]);
180
+ const wrapped = useMemo(
181
+ () => ({
182
+ ...flow,
183
+ setNodes: (next) => {
184
+ capture();
185
+ flow.setNodes(next);
186
+ },
187
+ setEdges: (next) => {
188
+ capture();
189
+ flow.setEdges(next);
190
+ },
191
+ setGraph: (g) => {
192
+ capture();
193
+ flow.setGraph(g);
194
+ },
195
+ onNodesChange: (changes) => {
196
+ if (!restoring.current && changes.some((c) => c.type === "remove")) capture();
197
+ flow.onNodesChange(changes);
198
+ },
199
+ onEdgesChange: (changes) => {
200
+ if (!restoring.current && changes.some((c) => c.type === "remove")) capture();
201
+ flow.onEdgesChange(changes);
202
+ },
203
+ onConnect: (conn) => {
204
+ capture();
205
+ flow.onConnect(conn);
206
+ }
207
+ }),
208
+ [flow, capture]
209
+ );
210
+ return {
211
+ flow: wrapped,
212
+ undo,
213
+ redo,
214
+ canUndo: history.canUndo(),
215
+ canRedo: history.canRedo(),
216
+ capture,
217
+ onNodeDragStart: capture
218
+ };
110
219
  }
111
220
 
112
- export { applyStatusesToNodes, useFlowRun, useFlowState };
113
- //# sourceMappingURL=chunk-MFMRTRPO.js.map
114
- //# sourceMappingURL=chunk-MFMRTRPO.js.map
221
+ export { applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState };
222
+ //# sourceMappingURL=chunk-PHX4SBOD.js.map
223
+ //# sourceMappingURL=chunk-PHX4SBOD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runtime/use-flow-run.ts","../src/runtime/use-flow-state.ts","../src/runtime/history.ts","../src/runtime/use-flow-history.ts"],"names":["useState","useCallback","useRef"],"mappings":"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","file":"chunk-PHX4SBOD.js","sourcesContent":["import { useCallback, useRef, useState } from \"react\";\nimport { runFlow, type RunOptions, type RunResult } from \"./run-flow\";\nimport type {\n ExecutorRegistry,\n FlowGraph,\n NodeRunStatus,\n RunEvent,\n} from \"../types\";\n\nexport type FlowRunFeedEntry = {\n id: string;\n at: number;\n level: \"info\" | \"warn\" | \"error\" | \"status\";\n text: string;\n nodeId?: string;\n detail?: unknown;\n};\n\nexport type UseFlowRunReturn = {\n /** Status keyed by nodeId — drive the UI overlay from this. */\n statuses: Record<string, NodeRunStatus>;\n /** Per-node status text (e.g. error message). */\n statusText: Record<string, string | undefined>;\n /** Live event log (capped to last N). */\n feed: FlowRunFeedEntry[];\n /** Whether a run is currently in progress. */\n running: boolean;\n /** Last run result, or null. */\n lastResult: RunResult | null;\n /** Kick off a run with the provided graph + executors. */\n run: (graph: FlowGraph, executors: ExecutorRegistry, options?: RunOptions) => Promise<RunResult>;\n /** Cancel the current run (if any). */\n cancel: () => void;\n /** Reset all runtime state (statuses, feed, lastResult). */\n reset: () => void;\n};\n\nexport type UseFlowRunOptions = {\n /** Cap the in-memory feed to this many entries. Default 200. */\n maxFeed?: number;\n};\n\n/**\n * useFlowRun — drives `runFlow` + maintains observability state. Pair with\n * `applyStatusesToNodes` (below) before passing nodes to `<FlowCanvas>` so\n * the per-node status badge renders.\n */\nexport function useFlowRun({ maxFeed = 200 }: UseFlowRunOptions = {}): UseFlowRunReturn {\n const [statuses, setStatuses] = useState<Record<string, NodeRunStatus>>({});\n const [statusText, setStatusText] = useState<Record<string, string | undefined>>({});\n const [feed, setFeed] = useState<FlowRunFeedEntry[]>([]);\n const [running, setRunning] = useState(false);\n const [lastResult, setLastResult] = useState<RunResult | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n\n const handleEvent = useCallback(\n (e: RunEvent) => {\n switch (e.type) {\n case \"node-status\":\n setStatuses((s) => ({ ...s, [e.nodeId]: e.status }));\n setStatusText((t) => ({ ...t, [e.nodeId]: e.text }));\n appendFeed({ level: \"status\", text: `${e.nodeId} → ${e.status}${e.text ? ` (${e.text})` : \"\"}`, nodeId: e.nodeId });\n break;\n case \"node-output\":\n appendFeed({ level: \"info\", text: `${e.nodeId}.${e.portId} = ${preview(e.value)}`, nodeId: e.nodeId, detail: e.value });\n break;\n case \"log\":\n appendFeed({ level: e.level, text: e.message, nodeId: e.nodeId, detail: e.detail });\n break;\n case \"run-start\":\n appendFeed({ level: \"info\", text: \"▶ run started\" });\n break;\n case \"run-end\":\n appendFeed({ level: e.ok ? \"info\" : \"error\", text: e.ok ? \"✓ run complete\" : \"✗ run failed\" });\n break;\n case \"run-error\":\n appendFeed({ level: \"error\", text: e.error });\n break;\n }\n function appendFeed(partial: Omit<FlowRunFeedEntry, \"id\" | \"at\">) {\n setFeed((f) => {\n const entry: FlowRunFeedEntry = { id: `${Date.now()}_${f.length}`, at: Date.now(), ...partial };\n const next = [...f, entry];\n return next.length > maxFeed ? next.slice(next.length - maxFeed) : next;\n });\n }\n },\n [maxFeed],\n );\n\n const run = useCallback(\n async (graph: FlowGraph, executors: ExecutorRegistry, options: RunOptions = {}) => {\n if (running) {\n return { ok: false, outputs: {}, error: \"another run is already in progress\" } satisfies RunResult;\n }\n const controller = new AbortController();\n abortRef.current = controller;\n // Reset previous statuses for the nodes we're about to run.\n const idleStatuses: Record<string, NodeRunStatus> = {};\n for (const n of graph.nodes) idleStatuses[n.id] = \"idle\";\n setStatuses(idleStatuses);\n setStatusText({});\n setRunning(true);\n try {\n const result = await runFlow(graph, executors, handleEvent, { ...options, signal: controller.signal });\n setLastResult(result);\n return result;\n } finally {\n setRunning(false);\n abortRef.current = null;\n }\n },\n [handleEvent, running],\n );\n\n const cancel = useCallback(() => abortRef.current?.abort(), []);\n\n const reset = useCallback(() => {\n setStatuses({});\n setStatusText({});\n setFeed([]);\n setLastResult(null);\n }, []);\n\n return { statuses, statusText, feed, running, lastResult, run, cancel, reset };\n}\n\n/** Merge runtime statuses into nodes for rendering. */\nexport function applyStatusesToNodes<TNode extends { id: string; data: any }>(\n nodes: TNode[],\n statuses: Record<string, NodeRunStatus>,\n statusText: Record<string, string | undefined>,\n): TNode[] {\n return nodes.map((n) => ({\n ...n,\n data: {\n ...n.data,\n status: statuses[n.id] ?? n.data?.status ?? \"idle\",\n statusText: statusText[n.id] ?? n.data?.statusText,\n },\n }));\n}\n\nfunction preview(v: unknown): string {\n try {\n const s = JSON.stringify(v);\n return s && s.length > 60 ? s.slice(0, 57) + \"…\" : (s ?? String(v));\n } catch {\n return String(v);\n }\n}\n","import { useCallback, useState } from \"react\";\nimport {\n addEdge,\n applyEdgeChanges,\n applyNodeChanges,\n type Connection,\n type Edge,\n type EdgeChange,\n type NodeChange,\n} from \"@xyflow/react\";\nimport type { FlowEdge, FlowGraph, FlowNode } from \"../types\";\n\nexport type UseFlowStateReturn = {\n nodes: FlowNode[];\n edges: FlowEdge[];\n setNodes: React.Dispatch<React.SetStateAction<FlowNode[]>>;\n setEdges: React.Dispatch<React.SetStateAction<FlowEdge[]>>;\n /**\n * Replace nodes AND edges atomically in a single commit. Use this for any op\n * that touches both (delete-with-edge-prune, undo/redo restore, setGraph):\n * calling `setNodes` then `setEdges` is NOT atomic in controlled mode (each\n * closes over a stale `value`), so the second write clobbers the first.\n */\n setGraph: (graph: FlowGraph) => void;\n onNodesChange: (changes: NodeChange[]) => void;\n onEdgesChange: (changes: EdgeChange[]) => void;\n onConnect: (connection: Connection) => void;\n /** Snapshot the current graph (suitable for serialization). */\n toGraph: () => FlowGraph;\n};\n\n/**\n * useFlowState — React Flow's standard controlled-state plumbing in one hook.\n * Spread into <FlowCanvas>.\n */\nexport function useFlowState(initial: FlowGraph): UseFlowStateReturn {\n const [nodes, setNodes] = useState<FlowNode[]>(initial.nodes);\n const [edges, setEdges] = useState<FlowEdge[]>(initial.edges);\n\n const onNodesChange = useCallback((changes: NodeChange[]) => {\n setNodes((ns) => applyNodeChanges(changes, ns) as FlowNode[]);\n }, []);\n const onEdgesChange = useCallback((changes: EdgeChange[]) => {\n setEdges((es) => applyEdgeChanges(changes, es));\n }, []);\n const onConnect = useCallback((connection: Connection) => {\n setEdges((es) => addEdge(connection, es) as Edge[]);\n }, []);\n\n const setGraph = useCallback((graph: FlowGraph) => {\n // Two useState writes in one event are batched, so this IS atomic here.\n setNodes(graph.nodes);\n setEdges(graph.edges);\n }, []);\n\n const toGraph = useCallback(() => ({ nodes, edges }), [nodes, edges]);\n\n return { nodes, edges, setNodes, setEdges, setGraph, onNodesChange, onEdgesChange, onConnect, toGraph };\n}\n","import type { FlowGraph } from \"../types\";\n\n/**\n * Undo/redo history — a pure, React-free snapshot controller. The editor pushes\n * a graph snapshot BEFORE each committing mutation; `undo`/`redo` swap snapshots\n * between the past/future stacks. Kept free of React so it can be unit-tested\n * and reused by any host (or a headless driver).\n *\n * Coalescing (collapsing a burst of setState calls from one logical op into a\n * single undo step) is the caller's job — see `useFlowHistory`.\n */\nexport type HistoryController = {\n /** Record `graph` as an undo point. Clears the redo stack (new branch). */\n push: (graph: FlowGraph) => void;\n /** Pop the last undo point; `current` is pushed onto the redo stack. */\n undo: (current: FlowGraph) => FlowGraph | null;\n /** Pop the last redo point; `current` is pushed back onto the undo stack. */\n redo: (current: FlowGraph) => FlowGraph | null;\n canUndo: () => boolean;\n canRedo: () => boolean;\n clear: () => void;\n /** Test/inspection helper — sizes of the two stacks. */\n size: () => { past: number; future: number };\n};\n\nexport function createHistory(limit = 100): HistoryController {\n let past: FlowGraph[] = [];\n let future: FlowGraph[] = [];\n\n return {\n push(graph) {\n past.push(graph);\n if (past.length > limit) past.shift();\n future = [];\n },\n undo(current) {\n const prev = past.pop();\n if (prev === undefined) return null;\n future.push(current);\n return prev;\n },\n redo(current) {\n const next = future.pop();\n if (next === undefined) return null;\n past.push(current);\n return next;\n },\n canUndo: () => past.length > 0,\n canRedo: () => future.length > 0,\n clear() {\n past = [];\n future = [];\n },\n size: () => ({ past: past.length, future: future.length }),\n };\n}\n","import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { EdgeChange, NodeChange } from \"@xyflow/react\";\nimport type { FlowGraph } from \"../types\";\nimport { createHistory } from \"./history\";\nimport type { UseFlowStateReturn } from \"./use-flow-state\";\n\nexport type UseFlowHistoryReturn = {\n /** The flow sink wrapped so committing mutations snapshot for undo first. */\n flow: UseFlowStateReturn;\n undo: () => void;\n redo: () => void;\n canUndo: boolean;\n canRedo: boolean;\n /** Snapshot the current graph as an undo point (before a programmatic edit). */\n capture: () => void;\n /** Wire to `<FlowCanvas onNodeDragStart>` — snapshots the pre-drag graph. */\n onNodeDragStart: () => void;\n};\n\n/**\n * useFlowHistory — the commit/undo pipeline. Wraps a flow sink (the uncontrolled\n * `useFlowState` hook OR the controlled adapter) so every *committing* mutation\n * records a snapshot first, giving undo/redo without the editor threading\n * history through each call site. This is the single interception point the\n * two-sink architecture otherwise lacks.\n *\n * Granularity: a burst of setState calls from one logical op (e.g. a\n * delete = setNodes+setEdges) is coalesced into ONE undo step; transient\n * interactive changes (drag-move, dimension-measure, selection) are NOT captured\n * — a drag is captured once at `onNodeDragStart` instead.\n */\nexport function useFlowHistory(flow: UseFlowStateReturn): UseFlowHistoryReturn {\n const history = useRef(createHistory()).current;\n const restoring = useRef(false);\n const coalesced = useRef(false);\n const [, bump] = useState(0);\n const rerender = useCallback(() => bump((n) => n + 1), []);\n\n // Latest graph via a ref, so capture never reads a stale snapshot.\n const graphRef = useRef<FlowGraph>({ nodes: flow.nodes, edges: flow.edges });\n graphRef.current = { nodes: flow.nodes, edges: flow.edges };\n\n const capture = useCallback(() => {\n if (restoring.current || coalesced.current) return;\n coalesced.current = true;\n queueMicrotask(() => {\n coalesced.current = false;\n });\n history.push(graphRef.current);\n rerender();\n }, [history, rerender]);\n\n const restore = useCallback(\n (g: FlowGraph | null) => {\n if (!g) return;\n restoring.current = true;\n flow.setGraph(g);\n queueMicrotask(() => {\n restoring.current = false;\n });\n rerender();\n },\n [flow, rerender],\n );\n\n const undo = useCallback(() => restore(history.undo(graphRef.current)), [history, restore]);\n const redo = useCallback(() => restore(history.redo(graphRef.current)), [history, restore]);\n\n const wrapped = useMemo<UseFlowStateReturn>(\n () => ({\n ...flow,\n setNodes: (next) => {\n capture();\n flow.setNodes(next);\n },\n setEdges: (next) => {\n capture();\n flow.setEdges(next);\n },\n setGraph: (g) => {\n capture();\n flow.setGraph(g);\n },\n onNodesChange: (changes: NodeChange[]) => {\n // Only structural removes commit; position/dimensions/select are transient.\n if (!restoring.current && changes.some((c) => c.type === \"remove\")) capture();\n flow.onNodesChange(changes);\n },\n onEdgesChange: (changes: EdgeChange[]) => {\n if (!restoring.current && changes.some((c) => c.type === \"remove\")) capture();\n flow.onEdgesChange(changes);\n },\n onConnect: (conn) => {\n capture();\n flow.onConnect(conn);\n },\n }),\n [flow, capture],\n );\n\n return {\n flow: wrapped,\n undo,\n redo,\n canUndo: history.canUndo(),\n canRedo: history.canRedo(),\n capture,\n onNodeDragStart: capture,\n };\n}\n"]}
package/dist/index.cjs CHANGED
@@ -10996,8 +10996,117 @@ function useFlowState(initial) {
10996
10996
  const onConnect = ReactExports.useCallback((connection) => {
10997
10997
  setEdges((es) => addEdge2(connection, es));
10998
10998
  }, []);
10999
+ const setGraph = ReactExports.useCallback((graph) => {
11000
+ setNodes(graph.nodes);
11001
+ setEdges(graph.edges);
11002
+ }, []);
10999
11003
  const toGraph = ReactExports.useCallback(() => ({ nodes, edges }), [nodes, edges]);
11000
- return { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, onConnect, toGraph };
11004
+ return { nodes, edges, setNodes, setEdges, setGraph, onNodesChange, onEdgesChange, onConnect, toGraph };
11005
+ }
11006
+
11007
+ // src/runtime/history.ts
11008
+ function createHistory(limit = 100) {
11009
+ let past = [];
11010
+ let future = [];
11011
+ return {
11012
+ push(graph) {
11013
+ past.push(graph);
11014
+ if (past.length > limit) past.shift();
11015
+ future = [];
11016
+ },
11017
+ undo(current) {
11018
+ const prev = past.pop();
11019
+ if (prev === void 0) return null;
11020
+ future.push(current);
11021
+ return prev;
11022
+ },
11023
+ redo(current) {
11024
+ const next = future.pop();
11025
+ if (next === void 0) return null;
11026
+ past.push(current);
11027
+ return next;
11028
+ },
11029
+ canUndo: () => past.length > 0,
11030
+ canRedo: () => future.length > 0,
11031
+ clear() {
11032
+ past = [];
11033
+ future = [];
11034
+ },
11035
+ size: () => ({ past: past.length, future: future.length })
11036
+ };
11037
+ }
11038
+
11039
+ // src/runtime/use-flow-history.ts
11040
+ function useFlowHistory(flow) {
11041
+ const history = ReactExports.useRef(createHistory()).current;
11042
+ const restoring = ReactExports.useRef(false);
11043
+ const coalesced = ReactExports.useRef(false);
11044
+ const [, bump] = ReactExports.useState(0);
11045
+ const rerender = ReactExports.useCallback(() => bump((n) => n + 1), []);
11046
+ const graphRef = ReactExports.useRef({ nodes: flow.nodes, edges: flow.edges });
11047
+ graphRef.current = { nodes: flow.nodes, edges: flow.edges };
11048
+ const capture = ReactExports.useCallback(() => {
11049
+ if (restoring.current || coalesced.current) return;
11050
+ coalesced.current = true;
11051
+ queueMicrotask(() => {
11052
+ coalesced.current = false;
11053
+ });
11054
+ history.push(graphRef.current);
11055
+ rerender();
11056
+ }, [history, rerender]);
11057
+ const restore = ReactExports.useCallback(
11058
+ (g) => {
11059
+ if (!g) return;
11060
+ restoring.current = true;
11061
+ flow.setGraph(g);
11062
+ queueMicrotask(() => {
11063
+ restoring.current = false;
11064
+ });
11065
+ rerender();
11066
+ },
11067
+ [flow, rerender]
11068
+ );
11069
+ const undo = ReactExports.useCallback(() => restore(history.undo(graphRef.current)), [history, restore]);
11070
+ const redo = ReactExports.useCallback(() => restore(history.redo(graphRef.current)), [history, restore]);
11071
+ const wrapped = ReactExports.useMemo(
11072
+ () => ({
11073
+ ...flow,
11074
+ setNodes: (next) => {
11075
+ capture();
11076
+ flow.setNodes(next);
11077
+ },
11078
+ setEdges: (next) => {
11079
+ capture();
11080
+ flow.setEdges(next);
11081
+ },
11082
+ setGraph: (g) => {
11083
+ capture();
11084
+ flow.setGraph(g);
11085
+ },
11086
+ onNodesChange: (changes) => {
11087
+ if (!restoring.current && changes.some((c) => c.type === "remove")) capture();
11088
+ flow.onNodesChange(changes);
11089
+ },
11090
+ onEdgesChange: (changes) => {
11091
+ if (!restoring.current && changes.some((c) => c.type === "remove")) capture();
11092
+ flow.onEdgesChange(changes);
11093
+ },
11094
+ onConnect: (conn) => {
11095
+ capture();
11096
+ flow.onConnect(conn);
11097
+ }
11098
+ }),
11099
+ [flow, capture]
11100
+ );
11101
+ return {
11102
+ flow: wrapped,
11103
+ undo,
11104
+ redo,
11105
+ canUndo: history.canUndo(),
11106
+ canRedo: history.canRedo(),
11107
+ capture,
11108
+ onNodeDragStart: capture
11109
+ };
11001
11110
  }
11002
11111
  function useFlowRun({ maxFeed = 200 } = {}) {
11003
11112
  const [statuses, setStatuses] = ReactExports.useState({});
@@ -11453,13 +11562,16 @@ function FlowEditorInner({
11453
11562
  onSelectionChange,
11454
11563
  onDelete,
11455
11564
  onEdgeDelete,
11565
+ confirmDelete,
11456
11566
  apiRef
11457
11567
  }) {
11458
11568
  const internal = useFlowState(initial);
11459
11569
  const runner = useFlowRun();
11460
11570
  const rf = useReactFlow();
11461
11571
  const controlled = value !== void 0;
11462
- const flow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
11572
+ const baseFlow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
11573
+ const hist = useFlowHistory(baseFlow);
11574
+ const flow = hist.flow;
11463
11575
  const [, force] = ReactExports.useState(0);
11464
11576
  ReactExports.useEffect(() => onNodeKindsChanged(() => force((n) => n + 1)), []);
11465
11577
  const nodeTypes = ReactExports.useMemo(() => buildNodeTypes(), [listNodeKinds().length]);
@@ -11512,6 +11624,23 @@ function FlowEditorInner({
11512
11624
  window.removeEventListener("keydown", onKey);
11513
11625
  };
11514
11626
  }, [menu, labelEdit]);
11627
+ ReactExports.useEffect(() => {
11628
+ const onKey = (e) => {
11629
+ const t = e.target;
11630
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
11631
+ if (!(e.ctrlKey || e.metaKey)) return;
11632
+ const key = e.key.toLowerCase();
11633
+ if (key === "z" && !e.shiftKey) {
11634
+ e.preventDefault();
11635
+ hist.undo();
11636
+ } else if (key === "z" && e.shiftKey || key === "y") {
11637
+ e.preventDefault();
11638
+ hist.redo();
11639
+ }
11640
+ };
11641
+ window.addEventListener("keydown", onKey);
11642
+ return () => window.removeEventListener("keydown", onKey);
11643
+ }, [hist]);
11515
11644
  ReactExports.useEffect(() => {
11516
11645
  if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
11517
11646
  }, [flow.nodes, flow.edges, onChange, controlled]);
@@ -11536,15 +11665,32 @@ function FlowEditorInner({
11536
11665
  [flow]
11537
11666
  );
11538
11667
  const deleteNodes = ReactExports.useCallback(
11539
- (ids) => {
11668
+ async (ids) => {
11540
11669
  if (ids.length === 0) return;
11670
+ const targets = flow.nodes.filter((n) => ids.includes(n.id));
11671
+ if (targets.length === 0) return;
11672
+ if (confirmDelete) {
11673
+ const attached = flow.edges.filter((e) => ids.includes(e.source) || ids.includes(e.target));
11674
+ if (!await confirmDelete({ nodes: targets, edges: attached })) return;
11675
+ }
11541
11676
  const doomed = new Set(ids);
11542
- flow.setNodes((all) => removeNodes({ nodes: all, edges: [] }, ids).nodes);
11543
- flow.setEdges((all) => removeNodes({ nodes: [], edges: all }, ids).edges);
11677
+ flow.setGraph(removeNodes({ nodes: flow.nodes, edges: flow.edges }, ids));
11544
11678
  setSelectedId((cur) => cur !== null && doomed.has(cur) ? null : cur);
11545
11679
  onDelete?.(ids);
11546
11680
  },
11547
- [flow, onDelete]
11681
+ [flow, onDelete, confirmDelete]
11682
+ );
11683
+ const deleteEdges = ReactExports.useCallback(
11684
+ async (ids) => {
11685
+ if (ids.length === 0) return;
11686
+ const targets = flow.edges.filter((e) => ids.includes(e.id));
11687
+ if (targets.length === 0) return;
11688
+ if (confirmDelete && !await confirmDelete({ nodes: [], edges: targets })) return;
11689
+ flow.setEdges((all) => removeEdges(all, ids));
11690
+ setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
11691
+ onEdgeDelete?.(ids);
11692
+ },
11693
+ [flow, onEdgeDelete, confirmDelete]
11548
11694
  );
11549
11695
  const api = ReactExports.useMemo(() => {
11550
11696
  const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
@@ -11564,18 +11710,8 @@ function FlowEditorInner({
11564
11710
  updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
11565
11711
  deleteNodes,
11566
11712
  deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
11567
- deleteEdges: (ids) => {
11568
- if (ids.length === 0) return;
11569
- flow.setEdges((all) => removeEdges(all, ids));
11570
- setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
11571
- onEdgeDelete?.(ids);
11572
- },
11573
- deleteSelectedEdge: () => {
11574
- if (!selectedEdgeId) return;
11575
- flow.setEdges((all) => removeEdges(all, [selectedEdgeId]));
11576
- setSelectedEdgeId(null);
11577
- onEdgeDelete?.([selectedEdgeId]);
11578
- },
11713
+ deleteEdges,
11714
+ deleteSelectedEdge: () => deleteEdges(selectedEdgeId ? [selectedEdgeId] : []),
11579
11715
  setEdgeLabel: (id2, label) => flow.setEdges((all) => setEdgeLabel(all, id2, label)),
11580
11716
  duplicateNode: (id2) => {
11581
11717
  const src = flow.nodes.find((n) => n.id === id2);
@@ -11585,22 +11721,20 @@ function FlowEditorInner({
11585
11721
  setSelectedId(copy.id);
11586
11722
  return copy.id;
11587
11723
  },
11588
- setGraph: (graph) => {
11589
- flow.setNodes(graph.nodes);
11590
- flow.setEdges(graph.edges);
11591
- },
11724
+ setGraph: (graph) => flow.setGraph(graph),
11592
11725
  run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
11593
11726
  cancel: runner.cancel,
11594
11727
  reset: runner.reset,
11595
11728
  toWorkflow,
11596
11729
  exportWorkflow: () => downloadWorkflow(toWorkflow(), metadata),
11597
- importWorkflow: () => pickWorkflow((graph) => {
11598
- flow.setNodes(graph.nodes);
11599
- flow.setEdges(graph.edges);
11600
- }),
11601
- fitView: () => rf.fitView({ padding: 0.2 })
11730
+ importWorkflow: () => pickWorkflow((graph) => flow.setGraph(graph)),
11731
+ fitView: () => rf.fitView({ padding: 0.2 }),
11732
+ undo: hist.undo,
11733
+ redo: hist.redo,
11734
+ canUndo: hist.canUndo,
11735
+ canRedo: hist.canRedo
11602
11736
  };
11603
- }, [flow, selectedId, selected2, runner, executors, metadata, addNode, deleteNodes, rf]);
11737
+ }, [flow, selectedId, selected2, selectedEdgeId, selectedEdge, runner, executors, metadata, addNode, deleteNodes, deleteEdges, hist, rf]);
11604
11738
  ReactExports.useImperativeHandle(apiRef, () => api, [api]);
11605
11739
  const dropHandlers = paletteDropHandlers((kindName, evt) => {
11606
11740
  const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
@@ -11611,6 +11745,31 @@ function FlowEditorInner({
11611
11745
  const toolbar = slots.toolbar ? slots.toolbar(api) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11612
11746
  startActions.map((a) => renderAction(a, api)),
11613
11747
  builtins.run !== false && /* @__PURE__ */ jsxRuntime.jsx(FlowRunControls, { running: api.running, onRun: api.run, onCancel: api.cancel, onReset: api.reset }),
11748
+ builtins.history !== false && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11749
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
11750
+ /* @__PURE__ */ jsxRuntime.jsx(
11751
+ "button",
11752
+ {
11753
+ className: "ff-editor__btn",
11754
+ "data-action": "undo",
11755
+ title: "Undo (Ctrl+Z)",
11756
+ disabled: !api.canUndo,
11757
+ onClick: api.undo,
11758
+ children: "\u21B6 Undo"
11759
+ }
11760
+ ),
11761
+ /* @__PURE__ */ jsxRuntime.jsx(
11762
+ "button",
11763
+ {
11764
+ className: "ff-editor__btn",
11765
+ "data-action": "redo",
11766
+ title: "Redo (Ctrl+Shift+Z)",
11767
+ disabled: !api.canRedo,
11768
+ onClick: api.redo,
11769
+ children: "\u21B7 Redo"
11770
+ }
11771
+ )
11772
+ ] }),
11614
11773
  (builtins.export !== false || builtins.import !== false) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ff-editor__sep" }),
11615
11774
  builtins.export !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "export", onClick: api.exportWorkflow, children: "\u2193 Export" }),
11616
11775
  builtins.import !== false && /* @__PURE__ */ jsxRuntime.jsx("button", { className: "ff-editor__btn", "data-action": "import", onClick: api.importWorkflow, children: "\u2191 Import" }),
@@ -11635,12 +11794,14 @@ function FlowEditorInner({
11635
11794
  onNodesChange: flow.onNodesChange,
11636
11795
  onEdgesChange: flow.onEdgesChange,
11637
11796
  onConnect: flow.onConnect,
11797
+ onNodeDragStart: hist.onNodeDragStart,
11638
11798
  onNodeClick: handleNodeClick2,
11639
11799
  onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
11640
11800
  onEdgeClick: handleEdgeClick,
11641
11801
  onEdgeContextMenu: builtins.edgeContextMenu === false ? void 0 : handleEdgeContextMenu,
11642
11802
  onEdgesDelete: (deleted) => onEdgeDelete?.(deleted.map((e) => e.id)),
11643
11803
  onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
11804
+ onBeforeDelete: confirmDelete ? async ({ nodes, edges }) => confirmDelete({ nodes, edges }) : void 0,
11644
11805
  deleteKeyCode: ["Delete", "Backspace"],
11645
11806
  height: "100%",
11646
11807
  toolbar,
@@ -11811,6 +11972,8 @@ function makeControlledFlowAdapter(value, onChange) {
11811
11972
  const nextEdges = typeof next === "function" ? next(value.edges) : next;
11812
11973
  apply({ nodes: value.nodes, edges: nextEdges });
11813
11974
  },
11975
+ // Atomic both-at-once commit — the reason this exists (see UseFlowStateReturn).
11976
+ setGraph: (graph) => apply(graph),
11814
11977
  onNodesChange: (changes) => {
11815
11978
  apply({ nodes: applyNodeChanges(changes, value.nodes), edges: value.edges });
11816
11979
  },
@@ -11924,6 +12087,7 @@ exports.applyStatusesToNodes = applyStatusesToNodes;
11924
12087
  exports.buildNodeTypes = buildNodeTypes;
11925
12088
  exports.categoryAccent = categoryAccent;
11926
12089
  exports.createConnectionValidator = createConnectionValidator;
12090
+ exports.createHistory = createHistory;
11927
12091
  exports.decodePause = decodePause;
11928
12092
  exports.defaultConfigFor = defaultConfigFor;
11929
12093
  exports.defaultNodeTypes = defaultNodeTypes;
@@ -11949,6 +12113,7 @@ exports.resolvePortSpec = resolvePortSpec;
11949
12113
  exports.runFlow = runFlow;
11950
12114
  exports.useFlowEditor = useFlowEditor;
11951
12115
  exports.useFlowEditorOptional = useFlowEditorOptional;
12116
+ exports.useFlowHistory = useFlowHistory;
11952
12117
  exports.useFlowRun = useFlowRun;
11953
12118
  exports.useFlowState = useFlowState;
11954
12119
  exports.validateConfig = validateConfig;