@particle-academy/fancy-flow 0.41.0 → 0.43.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/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { removeNodes, removeEdges, cloneSubgraph, alignNodes, distributeNodes, reconnectEdge, assignToLane, removeFromLane, duplicateNode, setEdgeLabel, FlowCanvas } from './chunk-WOO4B7M5.js';
2
- export { ActionNode, DecisionNode, FlowCanvas, FlowViewer, NodeShell, OutputNode, SubgraphNode, TriggerNode, alignNodes, cloneSubgraph, defaultNodeTypes, distributeNodes, reconnectEdge } from './chunk-WOO4B7M5.js';
1
+ import { removeNodes, removeEdges, cloneSubgraph, alignNodes, distributeNodes, reconnectEdge, assignToLane, removeFromLane, duplicateNode, setEdgeLabel, FlowCanvas } from './chunk-T3VQX3XB.js';
2
+ export { ActionNode, DecisionNode, FlowCanvas, FlowViewer, NodeShell, OutputNode, SubgraphNode, TriggerNode, alignNodes, cloneSubgraph, defaultNodeTypes, distributeNodes, reconnectEdge } from './chunk-T3VQX3XB.js';
3
3
  import { useFlowState, useFlowRun, useFlowHistory, applyOutputsToNodes, applyStatusesToNodes } from './chunk-A6RFLGWV.js';
4
4
  export { applyOutputsToNodes, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './chunk-A6RFLGWV.js';
5
5
  export { runCohort } from './chunk-QZTGV3ZL.js';
6
- import { buildNodeTypes, FlowEditorProvider, registerBuiltinKinds } from './chunk-UX65ML3J.js';
7
- export { ANY_PORT_TYPE, BUILTIN_KINDS, LaneNode, NoteNode, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility, registerBuiltinKinds, useFlowEditor, useFlowEditorOptional } from './chunk-UX65ML3J.js';
6
+ import { buildNodeTypes, FlowEditorProvider, registerBuiltinKinds } from './chunk-55KQS3LK.js';
7
+ export { ANY_PORT_TYPE, BUILTIN_KINDS, LaneNode, NoteNode, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility, registerBuiltinKinds, useFlowEditor, useFlowEditorOptional } from './chunk-55KQS3LK.js';
8
8
  import { ReactFlowProvider, useReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, Position, Handle } from './chunk-OWENS2H5.js';
9
9
  export { LEGACY_PAUSE_PREFIXES, PAUSE_PREFIX, decodePause, encodePause, isPause, pauseForHuman } from './chunk-UEOE6B52.js';
10
10
  import './chunk-USL4FMFU.js';
@@ -109,6 +109,230 @@ function paletteDropHandlers(onDrop) {
109
109
  }
110
110
  };
111
111
  }
