Repository navigation
feat(automation): add a config connector that names reusable variables - #535
Merged
Merged
Conversation
Congrats! CodSpeed is installed 🎉
You will start to see performance impacts in the reports once the benchmarks are run from your default branch.
|
LeadcodeDev
force-pushed
the
feature/automation-run-in-editor
branch
from
September 17, 2026 10:54
04a6b97 to
c07f533
Compare
A workflow had no way to declare a value once and reuse it. The same
customer id, base URL or retry limit had to be written into every connector
that needed it, and changing it meant hunting through the graph.
`flow.config` takes a JSON object of named variables and produces it as its
output. That is the whole implementation, and it is enough: the engine
already resolves `{{ }}` expressions inside a connector's config before
executing it, and a connector's output is already readable downstream as
`{{ connectors.<id>.output.<path> }}`. So a variable may itself be an
expression — `{{ trigger.payload.customer_id }}` is resolved before this
connector ever sees it — and every connector after it in the graph can read
the result with machinery that already existed.
Nothing changes in the frontend, and nothing should: the editor renders
whatever the catalogue describes, and no connector kind is named anywhere in
its code.
`variables` resolving to anything but an object fails with a message naming
what arrived instead. Someone who writes a list of variables rather than a
map gets told, instead of being handed an output nothing downstream can
address.
The doc comment on `flow_descriptors` said "the two flow-control connectors"
and there are now three; the count is dropped rather than bumped, since it
was only ever true by accident.
Removing a wire meant deleting one of the nodes it joined, or knowing that React Flow's keyboard delete applies to a selected edge. Neither is discoverable, so in practice a mistaken connection stayed. Clicking a connection now selects it and puts a trash button at its midpoint. The button routes through the same `onEdgesChange` path a keyboard delete already took, so the graph is rebuilt and the editor marked dirty by the code that handled edge removal all along — no second way to remove an edge, and no second place for that logic to drift. Every edge is deletable, including the one leaving a trigger. That used to be forbidden because the canvas invented those edges and no user could have drawn them; now that a trigger is a node you wire by hand, being unable to unwire it would be the odd rule. A trigger left pointing at nothing is refused at save, by name, which is where that check belongs.
Selecting a connection to reveal its trash was a click the gesture did not need: you already point at the wire you mean. The button now appears on hover, over a transparent path widened to twenty pixels so the wire is catchable without precision aiming. `BaseEdge` draws an interaction path of its own, so the first version had two stacked hit areas and events landed on the wrong one. Its `interactionWidth` is now zero and this component owns the single layer that carries the handlers. Hover alone would have put the button out of reach of a keyboard, which can focus an edge but cannot point at one, so selection still reveals it too. That is the reason the interaction path suppresses the static-element lint rather than answering it: the path is a hit area, the control is the labelled button, and the button stays reachable without a mouse.
…xample
The editor listed a config node under "Données disponibles" and then offered
`customer_id` and `retry_limit` beneath it — the descriptor's illustrative
output_example. Those are names the author never wrote and no run will ever
produce, so every path taken from that list resolved to nothing. Worse than
an empty branch: it looked like an answer.
For a config node the available data is known at edit time, because it is
exactly what the author typed. Nothing else in the catalogue is like that —
an HTTP response exists only once the call has been made.
`ConnectorDescriptor` now says so: `output_mirrors_field` names the config
field a connector's output is a copy of, `Some("variables")` for
`flow.config` and `None` everywhere else. The editor reads that declaration
and shows the connector's own configured value.
Declaring it rather than special-casing the kind is what keeps the standing
invariant intact: no connector kind is named anywhere in the frontend, and
the next connector whose output mirrors its input gets this behaviour by
filling in a field rather than by touching the editor.
A connector always rendered as an expandable branch, so one exposing nothing gave a chevron that opened onto nothing. The trigger already had the opposite treatment — a sentence explaining it is not configured — and a connector deserved the same. A connector with no data to offer is now a notice reading "Ce connecteur n'expose aucune donnée pour l'instant." A config node with no variables typed yet is exactly that case, and it was the one that made the gap visible: the node appeared in the list, opened, and said nothing. A notice rendered its message alone, which was legible while the trigger was the only one that could produce it. With several connectors upstream it would have printed the same sentence several times over with nothing to tell them apart, so a notice now names what it is about. The upstream/sibling test moved from querying a button to querying text: a connector exposing nothing is no longer a button at all, and what that test pins is which connectors appear, not the element they render as.
Dropping the example/last-run switch replaced a choice the author made with a choice the editor made for them, and it made it wrongly: the whole tree came from the last run as soon as one existed. A connector that run never reached has no recorded output, so it collapsed to nothing — and a config node added after the last run showed "aucune donnée" while its variables sat right there in its panel. The two sources are not alternatives, they are per connector. Each one now uses the last run's recorded output when it has one, and its own data otherwise. Nothing is lost by adding a node to a workflow that has already run, which is the normal way a workflow is built. `ConnectorConfigPanel` took `exampleData` and `lastRunData` and picked between them. That choice has moved to the canvas, where the information needed to make it per connector actually lives, so the panel now takes one `availableData` and renders it. The two tests covering its choosing are deleted: the behaviour they described is now the canvas's, and the canvas test added here pins it against the case that was broken — a past run that never reached the connector being read.
The panel's "Dernière sortie" section was a hard-coded "Aucune exécution." It said that whatever had happened, so a connector could carry a red border from a failed step while its own panel claimed nothing had ever run. The error was recorded on the step and reachable through the API; the editor simply never asked for it. The section now reports the step the node's border is painted from: the error when it failed, the output when it succeeded, its status when it has neither yet, and "Aucune exécution." only when there is genuinely no step. `representativeStepByConnector` picks that step with the same priority `aggregateConnectorStatuses` uses to pick the status, so the colour and the message can never come from different iterations of the same connector — which is how a red border and a success payload could have been shown side by side. Renamed to "Dernière exécution": the section no longer only shows output.
Every `{{ connectors.X.output.… }}` failed in a live run with `missing path`,
however well the upstream connector had succeeded. The engine recorded the
output correctly; nothing downstream could read it.
`ExpressionContext::connectors` maps a connector id to `{"output": …}` — the
evaluator looks the id up and then walks the *remaining* segments, which
still include the literal `output`. Its own tests wrap the value, and so does
the editor's `toEvaluateContext`. The engine did not: both sites filling
`connector_outputs` inserted the raw produced value, so the walk went looking
for an `output` key inside the output itself and found none.
The doc comment on that field said "Connector id -> its raw output" and then
"`connectors.c1.output` reads `connectors["c1"]["output"]`" in the same
breath. Those two sentences cannot both be true, and the engine implemented
the first. It now states the wrapped shape and says callers must wrap.
Two tests, one per insertion site, because they are reached in different
circumstances: one where both connectors run in a single slice, and one
forced across two slices so the upstream output is read back from its
persisted row on resumption. Each was verified to fail when its own site is
unwrapped and to pass when the other is — a single test would have covered
half the fix and looked complete.
Neither test reaches the network. They assert the resolved URL recorded on
the step, which is written only when resolution succeeded, so they prove the
expression resolved without depending on a third-party host being up.
The editor's side panel had `overflow-y-auto` and still ran off the bottom of the window: nothing above it had a definite height, so the browser had nothing to clip against. `SidebarProvider` was `min-h-svh`, a floor rather than a ceiling, and neither `SidebarInset` nor the wrapper around `Outlet` carried `min-h-0`, so no flex child could shrink below its content. The shell is now `h-svh overflow-hidden` and the area around `Outlet` scrolls instead of the document. Ordinary pages keep scrolling — `PageShell` has no scroll of its own and relied on the window, so the scrollbar moves into the content area rather than disappearing. The editor, which asks for `flex-1`, now receives a real height and keeps its scrolling inside its own panels, which is what it wanted all along. `ScopeBar` gained `shrink-0`. It had none, and a bounded height is exactly the condition under which a flex sibling starts compressing a bar that should never compress. This is a change to the shell every page renders in, not to the editor. It was made there because that is where the missing constraint was; putting a height on the editor alone would have hard-coded the sum of the header and the scope bar and broken the next time either changed.
LeadcodeDev
force-pushed
the
feature/automation-config-node
branch
from
September 17, 2026 11:11
dba248e to
ca42ada
Compare
Contributor
|
🚀 Preview deployed: https://pr-535.mestier.fr Synced revision |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #533 — review that one first; this diff shrinks to its own commit once #533 merges.
What this adds
A workflow had no way to declare a value once and reuse it. The same customer id, base URL or retry limit had to be written into every connector that needed it, and changing it meant hunting through the graph.
flow.configtakes a JSON object of named variables and produces it as its output.Why that is the whole implementation
Two pieces already existed, and this connector is the hinge between them:
{{ }}expressions inside a connector's config before executing it, so a variable may itself be an expression —{{ trigger.payload.customer_id }}is already resolved when this connector runs;{{ connectors.<id>.output.<path> }}.So the executor is a handful of lines, and every connector after it in the graph reads the result through machinery that was already there. No change to the expression engine, the run engine, or the frontend.
Nothing changes in the webapp, and nothing should. The editor renders whatever the catalogue describes, and "no connector kind appears anywhere in the frontend" is a standing invariant here. Adding this node to the editor's palette required zero frontend code.
Error behaviour worth a look
variablesresolving to anything but an object fails, with a message naming what arrived instead ("variablesmust resolve to a JSON object, got an array"). Someone who writes a list of variables rather than a map gets told, instead of being handed an output that nothing downstream can address.A comment corrected, not bumped
The doc comment on
flow_descriptorssaid "the two flow-control connectors" and there are now three. The count is dropped rather than incremented — it was only ever true by accident, and a number in prose beside a list is a fact that rots on the next addition.Verification
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test -p mestier-core --libThe tests were verified non-hollow: making the executor return
Value::Nullinstead of the variables object fails both the unit test and the registry dispatch test. The catalogue/registry completeness test (every_catalogue_kind_has_an_implementation_and_vice_versa) also failed at the right moment during development — after the descriptor was added but before the registry was wired — which is what proves the registration is real rather than assumed.Closes #534
Also here: deleting a connection (second commit)
Removing a wire meant deleting one of the nodes it joined, or knowing that React Flow's keyboard delete applies to a selected edge. Neither is discoverable, so a mistaken connection stayed.
Hovering a connection now puts a trash button at its midpoint, over a transparent path widened to twenty pixels so the wire is catchable without precision aiming. Selection reveals it too — see the accessibility note below. It routes through the same
onEdgesChangepath the keyboard delete already took, so the graph is rebuilt and the editor marked dirty by the code that handled edge removal all along — no second way to remove an edge, and no second place for that logic to drift.Every edge is deletable now, including the one leaving a trigger. That was forbidden while the canvas invented those edges and no user could have drawn them; now that a trigger is a node you wire by hand, being unable to unwire it would be the odd rule. A trigger left pointing at nothing is refused at save, by name — which is where that check belongs.
Verified non-hollow: forcing the trash button to never render makes the deletion test fail, while its companion ("no delete affordance until the connection is selected") passes either way by construction and is kept as a guard, not as evidence.
Re-verified after both commits: 441 automation tests pass,
pnpm checkclean on 545 files,tscatmain's 28-error baseline, build clean.Two things the hover version had to get right
BaseEdgedraws an interaction path of its own, so the first attempt stacked two hit areas and events landed on the wrong one. ItsinteractionWidthis now zero and this component owns the single layer carrying the handlers.Hover alone would have put the button out of reach of a keyboard, which can focus an edge but cannot point at one. Selection still reveals it, so the affordance is not mouse-only. That is also why the interaction path suppresses
noStaticElementInteractionsrather than answering it: the path is a hit area, the control is the labelled button, and the button stays reachable without a mouse. The suppression carries that reason inline.A config node's variables now reach the editor (third commit)
Found by using it: the editor listed the config node under "Données disponibles" and then offered
customer_idandretry_limitbeneath it — the descriptor's illustrativeoutput_example. Those are names the author never wrote and no run will ever produce, so every path taken from that list resolved to nothing. Worse than an empty branch, because it looked like an answer.For a config node the available data is known at edit time: it is exactly what the author typed. Nothing else in the catalogue is like that — an HTTP response exists only once the call has been made.
ConnectorDescriptornow says so.output_mirrors_fieldnames the config field a connector's output is a copy of —Some("variables")forflow.config,Noneeverywhere else — and the editor reads that declaration to show the connector's own configured value.Declaring it rather than special-casing the kind is the point. "No connector kind appears anywhere in the frontend" is a standing invariant here, and a
if (kind === 'flow.config')in the canvas would have broken it for one node's benefit. As it stands, the next connector whose output mirrors its input gets this behaviour by filling in a field, without the editor changing at all.Verified non-hollow: removing the mirror lookup makes the new test fail on the configured name it can no longer find.
Re-verified across the branch:
cargo clippy0 warnings, 1534 Rust tests pass, 443 automation frontend tests pass,tscatmain's 28-error baseline,pnpm checkclean on 545 files, build clean.An empty branch now says why (fourth commit)
A connector always rendered as an expandable branch, so one exposing nothing gave a chevron that opened onto nothing. The trigger already had the opposite treatment — a sentence explaining it is not configured — and a connector deserved the same.
A connector with no data to offer is now a notice: "Ce connecteur n'expose aucune donnée pour l'instant." A config node with no variables typed yet is exactly that case, and it is the one that made the gap visible.
While doing it: a notice rendered its message alone, which was legible while the trigger was the only thing that could produce one. With several connectors upstream it would have printed the same sentence repeatedly with nothing to tell them apart, so a notice now names what it is about.
The upstream/sibling test moved from querying a button to querying text — a connector exposing nothing is no longer a button at all, and what that test pins is which connectors appear, not the element they render as.
445 automation tests pass;
tscat baseline;pnpm checkand build clean.