@particle-academy/fancy-flow 0.42.0 → 0.43.1
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.cjs +84 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -1
- package/dist/index.d.ts +79 -1
- package/dist/index.js +75 -14
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.d.cts
CHANGED
|
@@ -661,4 +661,82 @@ type ExpressionFieldProps = {
|
|
|
661
661
|
*/
|
|
662
662
|
declare function ExpressionField({ value, onChange, placeholder, rows, handle, graph, nodeId, }: ExpressionFieldProps): react.JSX.Element;
|
|
663
663
|
|
|
664
|
-
|
|
664
|
+
/**
|
|
665
|
+
* `{{ }}` resolution — the TypeScript twin of `FancyFlow\Nodes\Support\Expr`.
|
|
666
|
+
*
|
|
667
|
+
* ## Why this is opt-in and not wired into `runFlow`
|
|
668
|
+
*
|
|
669
|
+
* The PHP runtime resolves expressions inside its batteries-included executors.
|
|
670
|
+
* The JS runtime never has: `runFlow` hands `node.data.config` to your executor
|
|
671
|
+
* verbatim, so every host that uses `{{ }}` today already resolves it itself.
|
|
672
|
+
* Turning resolution on inside `runFlow` would interpolate a second time over
|
|
673
|
+
* values those hosts had already substituted — silently, and only for graphs
|
|
674
|
+
* that happen to contain `{{` in their DATA.
|
|
675
|
+
*
|
|
676
|
+
* So this exports the semantics and changes no behaviour. Call it yourself:
|
|
677
|
+
*
|
|
678
|
+
* ```ts
|
|
679
|
+
* const url = evaluateExpression(node.data.config.url, inputs);
|
|
680
|
+
* ```
|
|
681
|
+
*
|
|
682
|
+
* ## The semantics are the PHP file's, deliberately
|
|
683
|
+
*
|
|
684
|
+
* This is not a general expression language and must not grow into one. It
|
|
685
|
+
* resolves a dot-path against a context and nothing else — no arithmetic, no
|
|
686
|
+
* comparisons, no calls. Hosts that want real expressions override the executor
|
|
687
|
+
* (the PHP docblock points at symfony/expression-language for the same reason).
|
|
688
|
+
*
|
|
689
|
+
* Divergence here is a correctness bug rather than a style difference: the same
|
|
690
|
+
* graph is authored once and may run on either runtime. `suites/shared/expr`
|
|
691
|
+
* in `@particle-academy/fancy-conformance` is the fixture table both sides run,
|
|
692
|
+
* so parity is a test result instead of a claim.
|
|
693
|
+
*/
|
|
694
|
+
/** Anything a context or a resolved value can be. */
|
|
695
|
+
type ExprValue = unknown;
|
|
696
|
+
/** The context an expression resolves against — the executor's `inputs`. */
|
|
697
|
+
type ExprContext = Record<string, ExprValue>;
|
|
698
|
+
/**
|
|
699
|
+
* Resolve a dot-path against the context, honouring the `$json` / `$input`
|
|
700
|
+
* alias.
|
|
701
|
+
*
|
|
702
|
+
* Both aliases point at the `in` port value when the context has one, and at
|
|
703
|
+
* the whole context otherwise — the same fallback the PHP does, which is what
|
|
704
|
+
* makes `{{ $json.x }}` work on a trigger node that has no upstream input.
|
|
705
|
+
*
|
|
706
|
+
* A path that does not resolve returns `null`, never `undefined`: PHP has one
|
|
707
|
+
* absent value and JS has two, and letting the difference leak would make the
|
|
708
|
+
* two runtimes disagree about `{{ missing }}` for no useful reason.
|
|
709
|
+
*/
|
|
710
|
+
declare function resolvePath(path: string, context: ExprContext): ExprValue;
|
|
711
|
+
/**
|
|
712
|
+
* Evaluate a template against a context.
|
|
713
|
+
*
|
|
714
|
+
* A string that is EXACTLY one expression returns the resolved value with its
|
|
715
|
+
* type intact — `{{ $json.count }}` gives you a number, not `"3"`. Anything
|
|
716
|
+
* else interpolates each run as text. That distinction is load-bearing: it is
|
|
717
|
+
* what lets a config field carry either a value or a sentence.
|
|
718
|
+
*
|
|
719
|
+
* Non-string templates pass through untouched, so this is safe to map over a
|
|
720
|
+
* whole config object.
|
|
721
|
+
*/
|
|
722
|
+
declare function evaluateExpression(template: ExprValue, context: ExprContext): ExprValue;
|
|
723
|
+
/**
|
|
724
|
+
* Truthiness for branch / switch decisions.
|
|
725
|
+
*
|
|
726
|
+
* Mirrors PHP's rules rather than JavaScript's, because the graph is authored
|
|
727
|
+
* once and may run on either side. The two disagree in exactly the places a
|
|
728
|
+
* workflow hits: `"0"` and `"false"` are truthy in JS and falsy here, and an
|
|
729
|
+
* empty array is truthy in JS and falsy here.
|
|
730
|
+
*/
|
|
731
|
+
declare function truthy(value: ExprValue): boolean;
|
|
732
|
+
/** Coerce a value to text the way interpolation does. */
|
|
733
|
+
declare function text(value: ExprValue): string;
|
|
734
|
+
/**
|
|
735
|
+
* Resolve every string in a config object, one level of nesting at a time.
|
|
736
|
+
*
|
|
737
|
+
* The convenience most hosts actually want, and the shape their hand-rolled
|
|
738
|
+
* version usually takes. Opt-in like the rest of this module.
|
|
739
|
+
*/
|
|
740
|
+
declare function evaluateConfig<T extends Record<string, ExprValue>>(config: T, context: ExprContext): T;
|
|
741
|
+
|
|
742
|
+
export { ActionNode, type AlignEdge, AutoLayoutOptions, ConfigFieldRenderFn, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, type ExprContext, type ExprValue, ExpressionField, type ExpressionFieldProps, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, LaneNode, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, evaluateConfig, evaluateExpression, flowKeys, flowLive, paletteDropHandlers, reconnectEdge, resolvePath, text, truthy, useFlowEditor, useFlowEditorOptional };
|
package/dist/index.d.ts
CHANGED
|
@@ -661,4 +661,82 @@ type ExpressionFieldProps = {
|
|
|
661
661
|
*/
|
|
662
662
|
declare function ExpressionField({ value, onChange, placeholder, rows, handle, graph, nodeId, }: ExpressionFieldProps): react.JSX.Element;
|
|
663
663
|
|
|
664
|
-
|
|
664
|
+
/**
|
|
665
|
+
* `{{ }}` resolution — the TypeScript twin of `FancyFlow\Nodes\Support\Expr`.
|
|
666
|
+
*
|
|
667
|
+
* ## Why this is opt-in and not wired into `runFlow`
|
|
668
|
+
*
|
|
669
|
+
* The PHP runtime resolves expressions inside its batteries-included executors.
|
|
670
|
+
* The JS runtime never has: `runFlow` hands `node.data.config` to your executor
|
|
671
|
+
* verbatim, so every host that uses `{{ }}` today already resolves it itself.
|
|
672
|
+
* Turning resolution on inside `runFlow` would interpolate a second time over
|
|
673
|
+
* values those hosts had already substituted — silently, and only for graphs
|
|
674
|
+
* that happen to contain `{{` in their DATA.
|
|
675
|
+
*
|
|
676
|
+
* So this exports the semantics and changes no behaviour. Call it yourself:
|
|
677
|
+
*
|
|
678
|
+
* ```ts
|
|
679
|
+
* const url = evaluateExpression(node.data.config.url, inputs);
|
|
680
|
+
* ```
|
|
681
|
+
*
|
|
682
|
+
* ## The semantics are the PHP file's, deliberately
|
|
683
|
+
*
|
|
684
|
+
* This is not a general expression language and must not grow into one. It
|
|
685
|
+
* resolves a dot-path against a context and nothing else — no arithmetic, no
|
|
686
|
+
* comparisons, no calls. Hosts that want real expressions override the executor
|
|
687
|
+
* (the PHP docblock points at symfony/expression-language for the same reason).
|
|
688
|
+
*
|
|
689
|
+
* Divergence here is a correctness bug rather than a style difference: the same
|
|
690
|
+
* graph is authored once and may run on either runtime. `suites/shared/expr`
|
|
691
|
+
* in `@particle-academy/fancy-conformance` is the fixture table both sides run,
|
|
692
|
+
* so parity is a test result instead of a claim.
|
|
693
|
+
*/
|
|
694
|
+
/** Anything a context or a resolved value can be. */
|
|
695
|
+
type ExprValue = unknown;
|
|
696
|
+
/** The context an expression resolves against — the executor's `inputs`. */
|
|
697
|
+
type ExprContext = Record<string, ExprValue>;
|
|
698
|
+
/**
|
|
699
|
+
* Resolve a dot-path against the context, honouring the `$json` / `$input`
|
|
700
|
+
* alias.
|
|
701
|
+
*
|
|
702
|
+
* Both aliases point at the `in` port value when the context has one, and at
|
|
703
|
+
* the whole context otherwise — the same fallback the PHP does, which is what
|
|
704
|
+
* makes `{{ $json.x }}` work on a trigger node that has no upstream input.
|
|
705
|
+
*
|
|
706
|
+
* A path that does not resolve returns `null`, never `undefined`: PHP has one
|
|
707
|
+
* absent value and JS has two, and letting the difference leak would make the
|
|
708
|
+
* two runtimes disagree about `{{ missing }}` for no useful reason.
|
|
709
|
+
*/
|
|
710
|
+
declare function resolvePath(path: string, context: ExprContext): ExprValue;
|
|
711
|
+
/**
|
|
712
|
+
* Evaluate a template against a context.
|
|
713
|
+
*
|
|
714
|
+
* A string that is EXACTLY one expression returns the resolved value with its
|
|
715
|
+
* type intact — `{{ $json.count }}` gives you a number, not `"3"`. Anything
|
|
716
|
+
* else interpolates each run as text. That distinction is load-bearing: it is
|
|
717
|
+
* what lets a config field carry either a value or a sentence.
|
|
718
|
+
*
|
|
719
|
+
* Non-string templates pass through untouched, so this is safe to map over a
|
|
720
|
+
* whole config object.
|
|
721
|
+
*/
|
|
722
|
+
declare function evaluateExpression(template: ExprValue, context: ExprContext): ExprValue;
|
|
723
|
+
/**
|
|
724
|
+
* Truthiness for branch / switch decisions.
|
|
725
|
+
*
|
|
726
|
+
* Mirrors PHP's rules rather than JavaScript's, because the graph is authored
|
|
727
|
+
* once and may run on either side. The two disagree in exactly the places a
|
|
728
|
+
* workflow hits: `"0"` and `"false"` are truthy in JS and falsy here, and an
|
|
729
|
+
* empty array is truthy in JS and falsy here.
|
|
730
|
+
*/
|
|
731
|
+
declare function truthy(value: ExprValue): boolean;
|
|
732
|
+
/** Coerce a value to text the way interpolation does. */
|
|
733
|
+
declare function text(value: ExprValue): string;
|
|
734
|
+
/**
|
|
735
|
+
* Resolve every string in a config object, one level of nesting at a time.
|
|
736
|
+
*
|
|
737
|
+
* The convenience most hosts actually want, and the shape their hand-rolled
|
|
738
|
+
* version usually takes. Opt-in like the rest of this module.
|
|
739
|
+
*/
|
|
740
|
+
declare function evaluateConfig<T extends Record<string, ExprValue>>(config: T, context: ExprContext): T;
|
|
741
|
+
|
|
742
|
+
export { ActionNode, type AlignEdge, AutoLayoutOptions, ConfigFieldRenderFn, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, type ExprContext, type ExprValue, ExpressionField, type ExpressionFieldProps, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, LaneNode, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, alignNodes, cloneSubgraph, defaultNodeTypes, defineNode, distributeNodes, evaluateConfig, evaluateExpression, flowKeys, flowLive, paletteDropHandlers, reconnectEdge, resolvePath, text, truthy, useFlowEditor, useFlowEditorOptional };
|
package/dist/index.js
CHANGED
|
@@ -173,8 +173,8 @@ function describeExpressionGrammar() {
|
|
|
173
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
174
|
};
|
|
175
175
|
}
|
|
176
|
-
function findTrigger(
|
|
177
|
-
const before =
|
|
176
|
+
function findTrigger(text2, caret) {
|
|
177
|
+
const before = text2.slice(0, caret);
|
|
178
178
|
const open = before.lastIndexOf("{{");
|
|
179
179
|
if (open === -1) return null;
|
|
180
180
|
if (before.indexOf("}}", open) !== -1) return null;
|
|
@@ -185,9 +185,9 @@ function filterVariables(vars, query) {
|
|
|
185
185
|
if (q === "") return vars;
|
|
186
186
|
return vars.filter((v) => v.path.toLowerCase().includes(q));
|
|
187
187
|
}
|
|
188
|
-
function applyCompletion(
|
|
189
|
-
const head =
|
|
190
|
-
const tail =
|
|
188
|
+
function applyCompletion(text2, caret, trigger, variable) {
|
|
189
|
+
const head = text2.slice(0, trigger.open);
|
|
190
|
+
const tail = text2.slice(caret);
|
|
191
191
|
return { value: `${head}${variable.expression}${tail}`, caret: head.length + variable.expression.length };
|
|
192
192
|
}
|
|
193
193
|
function ExpressionField({
|
|
@@ -711,7 +711,7 @@ function JsonField({
|
|
|
711
711
|
return "";
|
|
712
712
|
}
|
|
713
713
|
}, [value]);
|
|
714
|
-
const [
|
|
714
|
+
const [text2, setText] = useState(serialized);
|
|
715
715
|
const [error, setError] = useState(null);
|
|
716
716
|
const [touched, setTouched] = useState(false);
|
|
717
717
|
const lastProps = useRef(serialized);
|
|
@@ -749,7 +749,7 @@ function JsonField({
|
|
|
749
749
|
...handle,
|
|
750
750
|
className: "ff-panel__input ff-panel__input--json",
|
|
751
751
|
rows: rows ?? 6,
|
|
752
|
-
value:
|
|
752
|
+
value: text2,
|
|
753
753
|
spellCheck: false,
|
|
754
754
|
"aria-invalid": showError || void 0,
|
|
755
755
|
"aria-describedby": showError ? errorId : void 0,
|
|
@@ -1629,8 +1629,8 @@ function FlowEditorInner({
|
|
|
1629
1629
|
x: labelEdit.x,
|
|
1630
1630
|
y: labelEdit.y,
|
|
1631
1631
|
initial: flow.edges.find((e) => e.id === labelEdit.edgeId)?.label ?? "",
|
|
1632
|
-
onCommit: (
|
|
1633
|
-
api.setEdgeLabel(labelEdit.edgeId,
|
|
1632
|
+
onCommit: (text2) => {
|
|
1633
|
+
api.setEdgeLabel(labelEdit.edgeId, text2);
|
|
1634
1634
|
setLabelEdit(null);
|
|
1635
1635
|
},
|
|
1636
1636
|
onCancel: () => setLabelEdit(null)
|
|
@@ -1735,7 +1735,7 @@ function EdgeLabelEditor({
|
|
|
1735
1735
|
onCommit,
|
|
1736
1736
|
onCancel
|
|
1737
1737
|
}) {
|
|
1738
|
-
const [
|
|
1738
|
+
const [text2, setText] = useState(initial);
|
|
1739
1739
|
const ref = useRef(null);
|
|
1740
1740
|
useEffect(() => {
|
|
1741
1741
|
ref.current?.focus();
|
|
@@ -1752,17 +1752,17 @@ function EdgeLabelEditor({
|
|
|
1752
1752
|
{
|
|
1753
1753
|
ref,
|
|
1754
1754
|
className: "ff-panel__input",
|
|
1755
|
-
value:
|
|
1755
|
+
value: text2,
|
|
1756
1756
|
placeholder: "Label this connection",
|
|
1757
1757
|
"aria-label": "Connection label",
|
|
1758
1758
|
"data-action": "edge-label-input",
|
|
1759
1759
|
onChange: (e) => setText(e.target.value),
|
|
1760
1760
|
onKeyDown: (e) => {
|
|
1761
1761
|
e.stopPropagation();
|
|
1762
|
-
if (e.key === "Enter") onCommit(
|
|
1762
|
+
if (e.key === "Enter") onCommit(text2);
|
|
1763
1763
|
if (e.key === "Escape") onCancel();
|
|
1764
1764
|
},
|
|
1765
|
-
onBlur: () => onCommit(
|
|
1765
|
+
onBlur: () => onCommit(text2)
|
|
1766
1766
|
}
|
|
1767
1767
|
)
|
|
1768
1768
|
}
|
|
@@ -1823,9 +1823,70 @@ var flowKeys = {
|
|
|
1823
1823
|
run: (runId) => ["flow", "runs", runId]
|
|
1824
1824
|
};
|
|
1825
1825
|
|
|
1826
|
+
// src/expressions/expr.ts
|
|
1827
|
+
var WHOLE = /^\{\{([\s\S]*?)\}\}$/;
|
|
1828
|
+
var EACH = /\{\{([\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
|
+
|
|
1826
1887
|
// src/index.ts
|
|
1827
1888
|
registerBuiltinKinds();
|
|
1828
1889
|
|
|
1829
|
-
export { ConfigFieldRenderer, ExpressionField, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodePort, availableVariables, baseVariables, defineNode, describeExpressionGrammar, flowKeys, flowLive, outputFieldsFor, paletteDropHandlers };
|
|
1890
|
+
export { ConfigFieldRenderer, ExpressionField, FlowEditor, FlowRunControls, FlowRunFeed, NodeConfigPanel, NodePalette, NodePort, availableVariables, baseVariables, defineNode, describeExpressionGrammar, evaluateConfig, evaluateExpression, flowKeys, flowLive, outputFieldsFor, paletteDropHandlers, resolvePath, text, truthy };
|
|
1830
1891
|
//# sourceMappingURL=index.js.map
|
|
1831
1892
|
//# sourceMappingURL=index.js.map
|