feat(workflows): bind BPMN tasks to Elsa activities from a Performed by section - #1031
Merged
Merged
Conversation
…d element on selection The "Performed by" section (elsa-studio#1001) has to know two things about a clicked BPMN element that the canvas did not tell .NET: which element it is even when no activity is bound to it, and whether its activity is authored on it or derived by elsa-core's binder. - The canvas-neutral view model classifies each binding's kind: `unboundTask` when the activity comes from an elsa:activityBinding on the element, else `automatic` (timer/message/signal waits, message publish, call activity, nested process). Mirrors Bpmn.Interchange's reader and elsa-core's publish gate: every task kind is authored unless a send or receive task resolved a message (`bpmn.messageName`). The kind rides on the cell data and on the selection sent to .NET (`BpmnElementSelection.BindingKind`). - BpmnDesigner raises the selected element itself (ElementSelected), including a task nothing is bound to yet, which ActivitySelected cannot distinguish from a canvas click; the canvas raises null. BpmnDesignerWrapper forwards it from a named cascade an editor provides; the viewers provide none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ormat BPMN document
Binding a task means editing its elsa:activityBinding in place in the
bpmnDefinitions document the document endpoints exchange (D4, D5), without a
second encoder for activity inputs.
- BpmnActivityBindingFormat mirrors elsa-core's compatibility surface on the
document JSON: the elsa namespace and names, Find (first binding wins),
Create (one input per configured input, null ones skipped, ordered by name,
as elsa-core's Write does), Read (elsa-core's refusals: no activityType, an
unnamed or repeated input, input text that is not JSON) and Attach (replaces
the binding in place, drops any second one, touches nothing else).
- BpmnDefinitionsDocument finds an element across the document's processes,
and reports its subprocesses: the document carries no subprocess body, and
elsa-core writes the XML from the document alone, so a PUT would empty them.
- BpmnActivityBindingDraft maps between the activity JSON Studio's own input
editors write and the binding's inputs, node for node: an Input<T> keeps its
{typeName, expression} wrapper, a plain [Input] its own JSON. Names are
mapped through the descriptor (the server's camelCase and the editors'
Humanizer spelling differ for acronyms), and an input Studio's catalogue
does not describe is written back as it was read.
The document fixture is Bpmn.Interchange 0.2.0's own reader output for
camunda-order-process.bpmn, serialized as the document GET sends it; Studio
writes exactly the shape that reader produced for the same binding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extends the Studio-local interchange client and IBpmnInterchangeService with elsa-core's document endpoints. - The document body is read and written verbatim, with plain System.Text.Json defaults, never through the API-wide serializer conventions Bpmn.Model's reader does not accept. - The strong ETag is read verbatim and sent back as If-Match. A document that arrives without one is reported (MissingETag) instead of being editable and failing every save with 428: across origins that is a CORS policy that does not expose the header. - Refusals are classified by the envelope's code into BpmnDocumentFailureReason (precondition failed/required, binding invalid, capability unsupported, source stale, not imported, not found), falling back to the HTTP status and then to the server's own message, never to its wording. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nly through the document PUT Implements the BPMN binding authoring flow (elsa-studio#1001, W11): select a task, pick the Elsa activity that performs it, configure that activity's own inputs, and save the result into the task's elsa:activityBinding through the document write-back endpoint. The invariant: a BPMN-imported workflow's activity graph changes only through its BPMN document. An ordinary workflow-definition save that rewrote the graph would leave the stored document stale, after which elsa-core refuses to read or export it. So, for a definition whose root is an Elsa.BpmnProcess and that carries Bpmn:SourceXml: - The properties pane shows the Performed by section instead of the activity properties panel, whose edits only the ordinary save could persist. An authored task gets the existing activity picker (StateMachine's picker, given a leaf-work filter and its own context line) and the input editors the flowchart uses (InputsTab), bound to a draft that is never part of the graph; an automatically bound element shows what it is bound to, read-only. - BpmnDocumentSession holds the GET'd document and its ETag, applies edits to a working copy, and PUTs it with If-Match; a successful PUT reads the document back and the editor reloads the definition. A 412 is reported with a Reload action and never retried; a refused binding names the edited task alongside the server's message; a stale document explains that the workflow must be re-imported. A document declaring a subprocess is refused before anything is sent, since the document carries no subprocess body and the PUT would empty it. - The editor's ordinary save still carries the other properties, but refuses, loudly, any save whose graph differs from the root the server last delivered (the code view included), and never reads the designer for such a workflow. Save sends pending property changes first, then pending binding changes. Publishing with unsaved binding changes is refused; after an ordinary save the document is read again, so a save that left it stale is reported at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oubles BpmnDesigner.FindActivityById duplicated the Core JsonObjectActivityTreeExtensions.FindActivity extension with identical semantics, so the Designer now calls the shared extension instead. The BPMN editing test services also introduced a third near-identical ILocalizer/IActivityRegistry stub; StateMachineActivityPickerDialogTests and StateMachinePresentationTests now share one TestLocalizer/TestActivityRegistry pair from Support instead of keeping private copies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
…it against a lost edit The ordinary workflow save re-serializes the root and, after a publish, may open a new draft version, either of which can move the BPMN document's ETag without changing what it describes; the session now re-reads the document first and adopts the new ETag when the content is unchanged, rather than sending a stale If-Match and being refused for a change nobody made. A binding edit attempted while a save is in flight is now refused rather than silently lost to the reload that follows a successful PUT, and a PUT that succeeds but whose follow-up read fails is reported as saved-but-not-reloaded instead of a plain success. Also fixes two CodeQL findings: an empty `@if` branch in BpmnPerformedByPanel.razor, and inconsistent null handling around IsBpmnDocumentBacked in WorkflowEditor.razor.cs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ent is saving BpmnDocumentSession now raises a Changed event when IsSaving flips, so the panel can re-render itself during a save instead of relying on the editor's single before/after StateHasChanged, which never shows the disabled picker or "Saving…" state. As a backstop, SetBinding refuses quietly (returns false, leaves the working copy unchanged) instead of throwing when called while a save is in flight, since a late edit can otherwise still reach it despite the disabled UI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A second SaveAsync call while one was already in flight sent a second PUT with the same stale ETag, and the first call's finally block reset IsSaving while the second PUT/reload was still running, re-enabling edits that could be lost to that second reload. SaveAsync now returns the same in-flight task to a caller that calls it again, so at most one PUT is sent, and RefreshRevisionAfterOrdinarySaveAsync awaits an in-flight save before re-reading so it compares against the revision that save reloaded rather than racing the PUT. Also rewrite a bool? != false comparison as `is not false` in BpmnPerformedByPanel.razor to satisfy CodeQL while keeping null meaning browsable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 12, 2026
Closed
Merged
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.
Closes #1001. Part of elsa-workflows/elsa-core#7909 (W11, D5). Builds on elsa-workflows/elsa-core#8060 (document GET/PUT), #8063 (PUT preserves metadata), #8065 (content-based staleness), #8067 (refusal codes), and Studio #1029.
What changed
A BPMN task can now be bound to the Elsa activity that performs it without hand-editing XML.
task,serviceTask,userTask,scriptTask,manualTask,businessRuleTask,sendTask, and areceiveTaskthat names no message, following the library reader's own rule) shows a Performed-by section in place of the activity properties panel. The existing activity picker is filtered to leaf work, and the picked activity's own input editors render inline, exactly as in the flowchart panel. Elements the binder binds automatically (timers, message and signal waits, message publish, call activities, subprocesses) show their binding read-only.bpmnDefinitionsdocument fetched withGET bpmn/definitions/{id}/document. It sets only that element'selsa:activityBinding, in the exact format elsa-core'sBpmnActivityBindingFormatreads. Input JSON is what the flowchart's input editors already produce; there is no second encoder. A test compares the document before and after: only the target element's extension changes.If-Matchset to the ETag it was read at, then reloads the definition. For a BPMN-imported workflow,WorkflowEditornever reads the designer into an ordinary save. It refuses any ordinary save whose root differs from the one the server last delivered, so a graph change cannot slip through the ordinary save and make the document stale. Property-only saves (name, variables) still go through the ordinary path. Publishing with unsaved binding changes is refused.412keeps the working copy, explains that someone else changed the workflow, and offers Reload with no blind retry. A428shows a message and Reload. A422 bpmn.import.binding-invalidshows the server's message against the edited tasks. A GET422for a stale source explains that the workflow must be re-imported and offers no editing.IBpmnInterchangeApi/IBpmnInterchangeServicegain the document GET and PUT withETag/If-Match.Known limitations
Verification
Tests cover:
Input<string>expression and a plain[Input]collection round-trip through the binding format.Offline, the real Bpmn.Interchange 0.2.0 reader and writer show a Studio-written binding reads back intact with the Camunda extensions untouched. The live end-to-end run against the sample server follows after merge.
Review: two iterations on the standards and spec axes. Resolved: a duplicate activity-tree lookup and a third copy of test stubs, now one shared set. Won't-fix:
receiveTaskis included, per the library's rule.🤖 Generated with Claude Code