112
+
113
+ // src/expressions/variables.ts
114
+ var ROOT = "$json";
115
+ function expr(path) {
116
+ return `{{ ${path} }}`;
117
+ }
118
+ function outputFieldsFor(node) {
119
+ const kindName = node.data?.kind ?? node.type;
120
+ if (!kindName) return [];
121
+ const kind = getNodeKind(kindName);
122
+ const shape = kind?.outputShape;
123
+ if (!shape) return [];
124
+ if (typeof shape === "function") {
125
+ try {
126
+ const config = node.data?.config ?? {};
127
+ return shape(config) ?? [];
128
+ } catch {
129
+ return [];
130
+ }
131
+ }
132
+ return shape;
133
+ }
134
+ function baseVariables() {
135
+ return [
136
+ { expression: expr(ROOT), path: ROOT, type: "unknown", description: "The whole incoming value." }
137
+ ];
138
+ }
139
+ function availableVariables(graph, nodeId) {
140
+ const out = baseVariables();
141
+ const seen = /* @__PURE__ */ new Set([ROOT]);
142
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
143
+ const upstreamIds = graph.edges.filter((e) => e.target === nodeId).map((e) => e.source);
144
+ for (const id of upstreamIds) {
145
+ if (id === nodeId) continue;
146
+ const upstream = byId.get(id);
147
+ if (!upstream) continue;
148
+ const kindName = upstream.data?.kind ?? upstream.type;
149
+ const label = upstream.data?.label ?? (kindName ? getNodeKind(kindName)?.label : void 0);
150
+ for (const field of outputFieldsFor(upstream)) {
151
+ const path = `${ROOT}.${field.path}`;
152
+ if (seen.has(path)) continue;
153
+ seen.add(path);
154
+ out.push({
155
+ expression: expr(path),
156
+ path,
157
+ type: field.type,
158
+ description: field.description,
159
+ source: label
160
+ });
161
+ }
162
+ }
163
+ return out;
164
+ }
165
+ function describeExpressionGrammar() {
166
+ return {
167
+ forms: [
168
+ { syntax: "{{ $json }}", meaning: "The whole value arriving on this node's input." },
169
+ { syntax: "{{ $json.field }}", meaning: "One key of that value. Dot-paths nest: $json.user.email." },
170
+ { syntax: "{{ $input.field }}", meaning: "An alias for $json \u2014 the same value, either spelling." },
171
+ { syntax: "{{ in }}", meaning: "The input by its context key, rather than through the alias." }
172
+ ],
173
+ note: "Dot-paths only \u2014 no arithmetic, comparisons or function calls; anything else resolves to nothing. A field that is exactly one expression keeps the resolved value's type; mixed text interpolates as a string. fancy-flow's own runFlow does not interpolate: it hands config to your executor verbatim, and the fancy-flow-php runtime is what resolves these."
174
+ };
175
+ }
176
+ function findTrigger(text2, caret) {
177
+ const before = text2.slice(0, caret);
178
+ const open = before.lastIndexOf("{{");
179
+ if (open === -1) return null;
180
+ if (before.indexOf("}}", open) !== -1) return null;
181
+ return { open, query: before.slice(open + 2).trim() };
182
+ }
183
+ function filterVariables(vars, query) {
184
+ const q = query.trim().toLowerCase();
185
+ if (q === "") return vars;
186
+ return vars.filter((v) => v.path.toLowerCase().includes(q));
187
+ }
188
+ function applyCompletion(text2, caret, trigger, variable) {
189
+ const head = text2.slice(0, trigger.open);
190
+ const tail = text2.slice(caret);
191
+ return { value: `${head}${variable.expression}${tail}`, caret: head.length + variable.expression.length };
192
+ }
193
+ function ExpressionField({
194
+ value,
195
+ onChange,
196
+ placeholder,
197
+ rows = 2,
198
+ handle,
199
+ graph,
200
+ nodeId
201
+ }) {
202
+ const ref = useRef(null);
203
+ const [trigger, setTrigger] = useState(null);
204
+ const [active, setActive] = useState(0);
205
+ const [showHelp, setShowHelp] = useState(false);
206
+ const variables = useMemo(
207
+ () => graph && nodeId ? availableVariables(graph, nodeId) : baseVariables(),
208
+ [graph, nodeId]
209
+ );
210
+ const matches = useMemo(
211
+ () => trigger ? filterVariables(variables, trigger.query) : [],
212
+ [trigger, variables]
213
+ );
214
+ const help = useMemo(() => describeExpressionGrammar(), []);
215
+ const open = trigger !== null && matches.length > 0;
216
+ const sync = (el) => {
217
+ const next = findTrigger(el.value, el.selectionStart ?? el.value.length);
218
+ setTrigger(next);
219
+ setActive(0);
220
+ };
221
+ const choose = (variable) => {
222
+ const el = ref.current;
223
+ if (!el || !trigger) return;
224
+ const { value: next, caret } = applyCompletion(
225
+ el.value,
226
+ el.selectionStart ?? el.value.length,
227
+ trigger,
228
+ variable
229
+ );
230
+ onChange(next);
231
+ setTrigger(null);
232
+ requestAnimationFrame(() => {
233
+ el.focus();
234
+ el.setSelectionRange(caret, caret);
235
+ });
236
+ };
237
+ const onKeyDown = (e) => {
238
+ if (!open) return;
239
+ if (e.key === "ArrowDown") {
240
+ e.preventDefault();
241
+ setActive((i) => (i + 1) % matches.length);
242
+ } else if (e.key === "ArrowUp") {
243
+ e.preventDefault();
244
+ setActive((i) => (i - 1 + matches.length) % matches.length);
245
+ } else if (e.key === "Enter" || e.key === "Tab") {
246
+ e.preventDefault();
247
+ choose(matches[active] ?? matches[0]);
248
+ } else if (e.key === "Escape") {
249
+ e.preventDefault();
250
+ setTrigger(null);
251
+ }
252
+ };
253
+ return /* @__PURE__ */ jsxs("div", { className: "ff-expression", "data-ff-expression": handle["data-ff-field"], children: [
254
+ /* @__PURE__ */ jsx(
255
+ "textarea",
256
+ {
257
+ ...handle,
258
+ ref,
259
+ className: "ff-panel__input ff-panel__input--expression",
260
+ rows,
261
+ value: value ?? "",
262
+ placeholder: placeholder ?? "{{ $json.field }}",
263
+ spellCheck: false,
264
+ role: "combobox",
265
+ "aria-expanded": open,
266
+ "aria-autocomplete": "list",
267
+ "aria-controls": open ? `${handle.id ?? handle["data-ff-field"]}-vars` : void 0,
268
+ onChange: (e) => {
269
+ onChange(e.target.value);
270
+ sync(e.target);
271
+ },
272
+ onKeyDown,
273
+ onClick: (e) => sync(e.currentTarget),
274
+ onBlur: () => {
275
+ window.setTimeout(() => setTrigger(null), 120);
276
+ }
277
+ }
278
+ ),
279
+ /* @__PURE__ */ jsxs("div", { className: "ff-expression__bar", children: [
280
+ /* @__PURE__ */ jsxs(
281
+ "button",
282
+ {
283
+ type: "button",
284
+ className: "ff-expression__helpbtn",
285
+ "aria-expanded": showHelp,
286
+ onClick: () => setShowHelp((s) => !s),
287
+ children: [
288
+ "{{ }}",
289
+ " reference"
290
+ ]
291
+ }
292
+ ),
293
+ variables.length > 1 && /* @__PURE__ */ jsxs("span", { className: "ff-expression__hint", children: [
294
+ "type ",
295
+ /* @__PURE__ */ jsx("code", { children: "{{" }),
296
+ " for ",
297
+ variables.length,
298
+ " variables"
299
+ ] })
300
+ ] }),
301
+ open && /* @__PURE__ */ jsx(
302
+ "ul",
303
+ {
304
+ className: "ff-expression__menu",
305
+ id: `${handle.id ?? handle["data-ff-field"]}-vars`,
306
+ role: "listbox",
307
+ "data-ff-expression-menu": handle["data-ff-field"],
308
+ children: matches.map((v, i) => /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": i === active, children: /* @__PURE__ */ jsxs(
309
+ "button",
310
+ {
311
+ type: "button",
312
+ className: `ff-expression__option${i === active ? " is-active" : ""}`,
313
+ "data-ff-expression-option": v.path,
314
+ onMouseDown: (e) => {
315
+ e.preventDefault();
316
+ choose(v);
317
+ },
318
+ children: [
319
+ /* @__PURE__ */ jsx("code", { className: "ff-expression__path", children: v.path }),
320
+ v.source && /* @__PURE__ */ jsx("span", { className: "ff-expression__source", children: v.source }),
321
+ v.description && /* @__PURE__ */ jsx("span", { className: "ff-expression__desc", children: v.description })
322
+ ]
323
+ }
324
+ ) }, v.expression))
325
+ }
326
+ ),
327
+ showHelp && /* @__PURE__ */ jsxs("div", { className: "ff-expression__help", role: "note", "data-ff-expression-help": true, children: [
328
+ /* @__PURE__ */ jsx("dl", { children: help.forms.map((f) => /* @__PURE__ */ jsxs("div", { children: [
329
+ /* @__PURE__ */ jsx("dt", { children: /* @__PURE__ */ jsx("code", { children: f.syntax }) }),
330
+ /* @__PURE__ */ jsx("dd", { children: f.meaning })
331
+ ] }, f.syntax)) }),
332
+ /* @__PURE__ */ jsx("p", { className: "ff-expression__note", children: help.note })
333
+ ] })
334
+ ] });
335
+ }
112
336
  function ConfigFieldRenderer({
113
337
  field,
114
338
  value,
@@ -116,7 +340,9 @@ function ConfigFieldRenderer({
116
340
  id,
117
341
  renderCredentialField,
118
342
  renderDocumentField,
119
- fieldRenderers
343
+ fieldRenderers,
344
+ graph,
345
+ nodeId
120
346
  }) {
121
347
  const handle = { id, "data-ff-field": field.key };
122
348
  const custom = fieldRenderers?.[field.type];
@@ -198,15 +424,14 @@ function ConfigFieldRenderer({
198
424
  return /* @__PURE__ */ jsx(JsonField, { value, onChange, rows: field.rows, handle });
199
425
  case "expression":
200
426
  return /* @__PURE__ */ jsx(
201
- "textarea",
427
+ ExpressionField,
202
428
  {
203
- ...handle,
204
- className: "ff-panel__input ff-panel__input--expression",
205
- rows: 2,
429
+ handle,
206
430
  value: value ?? "",
431
+ onChange,
207
432
  placeholder: field.example ?? "{{ $json.field }}",
208
- spellCheck: false,
209
- onChange: (e) => onChange(e.target.value)
433
+ graph,
434
+ nodeId
210
435
  }
211
436
  );
212
437
  case "credential":
@@ -233,7 +458,9 @@ function ConfigFieldRenderer({
233
458
  onChange,
234
459
  renderCredentialField,
235
460
  renderDocumentField,
236
- fieldRenderers
461
+ fieldRenderers,
462
+ graph,
463
+ nodeId
237
464
  }
238
465
  );
239
466
  case "keyvalue":
@@ -286,6 +513,8 @@ function RepeaterField({
286
513
  field,
287
514
  value,
288
515
  onChange,
516
+ graph,
517
+ nodeId,
289
518
  renderCredentialField,
290
519
  renderDocumentField,
291
520
  fieldRenderers
@@ -366,6 +595,8 @@ function RepeaterField({
366
595
  /* @__PURE__ */ jsx(
367
596
  ConfigFieldRenderer,
368
597
  {
598
+ graph,
599
+ nodeId,
369
600
  field: sub,
370
601
  value: row[sub.key],
371
602
  onChange: (cell) => setCell(i, sub.key, cell),
@@ -480,7 +711,7 @@ function JsonField({
480
711
  return "";
481
712
  }
482
713
  }, [value]);
483
- const [text, setText] = useState(serialized);
714
+ const [text2, setText] = useState(serialized);
484
715
  const [error, setError] = useState(null);
485
716
  const [touched, setTouched] = useState(false);
486
717
  const lastProps = useRef(serialized);
@@ -518,7 +749,7 @@ function JsonField({
518
749
  ...handle,
519
750
  className: "ff-panel__input ff-panel__input--json",
520
751
  rows: rows ?? 6,
521
- value: text,
752
+ value: text2,
522
753
  spellCheck: false,
523
754
  "aria-invalid": showError || void 0,
524
755
  "aria-describedby": showError ? errorId : void 0,
@@ -547,6 +778,7 @@ function NodeConfigPanel({
547
778
  renderCredentialField,
548
779
  renderDocumentField,
549
780
  fieldRenderers,
781
+ graph,
550
782
  className,
551
783
  style
552
784
  }) {
@@ -652,7 +884,9 @@ function NodeConfigPanel({
652
884
  onChange: (v) => setConfigValue(field.key, v),
653
885
  renderCredentialField,
654
886
  renderDocumentField: documentField,
655
- fieldRenderers
887
+ fieldRenderers,
888
+ graph,
889
+ nodeId: node.id
656
890
  }
657
891
  )
658
892
  ] }, field.key))
@@ -1395,8 +1629,8 @@ function FlowEditorInner({
1395
1629
  x: labelEdit.x,
1396
1630
  y: labelEdit.y,
1397
1631
  initial: flow.edges.find((e) => e.id === labelEdit.edgeId)?.label ?? "",
1398
- onCommit: (text) => {
1399
- api.setEdgeLabel(labelEdit.edgeId, text);
1632
+ onCommit: (text2) => {
1633
+ api.setEdgeLabel(labelEdit.edgeId, text2);
1400
1634
  setLabelEdit(null);
1401
1635
  },
1402
1636
  onCancel: () => setLabelEdit(null)
@@ -1416,6 +1650,7 @@ function FlowEditorInner({
1416
1650
  node: api.selected,
1417
1651
  onChange: api.updateNode,
1418
1652
  fieldRenderers,
1653
+ graph: flow,
1419
1654
  onDelete: builtins.delete === false ? void 0 : (n) => api.deleteNodes([n.id])
1420
1655
  }
1421
1656
  ),
@@ -1500,7 +1735,7 @@ function EdgeLabelEditor({
1500
1735
  onCommit,
1501
1736
  onCancel
1502
1737
  }) {
1503
- const [text, setText] = useState(initial);
1738
+ const [text2, setText] = useState(initial);
1504
1739
  const ref = useRef(null);
1505
1740
  useEffect(() => {
1506
1741
  ref.current?.focus();
@@ -1517,17 +1752,17 @@ function EdgeLabelEditor({
1517
1752
  {
1518
1753
  ref,
1519
1754
  className: "ff-panel__input",
1520
- value: text,
1755
+ value: text2,
1521
1756
  placeholder: "Label this connection",
1522
1757
  "aria-label": "Connection label",
1523
1758
  "data-action": "edge-label-input",
1524
1759
  onChange: (e) => setText(e.target.value),
1525
1760
  onKeyDown: (e) => {
1526
1761
  e.stopPropagation();
1527
- if (e.key === "Enter") onCommit(text);
1762
+ if (e.key === "Enter") onCommit(text2);
1528
1763
  if (e.key === "Escape") onCancel();
1529
1764
  },
1530
- onBlur: () => onCommit(text)
1765
+ onBlur: () => onCommit(text2)
1531
1766
  }
1532
1767
  )
1533
1768
  }
@@ -1588,9 +1823,70 @@ var flowKeys = {
1588
1823
  run: (runId) => ["flow", "runs", runId]
1589
1824
  };
1590
1825
 
1826
+ // src/expressions/expr.ts
1827
+ var WHOLE = /^\{\{\s*([\s\S]*?)\s*\}\}$/;
1828
+ var EACH = /\{\{\s*([\s\S]*?)\s*\}\}/g;
1829
+ var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
1830
+ function resolvePath(path, context) {
1831
+ const trimmed = path.trim();
1832
+ if (trimmed === "") return null;
1833
+ const segments = trimmed.split(".");
1834
+ let cursor;
1835
+ const head = segments[0];
1836
+ if (head === "$json" || head === "$input") {
1837
+ cursor = context !== null && typeof context === "object" && "in" in context ? context.in : context;
1838
+ segments.shift();
1839
+ } else {
1840
+ cursor = context;
1841
+ }
1842
+ for (const segment of segments) {
1843
+ if (cursor === null || cursor === void 0) return null;
1844
+ if (typeof cursor !== "object") return null;
1845
+ const next = cursor[segment];
1846
+ if (next === void 0) return null;
1847
+ cursor = next;
1848
+ }
1849
+ return cursor === void 0 ? null : cursor;
1850
+ }
1851
+ function evaluateExpression(template, context) {
1852
+ if (typeof template !== "string") return template;
1853
+ const whole = WHOLE.exec(template.trim());
1854
+ if (whole) return resolvePath(whole[1] ?? "", context);
1855
+ return template.replace(EACH, (_m, path) => stringify(resolvePath(path, context)));
1856
+ }
1857
+ function truthy(value) {
1858
+ if (typeof value === "boolean") return value;
1859
+ if (value === null || value === void 0) return false;
1860
+ if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
1861
+ if (Array.isArray(value)) return value.length > 0;
1862
+ if (typeof value === "number") return value !== 0;
1863
+ return Boolean(value);
1864
+ }
1865
+ function text(value) {
1866
+ return stringify(value);
1867
+ }
1868
+ function stringify(value) {
1869
+ if (typeof value === "string") return value;
1870
+ if (typeof value === "boolean") return value ? "true" : "false";
1871
+ if (value === null || value === void 0) return "";
1872
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
1873
+ try {
1874
+ return JSON.stringify(value) ?? "";
1875
+ } catch {
1876
+ return "";
1877
+ }
1878
+ }
1879
+ function evaluateConfig(config, context) {
1880
+ const out = {};
1881
+ for (const [key, value] of Object.entries(config)) {
1882
+ out[key] = Array.isArray(value) ? value.map((v) => evaluateExpression(v, context)) : value !== null && typeof value === "object" ? evaluateConfig(value, context) : evaluateExpression(value, context);
1883
+ }
1884
+ return out;
1885
+ }
1886
+
1591
1887
  // src/index.ts
1592
1888
  registerBuiltinKinds();
1593
1889
 
1594
- export { ConfigFieldRenderer, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodePort, defineNode, flowKeys, flowLive, paletteDropHandlers };
1890
+ export { ConfigFieldRenderer, ExpressionField, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodePort, availableVariables, baseVariables, defineNode, describeExpressionGrammar, evaluateConfig, evaluateExpression, flowKeys, flowLive, outputFieldsFor, paletteDropHandlers, resolvePath, text, truthy };
1595
1891
  //# sourceMappingURL=index.js.map
1596
1892
  //# sourceMappingURL=index.js.map