@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.
- package/dist/{chunk-MFMRTRPO.js → chunk-PHX4SBOD.js} +114 -5
- package/dist/chunk-PHX4SBOD.js.map +1 -0
- package/dist/index.cjs +193 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +83 -29
- package/dist/index.js.map +1 -1
- package/dist/runtime/index.d.cts +61 -1
- package/dist/runtime/index.d.ts +61 -1
- package/dist/runtime.cjs +112 -1
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +1 -1
- package/package.json +6 -3
- package/dist/chunk-MFMRTRPO.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -10,7 +10,7 @@ export { ImportIssue, ImportOptions, ImportResult, WORKFLOW_SCHEMA_URL, WORKFLOW
|
|
|
10
10
|
import { N as NodeCategory, a as NodeKindDefinition, C as ConfigField } from './types-D_jOR3Z2.cjs';
|
|
11
11
|
export { D as DocumentConfigField, K as KeyValueConfigField, P as PortSpec, R as RenderBodyContext, d as RepeaterConfigField, e as RepeaterRowField } from './types-D_jOR3Z2.cjs';
|
|
12
12
|
import { FlowRunFeedEntry } from './runtime/index.cjs';
|
|
13
|
-
export { UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, useFlowRun, useFlowState } from './runtime/index.cjs';
|
|
13
|
+
export { HistoryController, UseFlowHistoryReturn, UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './runtime/index.cjs';
|
|
14
14
|
export { R as RunOptions, a as RunResult, r as runFlow } from './run-flow-VKm57Hcj.cjs';
|
|
15
15
|
export { L as LEGACY_PAUSE_PREFIXES, a as PAUSE_PREFIX, P as PauseAwaiting, b as PauseSignal, d as decodePause, e as encodePause, i as isPause, p as pauseForHuman } from './pause-9iT4tCEV.cjs';
|
|
16
16
|
import './capabilities-ChE3Xi8R.cjs';
|
|
@@ -109,6 +109,16 @@ type FlowEditorApi = {
|
|
|
109
109
|
/** Open a file picker and load a workflow. */
|
|
110
110
|
importWorkflow: () => void;
|
|
111
111
|
fitView: () => void;
|
|
112
|
+
/** Undo the last committing edit (add / delete / connect / config / drag /
|
|
113
|
+
* import). Transient interactions (a drag in progress, selection) are not
|
|
114
|
+
* their own steps. */
|
|
115
|
+
undo: () => void;
|
|
116
|
+
/** Redo the last undone edit. */
|
|
117
|
+
redo: () => void;
|
|
118
|
+
/** True when there is an edit to undo. */
|
|
119
|
+
canUndo: boolean;
|
|
120
|
+
/** True when there is an undone edit to redo. */
|
|
121
|
+
canRedo: boolean;
|
|
112
122
|
};
|
|
113
123
|
/**
|
|
114
124
|
* A declarative toolbar button. JSON-friendly on purpose — a host (or an
|
|
@@ -134,6 +144,8 @@ type FlowEditorAction = {
|
|
|
134
144
|
type FlowEditorBuiltins = {
|
|
135
145
|
run?: boolean;
|
|
136
146
|
delete?: boolean;
|
|
147
|
+
/** Undo/redo toolbar buttons. Default true. */
|
|
148
|
+
history?: boolean;
|
|
137
149
|
/** Right-click a node for Delete / Duplicate. Default true. */
|
|
138
150
|
contextMenu?: boolean;
|
|
139
151
|
/** Right-click a connection for Label / Delete. Default true. */
|
|
@@ -208,6 +220,17 @@ type FlowEditorProps = {
|
|
|
208
220
|
onDelete?: (ids: string[]) => void;
|
|
209
221
|
/** Called after connections are broken, with the deleted edge ids. */
|
|
210
222
|
onEdgeDelete?: (ids: string[]) => void;
|
|
223
|
+
/**
|
|
224
|
+
* Stage destructive edits for human confirmation. When set, every delete path
|
|
225
|
+
* — keyboard, panel, context menu, and `api.deleteNodes`/`deleteEdges` —
|
|
226
|
+
* calls this first; return false to veto. Default: delete immediately. This
|
|
227
|
+
* realizes the component contract's "agents propose, humans confirm" on the
|
|
228
|
+
* canvas.
|
|
229
|
+
*/
|
|
230
|
+
confirmDelete?: (targets: {
|
|
231
|
+
nodes: FlowNode[];
|
|
232
|
+
edges: Edge[];
|
|
233
|
+
}) => boolean | Promise<boolean>;
|
|
211
234
|
className?: string;
|
|
212
235
|
style?: CSSProperties;
|
|
213
236
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { ImportIssue, ImportOptions, ImportResult, WORKFLOW_SCHEMA_URL, WORKFLOW
|
|
|
10
10
|
import { N as NodeCategory, a as NodeKindDefinition, C as ConfigField } from './types-NerkPtHS.js';
|
|
11
11
|
export { D as DocumentConfigField, K as KeyValueConfigField, P as PortSpec, R as RenderBodyContext, d as RepeaterConfigField, e as RepeaterRowField } from './types-NerkPtHS.js';
|
|
12
12
|
import { FlowRunFeedEntry } from './runtime/index.js';
|
|
13
|
-
export { UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, useFlowRun, useFlowState } from './runtime/index.js';
|
|
13
|
+
export { HistoryController, UseFlowHistoryReturn, UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './runtime/index.js';
|
|
14
14
|
export { R as RunOptions, a as RunResult, r as runFlow } from './run-flow-ChBi6q-W.js';
|
|
15
15
|
export { L as LEGACY_PAUSE_PREFIXES, a as PAUSE_PREFIX, P as PauseAwaiting, b as PauseSignal, d as decodePause, e as encodePause, i as isPause, p as pauseForHuman } from './pause-9iT4tCEV.js';
|
|
16
16
|
import './capabilities-DUhAc-EJ.js';
|
|
@@ -109,6 +109,16 @@ type FlowEditorApi = {
|
|
|
109
109
|
/** Open a file picker and load a workflow. */
|
|
110
110
|
importWorkflow: () => void;
|
|
111
111
|
fitView: () => void;
|
|
112
|
+
/** Undo the last committing edit (add / delete / connect / config / drag /
|
|
113
|
+
* import). Transient interactions (a drag in progress, selection) are not
|
|
114
|
+
* their own steps. */
|
|
115
|
+
undo: () => void;
|
|
116
|
+
/** Redo the last undone edit. */
|
|
117
|
+
redo: () => void;
|
|
118
|
+
/** True when there is an edit to undo. */
|
|
119
|
+
canUndo: boolean;
|
|
120
|
+
/** True when there is an undone edit to redo. */
|
|
121
|
+
canRedo: boolean;
|
|
112
122
|
};
|
|
113
123
|
/**
|
|
114
124
|
* A declarative toolbar button. JSON-friendly on purpose — a host (or an
|
|
@@ -134,6 +144,8 @@ type FlowEditorAction = {
|
|
|
134
144
|
type FlowEditorBuiltins = {
|
|
135
145
|
run?: boolean;
|
|
136
146
|
delete?: boolean;
|
|
147
|
+
/** Undo/redo toolbar buttons. Default true. */
|
|
148
|
+
history?: boolean;
|
|
137
149
|
/** Right-click a node for Delete / Duplicate. Default true. */
|
|
138
150
|
contextMenu?: boolean;
|
|
139
151
|
/** Right-click a connection for Label / Delete. Default true. */
|
|
@@ -208,6 +220,17 @@ type FlowEditorProps = {
|
|
|
208
220
|
onDelete?: (ids: string[]) => void;
|
|
209
221
|
/** Called after connections are broken, with the deleted edge ids. */
|
|
210
222
|
onEdgeDelete?: (ids: string[]) => void;
|
|
223
|
+
/**
|
|
224
|
+
* Stage destructive edits for human confirmation. When set, every delete path
|
|
225
|
+
* — keyboard, panel, context menu, and `api.deleteNodes`/`deleteEdges` —
|
|
226
|
+
* calls this first; return false to veto. Default: delete immediately. This
|
|
227
|
+
* realizes the component contract's "agents propose, humans confirm" on the
|
|
228
|
+
* canvas.
|
|
229
|
+
*/
|
|
230
|
+
confirmDelete?: (targets: {
|
|
231
|
+
nodes: FlowNode[];
|
|
232
|
+
edges: Edge[];
|
|
233
|
+
}) => boolean | Promise<boolean>;
|
|
211
234
|
className?: string;
|
|
212
235
|
style?: CSSProperties;
|
|
213
236
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useFlowState, useFlowRun, applyStatusesToNodes } from './chunk-
|
|
2
|
-
export { applyStatusesToNodes, useFlowRun, useFlowState } from './chunk-
|
|
1
|
+
import { useFlowState, useFlowRun, useFlowHistory, applyStatusesToNodes } from './chunk-PHX4SBOD.js';
|
|
2
|
+
export { applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './chunk-PHX4SBOD.js';
|
|
3
3
|
import { buildNodeTypes, registerBuiltinKinds, createConnectionValidator } from './chunk-FSC64BPD.js';
|
|
4
4
|
export { ANY_PORT_TYPE, BUILTIN_KINDS, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility, registerBuiltinKinds } from './chunk-FSC64BPD.js';
|
|
5
5
|
import { ReactFlowProvider, useReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, Position, Handle, index, BackgroundVariant, Background, Controls, MiniMap } from './chunk-NF6NPY5N.js';
|
|
@@ -878,13 +878,16 @@ function FlowEditorInner({
|
|
|
878
878
|
onSelectionChange,
|
|
879
879
|
onDelete,
|
|
880
880
|
onEdgeDelete,
|
|
881
|
+
confirmDelete,
|
|
881
882
|
apiRef
|
|
882
883
|
}) {
|
|
883
884
|
const internal = useFlowState(initial);
|
|
884
885
|
const runner = useFlowRun();
|
|
885
886
|
const rf = useReactFlow();
|
|
886
887
|
const controlled = value !== void 0;
|
|
887
|
-
const
|
|
888
|
+
const baseFlow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
|
|
889
|
+
const hist = useFlowHistory(baseFlow);
|
|
890
|
+
const flow = hist.flow;
|
|
888
891
|
const [, force] = useState(0);
|
|
889
892
|
useEffect(() => onNodeKindsChanged(() => force((n) => n + 1)), []);
|
|
890
893
|
const nodeTypes = useMemo(() => buildNodeTypes(), [listNodeKinds().length]);
|
|
@@ -937,6 +940,23 @@ function FlowEditorInner({
|
|
|
937
940
|
window.removeEventListener("keydown", onKey);
|
|
938
941
|
};
|
|
939
942
|
}, [menu, labelEdit]);
|
|
943
|
+
useEffect(() => {
|
|
944
|
+
const onKey = (e) => {
|
|
945
|
+
const t = e.target;
|
|
946
|
+
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
|
947
|
+
if (!(e.ctrlKey || e.metaKey)) return;
|
|
948
|
+
const key = e.key.toLowerCase();
|
|
949
|
+
if (key === "z" && !e.shiftKey) {
|
|
950
|
+
e.preventDefault();
|
|
951
|
+
hist.undo();
|
|
952
|
+
} else if (key === "z" && e.shiftKey || key === "y") {
|
|
953
|
+
e.preventDefault();
|
|
954
|
+
hist.redo();
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
window.addEventListener("keydown", onKey);
|
|
958
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
959
|
+
}, [hist]);
|
|
940
960
|
useEffect(() => {
|
|
941
961
|
if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
|
|
942
962
|
}, [flow.nodes, flow.edges, onChange, controlled]);
|
|
@@ -961,15 +981,32 @@ function FlowEditorInner({
|
|
|
961
981
|
[flow]
|
|
962
982
|
);
|
|
963
983
|
const deleteNodes = useCallback(
|
|
964
|
-
(ids) => {
|
|
984
|
+
async (ids) => {
|
|
965
985
|
if (ids.length === 0) return;
|
|
986
|
+
const targets = flow.nodes.filter((n) => ids.includes(n.id));
|
|
987
|
+
if (targets.length === 0) return;
|
|
988
|
+
if (confirmDelete) {
|
|
989
|
+
const attached = flow.edges.filter((e) => ids.includes(e.source) || ids.includes(e.target));
|
|
990
|
+
if (!await confirmDelete({ nodes: targets, edges: attached })) return;
|
|
991
|
+
}
|
|
966
992
|
const doomed = new Set(ids);
|
|
967
|
-
flow.
|
|
968
|
-
flow.setEdges((all) => removeNodes({ nodes: [], edges: all }, ids).edges);
|
|
993
|
+
flow.setGraph(removeNodes({ nodes: flow.nodes, edges: flow.edges }, ids));
|
|
969
994
|
setSelectedId((cur) => cur !== null && doomed.has(cur) ? null : cur);
|
|
970
995
|
onDelete?.(ids);
|
|
971
996
|
},
|
|
972
|
-
[flow, onDelete]
|
|
997
|
+
[flow, onDelete, confirmDelete]
|
|
998
|
+
);
|
|
999
|
+
const deleteEdges = useCallback(
|
|
1000
|
+
async (ids) => {
|
|
1001
|
+
if (ids.length === 0) return;
|
|
1002
|
+
const targets = flow.edges.filter((e) => ids.includes(e.id));
|
|
1003
|
+
if (targets.length === 0) return;
|
|
1004
|
+
if (confirmDelete && !await confirmDelete({ nodes: [], edges: targets })) return;
|
|
1005
|
+
flow.setEdges((all) => removeEdges(all, ids));
|
|
1006
|
+
setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
|
|
1007
|
+
onEdgeDelete?.(ids);
|
|
1008
|
+
},
|
|
1009
|
+
[flow, onEdgeDelete, confirmDelete]
|
|
973
1010
|
);
|
|
974
1011
|
const api = useMemo(() => {
|
|
975
1012
|
const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
|
|
@@ -989,18 +1026,8 @@ function FlowEditorInner({
|
|
|
989
1026
|
updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
|
|
990
1027
|
deleteNodes,
|
|
991
1028
|
deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
|
|
992
|
-
deleteEdges
|
|
993
|
-
|
|
994
|
-
flow.setEdges((all) => removeEdges(all, ids));
|
|
995
|
-
setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
|
|
996
|
-
onEdgeDelete?.(ids);
|
|
997
|
-
},
|
|
998
|
-
deleteSelectedEdge: () => {
|
|
999
|
-
if (!selectedEdgeId) return;
|
|
1000
|
-
flow.setEdges((all) => removeEdges(all, [selectedEdgeId]));
|
|
1001
|
-
setSelectedEdgeId(null);
|
|
1002
|
-
onEdgeDelete?.([selectedEdgeId]);
|
|
1003
|
-
},
|
|
1029
|
+
deleteEdges,
|
|
1030
|
+
deleteSelectedEdge: () => deleteEdges(selectedEdgeId ? [selectedEdgeId] : []),
|
|
1004
1031
|
setEdgeLabel: (id, label) => flow.setEdges((all) => setEdgeLabel(all, id, label)),
|
|
1005
1032
|
duplicateNode: (id) => {
|
|
1006
1033
|
const src = flow.nodes.find((n) => n.id === id);
|
|
@@ -1010,22 +1037,20 @@ function FlowEditorInner({
|
|
|
1010
1037
|
setSelectedId(copy.id);
|
|
1011
1038
|
return copy.id;
|
|
1012
1039
|
},
|
|
1013
|
-
setGraph: (graph) =>
|
|
1014
|
-
flow.setNodes(graph.nodes);
|
|
1015
|
-
flow.setEdges(graph.edges);
|
|
1016
|
-
},
|
|
1040
|
+
setGraph: (graph) => flow.setGraph(graph),
|
|
1017
1041
|
run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
|
|
1018
1042
|
cancel: runner.cancel,
|
|
1019
1043
|
reset: runner.reset,
|
|
1020
1044
|
toWorkflow,
|
|
1021
1045
|
exportWorkflow: () => downloadWorkflow(toWorkflow(), metadata),
|
|
1022
|
-
importWorkflow: () => pickWorkflow((graph) =>
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1046
|
+
importWorkflow: () => pickWorkflow((graph) => flow.setGraph(graph)),
|
|
1047
|
+
fitView: () => rf.fitView({ padding: 0.2 }),
|
|
1048
|
+
undo: hist.undo,
|
|
1049
|
+
redo: hist.redo,
|
|
1050
|
+
canUndo: hist.canUndo,
|
|
1051
|
+
canRedo: hist.canRedo
|
|
1027
1052
|
};
|
|
1028
|
-
}, [flow, selectedId, selected, runner, executors, metadata, addNode, deleteNodes, rf]);
|
|
1053
|
+
}, [flow, selectedId, selected, selectedEdgeId, selectedEdge, runner, executors, metadata, addNode, deleteNodes, deleteEdges, hist, rf]);
|
|
1029
1054
|
useImperativeHandle(apiRef, () => api, [api]);
|
|
1030
1055
|
const dropHandlers = paletteDropHandlers((kindName, evt) => {
|
|
1031
1056
|
const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
|
|
@@ -1036,6 +1061,31 @@ function FlowEditorInner({
|
|
|
1036
1061
|
const toolbar = slots.toolbar ? slots.toolbar(api) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1037
1062
|
startActions.map((a) => renderAction(a, api)),
|
|
1038
1063
|
builtins.run !== false && /* @__PURE__ */ jsx(FlowRunControls, { running: api.running, onRun: api.run, onCancel: api.cancel, onReset: api.reset }),
|
|
1064
|
+
builtins.history !== false && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1065
|
+
/* @__PURE__ */ jsx("span", { className: "ff-editor__sep" }),
|
|
1066
|
+
/* @__PURE__ */ jsx(
|
|
1067
|
+
"button",
|
|
1068
|
+
{
|
|
1069
|
+
className: "ff-editor__btn",
|
|
1070
|
+
"data-action": "undo",
|
|
1071
|
+
title: "Undo (Ctrl+Z)",
|
|
1072
|
+
disabled: !api.canUndo,
|
|
1073
|
+
onClick: api.undo,
|
|
1074
|
+
children: "\u21B6 Undo"
|
|
1075
|
+
}
|
|
1076
|
+
),
|
|
1077
|
+
/* @__PURE__ */ jsx(
|
|
1078
|
+
"button",
|
|
1079
|
+
{
|
|
1080
|
+
className: "ff-editor__btn",
|
|
1081
|
+
"data-action": "redo",
|
|
1082
|
+
title: "Redo (Ctrl+Shift+Z)",
|
|
1083
|
+
disabled: !api.canRedo,
|
|
1084
|
+
onClick: api.redo,
|
|
1085
|
+
children: "\u21B7 Redo"
|
|
1086
|
+
}
|
|
1087
|
+
)
|
|
1088
|
+
] }),
|
|
1039
1089
|
(builtins.export !== false || builtins.import !== false) && /* @__PURE__ */ jsx("span", { className: "ff-editor__sep" }),
|
|
1040
1090
|
builtins.export !== false && /* @__PURE__ */ jsx("button", { className: "ff-editor__btn", "data-action": "export", onClick: api.exportWorkflow, children: "\u2193 Export" }),
|
|
1041
1091
|
builtins.import !== false && /* @__PURE__ */ jsx("button", { className: "ff-editor__btn", "data-action": "import", onClick: api.importWorkflow, children: "\u2191 Import" }),
|
|
@@ -1060,12 +1110,14 @@ function FlowEditorInner({
|
|
|
1060
1110
|
onNodesChange: flow.onNodesChange,
|
|
1061
1111
|
onEdgesChange: flow.onEdgesChange,
|
|
1062
1112
|
onConnect: flow.onConnect,
|
|
1113
|
+
onNodeDragStart: hist.onNodeDragStart,
|
|
1063
1114
|
onNodeClick: handleNodeClick,
|
|
1064
1115
|
onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
|
|
1065
1116
|
onEdgeClick: handleEdgeClick,
|
|
1066
1117
|
onEdgeContextMenu: builtins.edgeContextMenu === false ? void 0 : handleEdgeContextMenu,
|
|
1067
1118
|
onEdgesDelete: (deleted) => onEdgeDelete?.(deleted.map((e) => e.id)),
|
|
1068
1119
|
onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
|
|
1120
|
+
onBeforeDelete: confirmDelete ? async ({ nodes, edges }) => confirmDelete({ nodes, edges }) : void 0,
|
|
1069
1121
|
deleteKeyCode: ["Delete", "Backspace"],
|
|
1070
1122
|
height: "100%",
|
|
1071
1123
|
toolbar,
|
|
@@ -1236,6 +1288,8 @@ function makeControlledFlowAdapter(value, onChange) {
|
|
|
1236
1288
|
const nextEdges = typeof next === "function" ? next(value.edges) : next;
|
|
1237
1289
|
apply({ nodes: value.nodes, edges: nextEdges });
|
|
1238
1290
|
},
|
|
1291
|
+
// Atomic both-at-once commit — the reason this exists (see UseFlowStateReturn).
|
|
1292
|
+
setGraph: (graph) => apply(graph),
|
|
1239
1293
|
onNodesChange: (changes) => {
|
|
1240
1294
|
apply({ nodes: applyNodeChanges(changes, value.nodes), edges: value.edges });
|
|
1241
1295
|
},
|