@particle-academy/fancy-flow 0.7.0 → 0.9.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 +93 -0
- package/dist/{chunk-BCXECQUC.js → chunk-6GGFEZH7.js} +3 -3
- package/dist/{chunk-BCXECQUC.js.map → chunk-6GGFEZH7.js.map} +1 -1
- package/dist/{chunk-M2XKGQQL.js → chunk-6RHAQ2LP.js} +228 -17
- package/dist/chunk-6RHAQ2LP.js.map +1 -0
- package/dist/{chunk-WNVBXXOL.js → chunk-CPSOC27D.js} +43 -2
- package/dist/chunk-CPSOC27D.js.map +1 -0
- package/dist/{chunk-NVULCEDX.js → chunk-HNBO4HP3.js} +8 -4
- package/dist/chunk-HNBO4HP3.js.map +1 -0
- package/dist/chunk-TITD5W4Y.js +26 -0
- package/dist/chunk-TITD5W4Y.js.map +1 -0
- package/dist/{chunk-QSSQRQN4.js → chunk-VEI743ZX.js} +3 -3
- package/dist/{chunk-QSSQRQN4.js.map → chunk-VEI743ZX.js.map} +1 -1
- package/dist/engine.cjs +32 -2
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.js +3 -1
- package/dist/index.cjs +717 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -6
- package/dist/index.d.ts +45 -6
- package/dist/index.js +415 -23
- package/dist/index.js.map +1 -1
- package/dist/registry/index.d.cts +111 -4
- package/dist/registry/index.d.ts +111 -4
- package/dist/registry.cjs +313 -31
- package/dist/registry.cjs.map +1 -1
- package/dist/registry.js +3 -2
- package/dist/runtime.cjs +32 -2
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +4 -2
- package/dist/schema.cjs +41 -0
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +2 -2
- package/dist/styles.css +191 -0
- package/dist/styles.css.map +1 -1
- package/dist/types-BocBFh6l.d.ts +221 -0
- package/dist/types-DKqaUjF_.d.cts +221 -0
- package/dist/ux.cjs.map +1 -1
- package/dist/ux.d.cts +1 -1
- package/dist/ux.d.ts +1 -1
- package/dist/ux.js +1 -1
- package/package.json +4 -4
- package/dist/chunk-M2XKGQQL.js.map +0 -1
- package/dist/chunk-NVULCEDX.js.map +0 -1
- package/dist/chunk-WNVBXXOL.js.map +0 -1
- package/dist/types-C0wdN6QX.d.cts +0 -121
- package/dist/types-DnMe9Vsf.d.ts +0 -121
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { useFlowState, useFlowRun, applyStatusesToNodes } from './chunk-
|
|
2
|
-
export { applyStatusesToNodes, useFlowRun, useFlowState } from './chunk-
|
|
3
|
-
export { runFlow } from './chunk-
|
|
4
|
-
import { buildNodeTypes, registerBuiltinKinds } from './chunk-
|
|
5
|
-
export { BUILTIN_KINDS, RegistryNode, buildNodeTypes, registerBuiltinKinds } from './chunk-
|
|
1
|
+
import { useFlowState, useFlowRun, applyStatusesToNodes } from './chunk-VEI743ZX.js';
|
|
2
|
+
export { applyStatusesToNodes, useFlowRun, useFlowState } from './chunk-VEI743ZX.js';
|
|
3
|
+
export { runFlow } from './chunk-HNBO4HP3.js';
|
|
4
|
+
import { buildNodeTypes, registerBuiltinKinds, getRichInputAdapter } from './chunk-6RHAQ2LP.js';
|
|
5
|
+
export { BUILTIN_KINDS, RegistryNode, RichInputPreview, buildNodeTypes, getRichInputAdapter, isRichInputEnabled, onRichInputAdapterChanged, registerBuiltinKinds, registerRichInputAdapter } from './chunk-6RHAQ2LP.js';
|
|
6
6
|
import { ReactFlowProvider, useReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, Position, Handle, index, BackgroundVariant, Background, Controls, MiniMap } from './chunk-NF6NPY5N.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export { resolveNodePorts, resolvePortSpec } from './chunk-TITD5W4Y.js';
|
|
8
|
+
import { workflowToBlob, importWorkflow, exportWorkflow } from './chunk-6GGFEZH7.js';
|
|
9
|
+
export { WORKFLOW_SCHEMA_URL, WORKFLOW_SCHEMA_VERSION, exportWorkflow, importWorkflow, workflowToBlob } from './chunk-6GGFEZH7.js';
|
|
10
|
+
import { onNodeKindsChanged, listNodeKinds, getNodeKind, defaultConfigFor, validateConfig, categoryAccent } from './chunk-CPSOC27D.js';
|
|
11
|
+
export { categoryAccent, defaultConfigFor, getNodeKind, listNodeKinds, onNodeKindsChanged, registerNodeKind, validateConfig } from './chunk-CPSOC27D.js';
|
|
11
12
|
import { memo, createContext, forwardRef, useState, useEffect, useMemo, useCallback, useImperativeHandle, useRef, useContext } from 'react';
|
|
12
13
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
13
14
|
|
|
@@ -274,9 +275,18 @@ function paletteDropHandlers(onDrop) {
|
|
|
274
275
|
}
|
|
275
276
|
};
|
|
276
277
|
}
|
|
277
|
-
function ConfigFieldRenderer({
|
|
278
|
+
function ConfigFieldRenderer({
|
|
279
|
+
field,
|
|
280
|
+
value,
|
|
281
|
+
onChange,
|
|
282
|
+
renderCredentialField,
|
|
283
|
+
renderDocumentField
|
|
284
|
+
}) {
|
|
278
285
|
switch (field.type) {
|
|
279
|
-
case "text":
|
|
286
|
+
case "text": {
|
|
287
|
+
if (field.choices?.length) {
|
|
288
|
+
return /* @__PURE__ */ jsx(ChoiceField, { field, value, onChange });
|
|
289
|
+
}
|
|
280
290
|
return /* @__PURE__ */ jsx(
|
|
281
291
|
"input",
|
|
282
292
|
{
|
|
@@ -287,6 +297,7 @@ function ConfigFieldRenderer({ field, value, onChange, renderCredentialField })
|
|
|
287
297
|
onChange: (e) => onChange(e.target.value)
|
|
288
298
|
}
|
|
289
299
|
);
|
|
300
|
+
}
|
|
290
301
|
case "textarea":
|
|
291
302
|
return /* @__PURE__ */ jsx(
|
|
292
303
|
"textarea",
|
|
@@ -364,10 +375,244 @@ function ConfigFieldRenderer({ field, value, onChange, renderCredentialField })
|
|
|
364
375
|
onChange: (e) => onChange(e.target.value)
|
|
365
376
|
}
|
|
366
377
|
);
|
|
378
|
+
case "repeater":
|
|
379
|
+
return /* @__PURE__ */ jsx(
|
|
380
|
+
RepeaterField,
|
|
381
|
+
{
|
|
382
|
+
field,
|
|
383
|
+
value,
|
|
384
|
+
onChange,
|
|
385
|
+
renderCredentialField,
|
|
386
|
+
renderDocumentField
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
case "keyvalue":
|
|
390
|
+
return /* @__PURE__ */ jsx(KeyValueField, { field, value, onChange });
|
|
391
|
+
case "document":
|
|
392
|
+
if (renderDocumentField) {
|
|
393
|
+
return /* @__PURE__ */ jsx(Fragment, { children: renderDocumentField({ documentType: field.documentType, value, onChange }) });
|
|
394
|
+
}
|
|
395
|
+
return /* @__PURE__ */ jsxs("p", { className: "ff-panel__hint ff-panel__hint--missing", children: [
|
|
396
|
+
"No document editor supplied. Pass ",
|
|
397
|
+
/* @__PURE__ */ jsx("code", { children: "renderDocumentField" }),
|
|
398
|
+
" to NodeConfigPanel to author this field."
|
|
399
|
+
] });
|
|
367
400
|
default:
|
|
368
401
|
return null;
|
|
369
402
|
}
|
|
370
403
|
}
|
|
404
|
+
function normalizeChoices(choices) {
|
|
405
|
+
return choices.map(
|
|
406
|
+
(c) => typeof c === "string" ? { value: c, label: c } : { value: c.value, label: c.label ?? c.value }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
function ChoiceField({
|
|
410
|
+
field,
|
|
411
|
+
value,
|
|
412
|
+
onChange
|
|
413
|
+
}) {
|
|
414
|
+
const options = normalizeChoices(field.choices ?? []);
|
|
415
|
+
const current = typeof value === "string" ? value : "";
|
|
416
|
+
const known = options.some((o) => o.value === current);
|
|
417
|
+
return /* @__PURE__ */ jsxs(
|
|
418
|
+
"select",
|
|
419
|
+
{
|
|
420
|
+
className: "ff-panel__input",
|
|
421
|
+
value: current,
|
|
422
|
+
onChange: (e) => onChange(e.target.value),
|
|
423
|
+
children: [
|
|
424
|
+
/* @__PURE__ */ jsx("option", { value: "", disabled: true, children: field.placeholder ?? "\u2014" }),
|
|
425
|
+
options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value)),
|
|
426
|
+
current !== "" && !known && /* @__PURE__ */ jsxs("option", { value: current, children: [
|
|
427
|
+
current,
|
|
428
|
+
" (not in list)"
|
|
429
|
+
] })
|
|
430
|
+
]
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
function RepeaterField({
|
|
435
|
+
field,
|
|
436
|
+
value,
|
|
437
|
+
onChange,
|
|
438
|
+
renderCredentialField,
|
|
439
|
+
renderDocumentField
|
|
440
|
+
}) {
|
|
441
|
+
const rows = Array.isArray(value) ? value : [];
|
|
442
|
+
const max = field.maxItems ?? Infinity;
|
|
443
|
+
const min = field.minItems ?? 0;
|
|
444
|
+
const replace = (next) => onChange(next);
|
|
445
|
+
const addRow = () => {
|
|
446
|
+
const blank = {};
|
|
447
|
+
for (const f of field.fields) {
|
|
448
|
+
if ("default" in f && f.default !== void 0) blank[f.key] = f.default;
|
|
449
|
+
}
|
|
450
|
+
replace([...rows, blank]);
|
|
451
|
+
};
|
|
452
|
+
const removeRow = (i) => replace(rows.filter((_, idx) => idx !== i));
|
|
453
|
+
const moveRow = (i, delta) => {
|
|
454
|
+
const target = i + delta;
|
|
455
|
+
if (target < 0 || target >= rows.length) return;
|
|
456
|
+
const next = [...rows];
|
|
457
|
+
[next[i], next[target]] = [next[target], next[i]];
|
|
458
|
+
replace(next);
|
|
459
|
+
};
|
|
460
|
+
const setCell = (i, key, cell) => replace(rows.map((row, idx) => idx === i ? { ...row, [key]: cell } : row));
|
|
461
|
+
const rowTitle = (row, i) => {
|
|
462
|
+
const key = field.titleKey ?? field.fields[0]?.key;
|
|
463
|
+
const raw = key ? row[key] : void 0;
|
|
464
|
+
if (typeof raw === "string" && raw.trim() !== "") return raw;
|
|
465
|
+
if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
|
|
466
|
+
return `Item ${i + 1}`;
|
|
467
|
+
};
|
|
468
|
+
return /* @__PURE__ */ jsxs("div", { className: "ff-repeater", "data-ff-repeater": field.key, children: [
|
|
469
|
+
rows.length === 0 && /* @__PURE__ */ jsx("p", { className: "ff-repeater__empty", children: "None yet." }),
|
|
470
|
+
rows.map((row, i) => /* @__PURE__ */ jsxs("div", { className: "ff-repeater__row", "data-ff-repeater-row": i, children: [
|
|
471
|
+
/* @__PURE__ */ jsxs("div", { className: "ff-repeater__row-head", children: [
|
|
472
|
+
/* @__PURE__ */ jsx("span", { className: "ff-repeater__row-title", children: rowTitle(row, i) }),
|
|
473
|
+
/* @__PURE__ */ jsxs("div", { className: "ff-repeater__row-actions", children: [
|
|
474
|
+
/* @__PURE__ */ jsx(
|
|
475
|
+
"button",
|
|
476
|
+
{
|
|
477
|
+
type: "button",
|
|
478
|
+
className: "ff-repeater__btn",
|
|
479
|
+
onClick: () => moveRow(i, -1),
|
|
480
|
+
disabled: i === 0,
|
|
481
|
+
"aria-label": `Move ${rowTitle(row, i)} up`,
|
|
482
|
+
children: "\u2191"
|
|
483
|
+
}
|
|
484
|
+
),
|
|
485
|
+
/* @__PURE__ */ jsx(
|
|
486
|
+
"button",
|
|
487
|
+
{
|
|
488
|
+
type: "button",
|
|
489
|
+
className: "ff-repeater__btn",
|
|
490
|
+
onClick: () => moveRow(i, 1),
|
|
491
|
+
disabled: i === rows.length - 1,
|
|
492
|
+
"aria-label": `Move ${rowTitle(row, i)} down`,
|
|
493
|
+
children: "\u2193"
|
|
494
|
+
}
|
|
495
|
+
),
|
|
496
|
+
/* @__PURE__ */ jsx(
|
|
497
|
+
"button",
|
|
498
|
+
{
|
|
499
|
+
type: "button",
|
|
500
|
+
className: "ff-repeater__btn ff-repeater__btn--danger",
|
|
501
|
+
onClick: () => removeRow(i),
|
|
502
|
+
disabled: rows.length <= min,
|
|
503
|
+
"aria-label": `Remove ${rowTitle(row, i)}`,
|
|
504
|
+
children: "\u2715"
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
] })
|
|
508
|
+
] }),
|
|
509
|
+
field.fields.map((sub) => /* @__PURE__ */ jsxs("div", { className: "ff-repeater__cell", children: [
|
|
510
|
+
/* @__PURE__ */ jsxs("label", { className: "ff-panel__label ff-panel__label--sub", children: [
|
|
511
|
+
sub.label,
|
|
512
|
+
sub.required && /* @__PURE__ */ jsx("span", { className: "ff-panel__required", "aria-hidden": true, children: " *" })
|
|
513
|
+
] }),
|
|
514
|
+
/* @__PURE__ */ jsx(
|
|
515
|
+
ConfigFieldRenderer,
|
|
516
|
+
{
|
|
517
|
+
field: sub,
|
|
518
|
+
value: row[sub.key],
|
|
519
|
+
onChange: (cell) => setCell(i, sub.key, cell),
|
|
520
|
+
renderCredentialField,
|
|
521
|
+
renderDocumentField
|
|
522
|
+
}
|
|
523
|
+
)
|
|
524
|
+
] }, sub.key))
|
|
525
|
+
] }, i)),
|
|
526
|
+
/* @__PURE__ */ jsxs(
|
|
527
|
+
"button",
|
|
528
|
+
{
|
|
529
|
+
type: "button",
|
|
530
|
+
className: "ff-repeater__add",
|
|
531
|
+
onClick: addRow,
|
|
532
|
+
disabled: rows.length >= max,
|
|
533
|
+
children: [
|
|
534
|
+
"+ ",
|
|
535
|
+
field.addLabel ?? "Add"
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
] });
|
|
540
|
+
}
|
|
541
|
+
function KeyValueField({
|
|
542
|
+
field,
|
|
543
|
+
value,
|
|
544
|
+
onChange
|
|
545
|
+
}) {
|
|
546
|
+
const map = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
547
|
+
const entries = Object.entries(map);
|
|
548
|
+
const commit = (next) => {
|
|
549
|
+
const obj = {};
|
|
550
|
+
for (const [k, v] of next) {
|
|
551
|
+
if (k === "") continue;
|
|
552
|
+
obj[k] = v;
|
|
553
|
+
}
|
|
554
|
+
onChange(obj);
|
|
555
|
+
};
|
|
556
|
+
const setKey = (i, key) => commit(entries.map(([k, v], idx) => idx === i ? [key, v] : [k, v]));
|
|
557
|
+
const setVal = (i, val) => commit(entries.map(([k, v], idx) => idx === i ? [k, val] : [k, v]));
|
|
558
|
+
const remove = (i) => commit(entries.filter((_, idx) => idx !== i));
|
|
559
|
+
const add = () => commit([...entries, ["", ""]]);
|
|
560
|
+
return /* @__PURE__ */ jsxs("div", { className: "ff-keyvalue", "data-ff-keyvalue": field.key, children: [
|
|
561
|
+
entries.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ff-keyvalue__head", children: [
|
|
562
|
+
/* @__PURE__ */ jsx("span", { children: field.keyLabel ?? "Key" }),
|
|
563
|
+
/* @__PURE__ */ jsx("span", { children: field.valueLabel ?? "Value" }),
|
|
564
|
+
/* @__PURE__ */ jsx("span", {})
|
|
565
|
+
] }),
|
|
566
|
+
entries.map(([k, v], i) => /* @__PURE__ */ jsxs("div", { className: "ff-keyvalue__row", "data-ff-keyvalue-row": i, children: [
|
|
567
|
+
/* @__PURE__ */ jsx(
|
|
568
|
+
"input",
|
|
569
|
+
{
|
|
570
|
+
className: "ff-panel__input",
|
|
571
|
+
value: k,
|
|
572
|
+
placeholder: field.keyPlaceholder,
|
|
573
|
+
"aria-label": `${field.keyLabel ?? "Key"} ${i + 1}`,
|
|
574
|
+
onChange: (e) => setKey(i, e.target.value)
|
|
575
|
+
}
|
|
576
|
+
),
|
|
577
|
+
field.valueOptions ? /* @__PURE__ */ jsxs(
|
|
578
|
+
"select",
|
|
579
|
+
{
|
|
580
|
+
className: "ff-panel__input",
|
|
581
|
+
value: v ?? "",
|
|
582
|
+
"aria-label": `${field.valueLabel ?? "Value"} ${i + 1}`,
|
|
583
|
+
onChange: (e) => setVal(i, e.target.value),
|
|
584
|
+
children: [
|
|
585
|
+
/* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u2014" }),
|
|
586
|
+
field.valueOptions.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
|
|
587
|
+
]
|
|
588
|
+
}
|
|
589
|
+
) : /* @__PURE__ */ jsx(
|
|
590
|
+
"input",
|
|
591
|
+
{
|
|
592
|
+
className: "ff-panel__input",
|
|
593
|
+
value: v ?? "",
|
|
594
|
+
placeholder: field.valuePlaceholder,
|
|
595
|
+
"aria-label": `${field.valueLabel ?? "Value"} ${i + 1}`,
|
|
596
|
+
onChange: (e) => setVal(i, e.target.value)
|
|
597
|
+
}
|
|
598
|
+
),
|
|
599
|
+
/* @__PURE__ */ jsx(
|
|
600
|
+
"button",
|
|
601
|
+
{
|
|
602
|
+
type: "button",
|
|
603
|
+
className: "ff-repeater__btn ff-repeater__btn--danger",
|
|
604
|
+
onClick: () => remove(i),
|
|
605
|
+
"aria-label": `Remove ${k || `entry ${i + 1}`}`,
|
|
606
|
+
children: "\u2715"
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
] }, i)),
|
|
610
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "ff-repeater__add", onClick: add, children: [
|
|
611
|
+
"+ ",
|
|
612
|
+
field.addLabel ?? "Add"
|
|
613
|
+
] })
|
|
614
|
+
] });
|
|
615
|
+
}
|
|
371
616
|
function JsonField({ value, onChange, rows }) {
|
|
372
617
|
const initial = useMemo(() => {
|
|
373
618
|
try {
|
|
@@ -402,6 +647,7 @@ function NodeConfigPanel({
|
|
|
402
647
|
onChange,
|
|
403
648
|
header,
|
|
404
649
|
renderCredentialField,
|
|
650
|
+
renderDocumentField,
|
|
405
651
|
className,
|
|
406
652
|
style
|
|
407
653
|
}) {
|
|
@@ -427,6 +673,8 @@ function NodeConfigPanel({
|
|
|
427
673
|
const setDescription = (description) => onChange({ ...node, data: { ...node.data, description } });
|
|
428
674
|
const setConfigValue = (key, value) => onChange({ ...node, data: { ...node.data, config: { ...config, [key]: value } } });
|
|
429
675
|
const issues = validateConfig(kind, config);
|
|
676
|
+
const adapter = getRichInputAdapter();
|
|
677
|
+
const documentField = renderDocumentField ?? (adapter?.renderEditor ? ({ value, onChange: set }) => adapter.renderEditor({ value, onChange: set }) : void 0);
|
|
430
678
|
return /* @__PURE__ */ jsxs("aside", { className: ["ff-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
|
|
431
679
|
header,
|
|
432
680
|
/* @__PURE__ */ jsxs("header", { className: "ff-panel__header", children: [
|
|
@@ -475,7 +723,8 @@ function NodeConfigPanel({
|
|
|
475
723
|
field,
|
|
476
724
|
value: config[field.key],
|
|
477
725
|
onChange: (v) => setConfigValue(field.key, v),
|
|
478
|
-
renderCredentialField
|
|
726
|
+
renderCredentialField,
|
|
727
|
+
renderDocumentField: documentField
|
|
479
728
|
}
|
|
480
729
|
)
|
|
481
730
|
] }, field.key))
|
|
@@ -523,6 +772,17 @@ function removeEdges(edges, ids) {
|
|
|
523
772
|
const doomed = new Set(ids);
|
|
524
773
|
return edges.filter((e) => !doomed.has(e.id));
|
|
525
774
|
}
|
|
775
|
+
function setEdgeLabel(edges, id, label) {
|
|
776
|
+
const next = label?.trim();
|
|
777
|
+
return edges.map((e) => {
|
|
778
|
+
if (e.id !== id) return e;
|
|
779
|
+
if (!next) {
|
|
780
|
+
const { label: _drop, ...rest } = e;
|
|
781
|
+
return rest;
|
|
782
|
+
}
|
|
783
|
+
return { ...e, label: next };
|
|
784
|
+
});
|
|
785
|
+
}
|
|
526
786
|
function duplicateNode(node, id, offset = 40) {
|
|
527
787
|
return {
|
|
528
788
|
...node,
|
|
@@ -570,6 +830,7 @@ function FlowEditorInner({
|
|
|
570
830
|
onChange,
|
|
571
831
|
onSelectionChange,
|
|
572
832
|
onDelete,
|
|
833
|
+
onEdgeDelete,
|
|
573
834
|
apiRef
|
|
574
835
|
}) {
|
|
575
836
|
const internal = useFlowState(initial);
|
|
@@ -587,18 +848,39 @@ function FlowEditorInner({
|
|
|
587
848
|
const [selectedId, setSelectedId] = useState(null);
|
|
588
849
|
const selected = useMemo(() => flow.nodes.find((n) => n.id === selectedId) ?? null, [flow.nodes, selectedId]);
|
|
589
850
|
useEffect(() => onSelectionChange?.(selected), [selected, onSelectionChange]);
|
|
590
|
-
const
|
|
851
|
+
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
|
852
|
+
const selectedEdge = useMemo(
|
|
853
|
+
() => flow.edges.find((e) => e.id === selectedEdgeId) ?? null,
|
|
854
|
+
[flow.edges, selectedEdgeId]
|
|
855
|
+
);
|
|
856
|
+
const handleNodeClick = (_e, node) => {
|
|
857
|
+
setSelectedId(node.id);
|
|
858
|
+
setSelectedEdgeId(null);
|
|
859
|
+
};
|
|
591
860
|
const [menu, setMenu] = useState(null);
|
|
861
|
+
const [labelEdit, setLabelEdit] = useState(null);
|
|
592
862
|
const closeMenu = useCallback(() => setMenu(null), []);
|
|
593
863
|
const handleNodeContextMenu = (event, node) => {
|
|
594
864
|
event.preventDefault();
|
|
595
865
|
setSelectedId(node.id);
|
|
596
|
-
|
|
866
|
+
setSelectedEdgeId(null);
|
|
867
|
+
setMenu({ x: event.clientX, y: event.clientY, target: { type: "node", id: node.id } });
|
|
868
|
+
};
|
|
869
|
+
const handleEdgeClick = (_e, edge) => {
|
|
870
|
+
setSelectedEdgeId(edge.id);
|
|
871
|
+
};
|
|
872
|
+
const handleEdgeContextMenu = (event, edge) => {
|
|
873
|
+
event.preventDefault();
|
|
874
|
+
setSelectedEdgeId(edge.id);
|
|
875
|
+
setMenu({ x: event.clientX, y: event.clientY, target: { type: "edge", id: edge.id } });
|
|
597
876
|
};
|
|
598
877
|
useEffect(() => {
|
|
599
|
-
if (menu === null) return;
|
|
600
|
-
const close = () =>
|
|
601
|
-
|
|
878
|
+
if (menu === null && labelEdit === null) return;
|
|
879
|
+
const close = () => {
|
|
880
|
+
setMenu(null);
|
|
881
|
+
setLabelEdit(null);
|
|
882
|
+
};
|
|
883
|
+
const onKey = (e) => e.key === "Escape" && close();
|
|
602
884
|
window.addEventListener("click", close);
|
|
603
885
|
window.addEventListener("scroll", close, true);
|
|
604
886
|
window.addEventListener("keydown", onKey);
|
|
@@ -607,7 +889,7 @@ function FlowEditorInner({
|
|
|
607
889
|
window.removeEventListener("scroll", close, true);
|
|
608
890
|
window.removeEventListener("keydown", onKey);
|
|
609
891
|
};
|
|
610
|
-
}, [menu]);
|
|
892
|
+
}, [menu, labelEdit]);
|
|
611
893
|
useEffect(() => {
|
|
612
894
|
if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
|
|
613
895
|
}, [flow.nodes, flow.edges, onChange, controlled]);
|
|
@@ -650,14 +932,29 @@ function FlowEditorInner({
|
|
|
650
932
|
edges: flow.edges,
|
|
651
933
|
selectedId,
|
|
652
934
|
selected,
|
|
935
|
+
selectedEdgeId,
|
|
936
|
+
selectedEdge,
|
|
653
937
|
running: runner.running,
|
|
654
938
|
statuses: runner.statuses,
|
|
655
939
|
select: setSelectedId,
|
|
940
|
+
selectEdge: setSelectedEdgeId,
|
|
656
941
|
addNode,
|
|
657
942
|
updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
|
|
658
943
|
deleteNodes,
|
|
659
944
|
deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
|
|
660
|
-
deleteEdges: (ids) =>
|
|
945
|
+
deleteEdges: (ids) => {
|
|
946
|
+
if (ids.length === 0) return;
|
|
947
|
+
flow.setEdges((all) => removeEdges(all, ids));
|
|
948
|
+
setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
|
|
949
|
+
onEdgeDelete?.(ids);
|
|
950
|
+
},
|
|
951
|
+
deleteSelectedEdge: () => {
|
|
952
|
+
if (!selectedEdgeId) return;
|
|
953
|
+
flow.setEdges((all) => removeEdges(all, [selectedEdgeId]));
|
|
954
|
+
setSelectedEdgeId(null);
|
|
955
|
+
onEdgeDelete?.([selectedEdgeId]);
|
|
956
|
+
},
|
|
957
|
+
setEdgeLabel: (id, label) => flow.setEdges((all) => setEdgeLabel(all, id, label)),
|
|
661
958
|
duplicateNode: (id) => {
|
|
662
959
|
const src = flow.nodes.find((n) => n.id === id);
|
|
663
960
|
if (!src) return null;
|
|
@@ -729,6 +1026,9 @@ function FlowEditorInner({
|
|
|
729
1026
|
onConnect: flow.onConnect,
|
|
730
1027
|
onNodeClick: handleNodeClick,
|
|
731
1028
|
onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
|
|
1029
|
+
onEdgeClick: handleEdgeClick,
|
|
1030
|
+
onEdgeContextMenu: builtins.edgeContextMenu === false ? void 0 : handleEdgeContextMenu,
|
|
1031
|
+
onEdgesDelete: (deleted) => onEdgeDelete?.(deleted.map((e) => e.id)),
|
|
732
1032
|
onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
|
|
733
1033
|
deleteKeyCode: ["Delete", "Backspace"],
|
|
734
1034
|
height: "100%",
|
|
@@ -737,14 +1037,14 @@ function FlowEditorInner({
|
|
|
737
1037
|
}
|
|
738
1038
|
),
|
|
739
1039
|
slots.empty && api.nodes.length === 0 && /* @__PURE__ */ jsx("div", { className: "ff-editor__empty", children: slots.empty(api) }),
|
|
740
|
-
menu
|
|
1040
|
+
menu?.target.type === "node" && builtins.contextMenu !== false && /* @__PURE__ */ jsx(
|
|
741
1041
|
"div",
|
|
742
1042
|
{
|
|
743
1043
|
className: "ff-editor__ctx",
|
|
744
1044
|
style: { top: menu.y, left: menu.x },
|
|
745
1045
|
role: "menu",
|
|
746
1046
|
onClick: (e) => e.stopPropagation(),
|
|
747
|
-
children: slots.contextMenu ? slots.contextMenu(api, menu.
|
|
1047
|
+
children: slots.contextMenu ? slots.contextMenu(api, menu.target.id, closeMenu) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
748
1048
|
/* @__PURE__ */ jsx(
|
|
749
1049
|
"button",
|
|
750
1050
|
{
|
|
@@ -753,7 +1053,7 @@ function FlowEditorInner({
|
|
|
753
1053
|
className: "ff-editor__ctx-item",
|
|
754
1054
|
"data-action": "ctx-duplicate",
|
|
755
1055
|
onClick: () => {
|
|
756
|
-
api.duplicateNode(menu.
|
|
1056
|
+
api.duplicateNode(menu.target.id);
|
|
757
1057
|
closeMenu();
|
|
758
1058
|
},
|
|
759
1059
|
children: "Duplicate"
|
|
@@ -767,7 +1067,7 @@ function FlowEditorInner({
|
|
|
767
1067
|
className: "ff-editor__ctx-item ff-editor__ctx-item--danger",
|
|
768
1068
|
"data-action": "ctx-delete",
|
|
769
1069
|
onClick: () => {
|
|
770
|
-
api.deleteNodes([menu.
|
|
1070
|
+
api.deleteNodes([menu.target.id]);
|
|
771
1071
|
closeMenu();
|
|
772
1072
|
},
|
|
773
1073
|
children: "Delete"
|
|
@@ -776,6 +1076,58 @@ function FlowEditorInner({
|
|
|
776
1076
|
] })
|
|
777
1077
|
}
|
|
778
1078
|
),
|
|
1079
|
+
menu?.target.type === "edge" && builtins.edgeContextMenu !== false && /* @__PURE__ */ jsx(
|
|
1080
|
+
"div",
|
|
1081
|
+
{
|
|
1082
|
+
className: "ff-editor__ctx",
|
|
1083
|
+
style: { top: menu.y, left: menu.x },
|
|
1084
|
+
role: "menu",
|
|
1085
|
+
onClick: (e) => e.stopPropagation(),
|
|
1086
|
+
children: slots.edgeContextMenu ? slots.edgeContextMenu(api, menu.target.id, closeMenu) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1087
|
+
/* @__PURE__ */ jsx(
|
|
1088
|
+
"button",
|
|
1089
|
+
{
|
|
1090
|
+
type: "button",
|
|
1091
|
+
role: "menuitem",
|
|
1092
|
+
className: "ff-editor__ctx-item",
|
|
1093
|
+
"data-action": "ctx-edge-label",
|
|
1094
|
+
onClick: () => {
|
|
1095
|
+
setLabelEdit({ x: menu.x, y: menu.y, edgeId: menu.target.id });
|
|
1096
|
+
setMenu(null);
|
|
1097
|
+
},
|
|
1098
|
+
children: "Label\u2026"
|
|
1099
|
+
}
|
|
1100
|
+
),
|
|
1101
|
+
/* @__PURE__ */ jsx(
|
|
1102
|
+
"button",
|
|
1103
|
+
{
|
|
1104
|
+
type: "button",
|
|
1105
|
+
role: "menuitem",
|
|
1106
|
+
className: "ff-editor__ctx-item ff-editor__ctx-item--danger",
|
|
1107
|
+
"data-action": "ctx-edge-delete",
|
|
1108
|
+
onClick: () => {
|
|
1109
|
+
api.deleteEdges([menu.target.id]);
|
|
1110
|
+
closeMenu();
|
|
1111
|
+
},
|
|
1112
|
+
children: "Delete connection"
|
|
1113
|
+
}
|
|
1114
|
+
)
|
|
1115
|
+
] })
|
|
1116
|
+
}
|
|
1117
|
+
),
|
|
1118
|
+
labelEdit !== null && /* @__PURE__ */ jsx(
|
|
1119
|
+
EdgeLabelEditor,
|
|
1120
|
+
{
|
|
1121
|
+
x: labelEdit.x,
|
|
1122
|
+
y: labelEdit.y,
|
|
1123
|
+
initial: flow.edges.find((e) => e.id === labelEdit.edgeId)?.label ?? "",
|
|
1124
|
+
onCommit: (text) => {
|
|
1125
|
+
api.setEdgeLabel(labelEdit.edgeId, text);
|
|
1126
|
+
setLabelEdit(null);
|
|
1127
|
+
},
|
|
1128
|
+
onCancel: () => setLabelEdit(null)
|
|
1129
|
+
}
|
|
1130
|
+
),
|
|
779
1131
|
showFeed && (slots.feed ? slots.feed(api) : /* @__PURE__ */ jsx(FlowRunFeed, { entries: runner.feed, className: "ff-editor__feed" }))
|
|
780
1132
|
] }),
|
|
781
1133
|
showPanel && (slots.panel ? slots.panel(api) : /* @__PURE__ */ jsxs("div", { className: "ff-editor__panel-wrap", children: [
|
|
@@ -852,6 +1204,46 @@ function makeControlledFlowAdapter(value, onChange) {
|
|
|
852
1204
|
toGraph: () => value
|
|
853
1205
|
};
|
|
854
1206
|
}
|
|
1207
|
+
function EdgeLabelEditor({
|
|
1208
|
+
x,
|
|
1209
|
+
y,
|
|
1210
|
+
initial,
|
|
1211
|
+
onCommit,
|
|
1212
|
+
onCancel
|
|
1213
|
+
}) {
|
|
1214
|
+
const [text, setText] = useState(initial);
|
|
1215
|
+
const ref = useRef(null);
|
|
1216
|
+
useEffect(() => {
|
|
1217
|
+
ref.current?.focus();
|
|
1218
|
+
ref.current?.select();
|
|
1219
|
+
}, []);
|
|
1220
|
+
return /* @__PURE__ */ jsx(
|
|
1221
|
+
"div",
|
|
1222
|
+
{
|
|
1223
|
+
className: "ff-editor__ctx ff-editor__edge-label",
|
|
1224
|
+
style: { top: y, left: x },
|
|
1225
|
+
onClick: (e) => e.stopPropagation(),
|
|
1226
|
+
children: /* @__PURE__ */ jsx(
|
|
1227
|
+
"input",
|
|
1228
|
+
{
|
|
1229
|
+
ref,
|
|
1230
|
+
className: "ff-panel__input",
|
|
1231
|
+
value: text,
|
|
1232
|
+
placeholder: "Label this connection",
|
|
1233
|
+
"aria-label": "Connection label",
|
|
1234
|
+
"data-action": "edge-label-input",
|
|
1235
|
+
onChange: (e) => setText(e.target.value),
|
|
1236
|
+
onKeyDown: (e) => {
|
|
1237
|
+
e.stopPropagation();
|
|
1238
|
+
if (e.key === "Enter") onCommit(text);
|
|
1239
|
+
if (e.key === "Escape") onCancel();
|
|
1240
|
+
},
|
|
1241
|
+
onBlur: () => onCommit(text)
|
|
1242
|
+
}
|
|
1243
|
+
)
|
|
1244
|
+
}
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
855
1247
|
function defineNode(render) {
|
|
856
1248
|
function Wrapped(props) {
|
|
857
1249
|
return render({
|