fix(editor): per-field ports render from YAML refs when ModuleSpec is missing
Stefan reported: per-field output ports never appeared even after the hub-side capability-lookup fix. Root cause: the editor only used ModuleSpec when the host returned one. If the operator's hub still ran the pre-fix binary, or hadn't re-installed v3 manifests, or the step's capability was a built-in (system.approval) that ships no manifest at all, the editor would silently collapse to a single output anchor even though the flow YAML clearly references named fields. Robust fix: per-field labels are now a UNION of declared fields (from the manifest, if any) and implicit ones (inferred from the flow graph itself — every $step.field reference an outgoing edge carries adds a field name). Declared fields keep manifest order; implicit ones follow alphabetically. Two helpers (_inputLabelsForStep, _outputLabelsForStep) single-source the merge so the renderer, port-overlay generator, edge router, and hit-tester all see the same list. What this means at run time: - v3 modules with description.en/de → full per-field ports with tooltips, exactly as designed. - v2 modules without descriptions → per-field ports, no tooltips. Was the case already. - v1 modules / hubs without inputs/outputs in ModuleInfoResponse → per-field ports inferred from the YAML references. New behaviour, no regression. - Built-in capabilities (system.approval) → per-field ports from with_-keys and outgoing edges. Works without any hub manifest. Bumps fai_studio_flow_editor to 0.10.1. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
parent
a71f654162
commit
7797654572
2 changed files with 70 additions and 38 deletions
|
|
@ -232,6 +232,42 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
return _moduleSpecs[cap];
|
||||
}
|
||||
|
||||
/// Merge ModuleSpec-declared input names with whatever the
|
||||
/// flow YAML's `with_:` block carries that the manifest
|
||||
/// didn't declare. Declared inputs keep their manifest
|
||||
/// order; implicit ones land alphabetically after. Same
|
||||
/// list used by [_stepPositioned] for rendering and by
|
||||
/// [_buildSegments] / [_hitTestEdge] for port positions.
|
||||
List<String> _inputLabelsForStep(FlowStep step) {
|
||||
final spec = _specForStep(step);
|
||||
final declared = spec?.inputs.map((f) => f.name).toList() ?? const [];
|
||||
final implicit =
|
||||
step.with_.keys.where((k) => !declared.contains(k)).toList()..sort();
|
||||
return [...declared, ...implicit];
|
||||
}
|
||||
|
||||
/// Merge ModuleSpec-declared output names with whatever
|
||||
/// the flow YAML's edges reference for this step. Declared
|
||||
/// outputs keep their manifest order; implicit (referenced
|
||||
/// but undeclared) outputs land alphabetically after. Lets
|
||||
/// the editor render per-field output anchors even when the
|
||||
/// hub hasn't shipped a manifest (built-ins like
|
||||
/// system.approval, or older hub binaries).
|
||||
List<String> _outputLabelsForStep(FlowStep step) {
|
||||
final spec = _specForStep(step);
|
||||
final declared = spec?.outputs.map((f) => f.name).toList() ?? const [];
|
||||
final implicit = <String>{};
|
||||
for (final edge in widget.controller.graph.edges) {
|
||||
if (edge.fromKind == EdgeEndpointKind.step &&
|
||||
edge.fromId == step.id &&
|
||||
edge.fromField.isNotEmpty &&
|
||||
!declared.contains(edge.fromField)) {
|
||||
implicit.add(edge.fromField);
|
||||
}
|
||||
}
|
||||
return [...declared, ...(implicit.toList()..sort())];
|
||||
}
|
||||
|
||||
void _onControllerChanged() {
|
||||
if (!mounted) return;
|
||||
// Start the flow-animation controller only while a step
|
||||
|
|
@ -669,17 +705,23 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
.whereType<Object>()
|
||||
.where((v) => _isWiredExpression(v.toString()))
|
||||
.length;
|
||||
// Per-field input/output ports: prefer the resolved
|
||||
// ModuleSpec from the hub when present, else fall back
|
||||
// to the legacy YAML-derived shape (with_ keys on the
|
||||
// left, single anchor on the right).
|
||||
// Per-field input/output ports: merge whatever the
|
||||
// ModuleSpec from the hub declares with whatever the
|
||||
// flow YAML actually references. The union ensures we
|
||||
// render ports even when the hub hasn't shipped a
|
||||
// manifest (built-in capabilities like system.approval),
|
||||
// when the hub binary is older than the per-field-port
|
||||
// RPC, or when the operator hasn't re-installed the
|
||||
// module to pick up its new schema_version 3 manifest.
|
||||
//
|
||||
// Declared fields keep their manifest order (alphabetical
|
||||
// server-side). Implicit fields (those that show up in
|
||||
// the YAML but aren't declared) are appended alphabetically
|
||||
// afterwards — so the layout stays stable when a new
|
||||
// manifest later lands and adds them officially.
|
||||
final spec = _specForStep(step);
|
||||
final inputLabels = spec != null
|
||||
? spec.inputs.map((f) => f.name).toList()
|
||||
: step.with_.keys.toList();
|
||||
final outputLabels = spec != null
|
||||
? spec.outputs.map((f) => f.name).toList()
|
||||
: const <String>[];
|
||||
final inputLabels = _inputLabelsForStep(step);
|
||||
final outputLabels = _outputLabelsForStep(step);
|
||||
final tooltips = <String, String>{};
|
||||
if (spec != null) {
|
||||
final loc = widget.locale == FlowEditorLocale.de ? 'de' : 'en';
|
||||
|
|
@ -806,11 +848,10 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
(s) => s.id == nodeId,
|
||||
orElse: () => const FlowStep(id: '__missing__', use: ''),
|
||||
);
|
||||
final spec = _specForStep(step);
|
||||
final outputs = spec?.outputs ?? const [];
|
||||
final labels = _outputLabelsForStep(step);
|
||||
double y;
|
||||
if (fieldName != null && outputs.isNotEmpty) {
|
||||
final idx = outputs.indexWhere((f) => f.name == fieldName);
|
||||
if (fieldName != null && labels.isNotEmpty) {
|
||||
final idx = labels.indexOf(fieldName);
|
||||
if (idx >= 0) {
|
||||
y = NodeGeometry.outputPortY(idx);
|
||||
} else {
|
||||
|
|
@ -876,15 +917,7 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
(s) => s.id == edge.toId,
|
||||
orElse: () => const FlowStep(id: '__missing__', use: ''),
|
||||
);
|
||||
// Prefer the resolved ModuleSpec's declared input
|
||||
// order when present (matches what the renderer
|
||||
// draws); fall back to the with_ keys for legacy
|
||||
// / unresolved modules.
|
||||
final spec = _specForStep(step);
|
||||
final inputLabels = spec != null
|
||||
? spec.inputs.map((f) => f.name).toList()
|
||||
: step.with_.keys.toList();
|
||||
final idx = inputLabels.indexOf(edge.toField);
|
||||
final idx = _inputLabelsForStep(step).indexOf(edge.toField);
|
||||
if (idx >= 0) {
|
||||
to = _inputPortPosition(edge.toId, idx, layout);
|
||||
toSide = EdgeSide.left;
|
||||
|
|
@ -1033,11 +1066,7 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
(s) => s.id == edge.toId,
|
||||
orElse: () => const FlowStep(id: '__missing__', use: ''),
|
||||
);
|
||||
final spec = _specForStep(step);
|
||||
final inputLabels = spec != null
|
||||
? spec.inputs.map((f) => f.name).toList()
|
||||
: step.with_.keys.toList();
|
||||
final idx = inputLabels.indexOf(edge.toField);
|
||||
final idx = _inputLabelsForStep(step).indexOf(edge.toField);
|
||||
if (idx >= 0) to = _inputPortPosition(edge.toId, idx, layout);
|
||||
} else if (edge.toKind == EdgeEndpointKind.outputs) {
|
||||
final outs = graph.outputs.keys.toList();
|
||||
|
|
@ -1381,12 +1410,15 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
);
|
||||
}
|
||||
// Step input port targets — left edges. Right-click on a
|
||||
// wired port opens the disconnect menu.
|
||||
// wired port opens the disconnect menu. We render every
|
||||
// label the merged-labels helper emits (declared by the
|
||||
// manifest + implicit from with_) so the dot positions
|
||||
// match what FlowNode.build draws on the card body.
|
||||
for (final step in graph.steps) {
|
||||
final keys = step.with_.keys.toList();
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
final labels = _inputLabelsForStep(step);
|
||||
for (var i = 0; i < labels.length; i++) {
|
||||
final p = _inputPortPosition(step.id, i, layout);
|
||||
final field = keys[i];
|
||||
final field = labels[i];
|
||||
final value = step.with_[field]?.toString() ?? '';
|
||||
final wired = _isWiredExpression(value);
|
||||
yield _portDot(
|
||||
|
|
@ -1678,8 +1710,8 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
double bestDist = double.infinity;
|
||||
Offset? best;
|
||||
for (final step in graph.steps) {
|
||||
final keys = step.with_.keys.toList();
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
final labels = _inputLabelsForStep(step);
|
||||
for (var i = 0; i < labels.length; i++) {
|
||||
final p = _inputPortPosition(step.id, i, layout);
|
||||
final d = (draft.cursor - p).distance;
|
||||
if (d < bestDist && d <= maxDist) {
|
||||
|
|
@ -1712,8 +1744,8 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
double bestDist = double.infinity;
|
||||
const maxDist = 32.0;
|
||||
for (final step in graph.steps) {
|
||||
final keys = step.with_.keys.toList();
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
final labels = _inputLabelsForStep(step);
|
||||
for (var i = 0; i < labels.length; i++) {
|
||||
final p = _inputPortPosition(step.id, i, layout);
|
||||
final d = (draft.cursor - p).distance;
|
||||
if (d < bestDist && d <= maxDist) {
|
||||
|
|
@ -1721,7 +1753,7 @@ class _FlowCanvasState extends State<FlowCanvas>
|
|||
best = _DropTarget(
|
||||
kind: _DraftTargetKind.step,
|
||||
id: step.id,
|
||||
field: keys[i],
|
||||
field: labels[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name: fai_studio_flow_editor
|
||||
description: Swappable inline YAML editor for F∆I Studio flows.
|
||||
version: 0.10.0
|
||||
version: 0.10.1
|
||||
publish_to: 'none'
|
||||
repository: https://git.flemming.ai/fai/studio-flow-editor
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue