Repository navigation
Add BPMN findings, refusal dialogs and import/export #8949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
9871a95
Add loss-aware BPMN import and export to React Studio
sfmskywalker d076b88
Merge branch 'codex/8859-react-studio-release' into codex/8859-react-…
sfmskywalker 0e08fec
Preserve native BPMN roots and verify pending request cancellation
sfmskywalker 4f8880d
Merge branch 'codex/8859-react-studio-release' into codex/8859-react-…
sfmskywalker 3452892
Merge branch 'codex/8859-react-studio-release' into codex/8859-react-…
sfmskywalker 8d3ed6e
Merge branch 'codex/8859-react-studio-release' into codex/8859-react-…
sfmskywalker fb288ca
Merge branch 'codex/8859-react-studio-release' into codex/8859-react-…
sfmskywalker 076767c
Merge branch 'codex/8859-react-studio-release' into codex/8859-react-…
sfmskywalker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # BPMN import and export | ||
|
|
||
| From Workflow definitions, choose **Import BPMN**, select a `.bpmn` file up to | ||
| 10 MB, and review the server's Info, Degraded and Dropped findings. Element and | ||
| process IDs identify affected parts. Dropped elements will not run; Degraded | ||
| elements behave differently from their authored definition. A document with more | ||
| than one process requires an explicit process selection before **Import** enables. | ||
| Canceling the dialog persists nothing. Confirming imports the same selected file | ||
| and opens a read-only view of the new draft. This view preserves the native | ||
| BPMN root and never normalizes or autosaves it. Export remains available; canvas | ||
| editing is separate work in #8929. | ||
|
|
||
| The API remains the source of truth for supported BPMN capabilities. Capability | ||
| refusals list missing capabilities and offending elements. Unsupported expression | ||
| conditions list sequence-flow IDs and explain why unconditional execution would | ||
| change routing. These messages use stable server error codes and retain the | ||
| server's explanation. Unknown codes and older-server errors show the ordinary | ||
| server error rather than guessing their meaning. | ||
|
|
||
| Use a definition row's **Actions → Export BPMN** to download the latest imported | ||
| document. The confirmation warns that bindings and expressions are included. | ||
| Export is refused when BPMN source is absent, its recorded version is unknown, | ||
| or it has become stale after workflow edits. The browser downloads the server's | ||
| XML bytes; the server may normalize XML and add missing diagram layout. Reimport | ||
| preserves the process/flow model, and a subsequent export is stable. It does not | ||
| promise byte equality with an original document that omitted layout. | ||
|
|
||
| Import requires `workflows/definitions:write`, export/analyze require `:view`, | ||
| and the backend must have BPMN interchange enabled. Existing read-only server | ||
| configuration hides the import action. Requests share the configured API origin, | ||
| authentication and tenant headers; closing a dialog cancels its outstanding request. | ||
|
|
||
| The public API client exposes `client.bpmn.analyze(file, signal)`, | ||
| `client.bpmn.import(file, options, signal)` and | ||
| `client.bpmn.export(definitionId, signal)`, with typed analysis/results and optional | ||
| process/name/definition IDs for API consumers. The UI imports new drafts, so it | ||
| does not overwrite an existing definition. | ||
|
|
||
| Browser tests cover findings, process choice, cancellation of pending analysis and | ||
| export requests, native-root preservation without autosave, capability/condition | ||
| refusals, stale/missing-source refusals, unknown error fallback and download/reimport. | ||
| To check a disposable real backend, set `ELSA_TEST_API_URL`, `ELSA_TEST_USERNAME` | ||
| and `ELSA_TEST_PASSWORD`, then run `pnpm test:bpmn`. This checks analysis, | ||
| import/export/reimport, stable canonical XML, structured expression-condition | ||
| refusal and cleanup of its temporary definitions. | ||
|
|
||
| This implements interchange for [#8853](https://github.com/elsa-workflows/elsa-core/issues/8853). | ||
| An editable BPMN diagram canvas is separate work in #8929. |
21 changes: 21 additions & 0 deletions
21
...t/modules/Elsa.ReactStudio.WorkflowDefinitions/Client/components/bpmn-definition-view.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { useState } from "react"; | ||
| import Link from "@elsa-workflows/react-studio-sdk/navigation/link"; | ||
| import type { WorkflowDefinition } from "@elsa-workflows/react-studio-api-client/types/workflow-definitions"; | ||
| import { Button } from "@elsa-workflows/react-studio-ui/button"; | ||
| import { PageHeader } from "@elsa-workflows/react-studio-ui/common/page-header"; | ||
| import { JsonViewer } from "@elsa-workflows/react-studio-ui/common/json-viewer"; | ||
| import { BpmnExportDialog } from "./bpmn-dialogs"; | ||
|
|
||
| /** The generic designer normalizes unknown roots. Keep BPMN outside that workspace | ||
| * until the dedicated canvas in #8929 can edit its native process model. */ | ||
| export function BpmnDefinitionView({ definition }: { definition: WorkflowDefinition }) { | ||
| const [exporting, setExporting] = useState(false); | ||
| return <div className="p-4"> | ||
| <PageHeader title={definition.name ?? definition.definitionId} eyebrow="Read-only BPMN definition" | ||
| description="This Preview preserves the BPMN process without editing or autosaving it. BPMN canvas editing is not available yet." | ||
| actions={<><Link href="/workflows/definitions">Workflow definitions</Link>{definition.isLatest && <Button onClick={() => setExporting(true)}>Export BPMN</Button>}</>} /> | ||
| <p>Version {definition.version}</p> | ||
| <JsonViewer value={definition} /> | ||
| {exporting && <BpmnExportDialog definitionId={definition.definitionId} onClose={() => setExporting(false)} />} | ||
| </div>; | ||
| } |
95 changes: 95 additions & 0 deletions
95
...ct/dotnet/modules/Elsa.ReactStudio.WorkflowDefinitions/Client/components/bpmn-dialogs.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { useElsaClient } from "@elsa-workflows/react-studio-api-client/react"; | ||
| import { ApiError } from "@elsa-workflows/react-studio-api-client/http"; | ||
| import type { BpmnImportAnalysis, BpmnImportResult } from "@elsa-workflows/react-studio-api-client/types/bpmn"; | ||
| import { Button } from "@elsa-workflows/react-studio-ui/button"; | ||
| import { Input } from "@elsa-workflows/react-studio-ui/input"; | ||
| import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@elsa-workflows/react-studio-ui/dialog"; | ||
| import { ErrorAlert } from "@elsa-workflows/react-studio-ui/common/error-alert"; | ||
| import { downloadBlob } from "@elsa-workflows/react-studio-sdk/utils/download"; | ||
|
|
||
| /** Stable server codes select explanations; unknown envelopes retain the server message. */ | ||
| function Refusal({ error }: { error: unknown }) { | ||
| const raw = error instanceof ApiError ? error.problem.raw : undefined; | ||
| const envelope = raw && typeof raw === "object" ? raw as Record<string, unknown> : {}; | ||
| const data = envelope.data && typeof envelope.data === "object" ? envelope.data as Record<string, unknown> : {}; | ||
| const strings = (value: unknown) => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; | ||
| const capabilities = strings(data.capabilities); | ||
| const elements = strings(data.elementIds); | ||
| const messages: Record<string, string> = { | ||
| "bpmn.import.capability-unsupported": "This deployment does not support every BPMN capability the document requires. Import was refused.", | ||
| "bpmn.import.condition-unsupported": "These sequence flows carry expression conditions that Elsa does not evaluate. Import was refused to prevent unconditional routing. Remove the conditions or use conditionOutcome.", | ||
| "bpmn.export.not-imported": "This workflow has no recorded BPMN source to export.", | ||
| "bpmn.export.source-version-unknown": "The recorded BPMN source version is unknown, so a current export cannot be verified.", | ||
| "bpmn.export.source-stale": "The workflow changed after import. Export was refused because the recorded BPMN source is stale.", | ||
| }; | ||
| const message = typeof envelope.code === "string" ? messages[envelope.code] : undefined; | ||
| return <> | ||
| {message && <p role="alert">{message}</p>} | ||
| {!!capabilities.length && <div><h3>Missing capabilities</h3><ul>{capabilities.map(item => <li key={item}>{item}</li>)}</ul></div>} | ||
| {!!elements.length && <div><h3>{envelope.code === "bpmn.import.condition-unsupported" ? "Conditional sequence flows" : "Offending elements"}</h3><ul>{elements.map(item => <li key={item}>{item}</li>)}</ul></div>} | ||
| <ErrorAlert error={error} title="BPMN request failed" /> | ||
| </>; | ||
| } | ||
|
|
||
| function useRequest() { | ||
| const request = useRef<AbortController | undefined>(undefined); | ||
| useEffect(() => () => request.current?.abort(), []); | ||
| return () => { request.current?.abort(); return (request.current = new AbortController()).signal; }; | ||
| } | ||
|
|
||
| /** Mounted only while open: closing cancels work and discards the selected document. */ | ||
| export function BpmnImportDialog({ onClose, onImported }: { onClose: () => void; onImported: (result: BpmnImportResult) => void }) { | ||
| const client = useElsaClient(); | ||
| const begin = useRequest(); | ||
| const [file, setFile] = useState<File>(); | ||
| const [analysis, setAnalysis] = useState<BpmnImportAnalysis>(); | ||
| const [processId, setProcessId] = useState(""); | ||
| const [busy, setBusy] = useState(false); | ||
| const [error, setError] = useState<unknown>(); | ||
| const analyze = async (document: File | undefined) => { | ||
| setAnalysis(undefined); setError(undefined); setFile(undefined); setProcessId(""); | ||
| if (!document) return; | ||
| if (!document.name.toLowerCase().endsWith(".bpmn") || document.size > 10 * 1024 * 1024) { setError(new Error("Select a .bpmn document no larger than 10 MB.")); return; } | ||
| setBusy(true); | ||
| try { | ||
| const result = await client.bpmn.analyze(document, begin()); | ||
| setFile(document); setAnalysis(result); setProcessId(result.processIds.length === 1 ? result.processIds[0] : ""); | ||
| } catch (failure) { setError(failure); } finally { setBusy(false); } | ||
| }; | ||
| const submit = async () => { | ||
| if (!file || !analysis || (analysis.processIds.length > 1 && !processId)) return; | ||
| setBusy(true); setError(undefined); | ||
| try { onImported(await client.bpmn.import(file, { processId: processId || undefined }, begin())); } | ||
| catch (failure) { setError(failure); } finally { setBusy(false); } | ||
| }; | ||
| return <Dialog open onOpenChange={open => { if (!open) onClose(); }}><DialogContent className="max-h-[85vh] overflow-y-auto"> | ||
| <DialogHeader><DialogTitle>Import BPMN</DialogTitle><DialogDescription>Review findings before importing. Dropped elements will not run; Degraded elements do not run exactly as authored.</DialogDescription></DialogHeader> | ||
| <label htmlFor="bpmn-file">BPMN document (up to 10 MB)</label> | ||
| <Input id="bpmn-file" type="file" accept=".bpmn" disabled={busy} onChange={event => void analyze(event.target.files?.[0])} /> | ||
| {busy && <p role="status">Processing BPMN document…</p>} | ||
| <Refusal error={error} /> | ||
| {analysis && <> | ||
| {analysis.processIds.length > 1 && <label>Process<select aria-label="Process" value={processId} disabled={busy} onChange={event => setProcessId(event.target.value)}><option value="">Select a process</option>{analysis.processIds.map(id => <option key={id} value={id}>{id}</option>)}</select></label>} | ||
| {analysis.issues.length === 0 ? <p>No findings. This document reads without loss.</p> : [...new Set(analysis.issues.map(issue => issue.severity))].map(severity => <section key={severity}><h3>{severity}</h3><ul>{analysis.issues.filter(issue => issue.severity === severity).map((issue, index) => <li key={index}>{issue.elementId && <strong>[{issue.elementId}] </strong>}{issue.message}{issue.processId && <span> (process {issue.processId})</span>}</li>)}</ul></section>)} | ||
| </>} | ||
| <DialogFooter><Button variant="outline" onClick={onClose}>Cancel</Button><Button disabled={busy || !analysis || (analysis.processIds.length > 1 && !processId)} onClick={() => void submit()}>Import</Button></DialogFooter> | ||
| </DialogContent></Dialog>; | ||
| } | ||
|
|
||
| export function BpmnExportDialog({ definitionId, onClose }: { definitionId: string; onClose: () => void }) { | ||
| const client = useElsaClient(); | ||
| const begin = useRequest(); | ||
| const [busy, setBusy] = useState(false); | ||
| const [error, setError] = useState<unknown>(); | ||
| const submit = async () => { | ||
| setBusy(true); setError(undefined); | ||
| try { downloadBlob(await client.bpmn.export(definitionId, begin()), `${definitionId}.bpmn`); onClose(); } | ||
| catch (failure) { setError(failure); } finally { setBusy(false); } | ||
| }; | ||
| return <Dialog open onOpenChange={open => { if (!open) onClose(); }}><DialogContent> | ||
| <DialogHeader><DialogTitle>Export BPMN</DialogTitle><DialogDescription>The exported file contains this workflow's binding configuration and expressions. Only current, unchanged imported BPMN source can be exported.</DialogDescription></DialogHeader> | ||
| <Refusal error={error} /> | ||
| <DialogFooter><Button variant="outline" onClick={onClose}>Cancel</Button><Button disabled={busy} onClick={() => void submit()}>{busy ? "Exporting…" : "Export"}</Button></DialogFooter> | ||
| </DialogContent></Dialog>; | ||
| } |
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.