React components for BPMN/DMN diagram editing, built on bpmn-js-spiffworkflow.
{
"dependencies": {
"bpmn-js-spiffworkflow-react": "github:sartography/spiff-arena#main&path=spiffworkflow-frontend/packages/bpmn-js-spiffworkflow-react"
}
}Your app must install these dependencies:
npm install react react-dom bpmn-js bpmn-js-properties-panel bpmn-js-spiffworkflow dmn-js dmn-js-properties-panel diagram-jsOr add to package.json:
{
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"bpmn-js": "^18.9.1",
"bpmn-js-properties-panel": "^5.44.0",
"bpmn-js-spiffworkflow": "github:sartography/bpmn-js-spiffworkflow#main",
"dmn-js": "^17.4.0",
"dmn-js-properties-panel": "^3.8.3",
"diagram-js": "^15.4.0"
}
}import {
BpmnEditor,
BpmnEditorRef,
DefaultBpmnApiService,
} from "bpmn-js-spiffworkflow-react";
import { useRef } from "react";
// Create an API service (or implement your own)
const apiService = new DefaultBpmnApiService({
baseUrl: "/api/v1",
templateBaseUrl: "/templates/",
});
function MyEditor() {
const editorRef = useRef<BpmnEditorRef>(null);
const handleSave = async () => {
const xml = await editorRef.current?.getXML();
console.log("Diagram XML:", xml);
};
return (
<div>
<button onClick={handleSave}>Save</button>
<BpmnEditor
ref={editorRef}
apiService={apiService}
processModelId="my-process"
diagramType="bpmn"
fileName="diagram.bpmn"
/>
</div>
);
}The main diagram editor component supporting BPMN, DMN, and read-only viewing.
| Prop | Type | Required | Description |
|---|---|---|---|
apiService |
BpmnApiService |
Yes | API service for loading/saving diagrams |
processModelId |
string |
Yes | Identifier for the process model |
diagramType |
'bpmn' | 'dmn' | 'readonly' |
Yes | Type of diagram editor |
diagramXML |
string | null |
No | Pre-loaded diagram XML (skips API call) |
fileName |
string |
No | File name to load from API |
url |
string |
No | URL to fetch diagram from |
tasks |
BasicTask[] |
No | Tasks to highlight (for readonly view) |
taskMetadataKeys |
TaskMetadataItem[] |
No | Metadata keys for task configuration |
| Handler | Description |
|---|---|
onElementClick |
Called when a diagram element is clicked |
onElementsChanged |
Called when diagram elements are modified |
onLaunchScriptEditor |
Called when user wants to edit a script |
onLaunchMarkdownEditor |
Called when user wants to edit markdown |
onLaunchBpmnEditor |
Called when navigating to a call activity |
onLaunchDmnEditor |
Called when editing a DMN reference |
onLaunchJsonSchemaEditor |
Called when editing a JSON schema file |
onLaunchMessageEditor |
Called when editing a message |
onServiceTasksRequested |
Called when service task list is needed |
onDataStoresRequested |
Called when data store list is needed |
onMessagesRequested |
Called when message list is needed |
onDmnFilesRequested |
Called when DMN file list is needed |
onJsonSchemaFilesRequested |
Called when JSON schema list is needed |
onSearchProcessModels |
Called when searching for call activities |
onCallActivityOverlayClick |
Called when call activity overlay is clicked |
interface BpmnEditorRef {
getXML(): Promise<string>; // Get current diagram XML
zoom(amount: number): void; // Zoom: 1=in, -1=out, 0=fit
getModeler(): any; // Access underlying bpmn-js modeler
}You must provide an API service that implements BpmnApiService. You can:
- Use
DefaultBpmnApiServicewith configuration - Extend
DefaultBpmnApiService - Implement
BpmnApiServicefrom scratch
interface BpmnApiService {
// Required
loadDiagramFile(
processModelId: string,
fileName: string,
): Promise<{ file_contents: string }>;
saveDiagramFile(
processModelId: string,
fileName: string,
content: string,
): Promise<void>;
loadDiagramTemplate(templateName: string): Promise<string>;
// Optional - for enhanced editor features
getServiceTasks?(): Promise<any[]>;
getDataStores?(): Promise<any[]>;
getMessages?(): Promise<any[]>;
getDmnFiles?(): Promise<any[]>;
getJsonSchemaFiles?(): Promise<any[]>;
searchProcessModels?(query: string): Promise<any[]>;
}import { DefaultBpmnApiService } from "bpmn-js-spiffworkflow-react";
const apiService = new DefaultBpmnApiService({
baseUrl: "/api/v1", // Base URL for API calls
templateBaseUrl: "/static/", // Base URL for diagram templates
headers: {
// Custom headers (e.g., auth)
Authorization: "Bearer xxx",
},
onError: (error) => {
// Global error handler
console.error("API Error:", error);
},
onUnauthorized: () => {
// Handle 401 responses
window.location.href = "/login";
},
});import { BpmnApiService } from "bpmn-js-spiffworkflow-react";
class MyApiService implements BpmnApiService {
async loadDiagramFile(processModelId: string, fileName: string) {
const response = await myHttpClient.get(
`/models/${processModelId}/files/${fileName}`,
);
return { file_contents: response.data.content };
}
async saveDiagramFile(
processModelId: string,
fileName: string,
content: string,
) {
await myHttpClient.put(`/models/${processModelId}/files/${fileName}`, {
content,
});
}
async loadDiagramTemplate(templateName: string) {
const response = await fetch(`/templates/${templateName}`);
return response.text();
}
}The following endpoints are used by DefaultBpmnApiService. Implement these in your backend or customize the API service.
GET /process-models/{process_model_id}/files/{file_name}
Response:
{
"file_contents": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>..."
}PUT /process-models/{process_model_id}/files/{file_name}
Request Body:
{
"file_contents": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>..."
}Response: 200 OK (empty or confirmation)
GET /{template_name}
Static file serving. Templates should be .bpmn or .dmn XML files.
Required templates:
new_bpmn_diagram.bpmn- Empty BPMN diagram with{{PROCESS_ID}}placeholdernew_dmn_diagram.dmn- Empty DMN diagram with{{DECISION_ID}}placeholder
Example new_bpmn_diagram.bpmn:
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
id="Definitions_1"
targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="{{PROCESS_ID}}" isExecutable="true">
<bpmn:startEvent id="StartEvent_1" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="{{PROCESS_ID}}">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="79" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>These endpoints enable additional editor features. If not implemented, the corresponding features won't work but the editor will still function.
Used for service task autocomplete in the properties panel.
GET /service-tasks
Response:
[
{
"id": "send-email",
"name": "Send Email",
"description": "Sends an email via SMTP",
"parameters": [
{ "name": "to", "type": "string", "required": true },
{ "name": "subject", "type": "string", "required": true },
{ "name": "body", "type": "string", "required": true }
]
}
]Used for data store reference selection.
GET /data-stores
Response:
[
{
"id": "customer-db",
"name": "Customer Database",
"type": "database"
}
]Used for message event configuration.
GET /messages
Response:
[
{
"id": "order-received",
"name": "Order Received",
"correlation_properties": [{ "name": "orderId", "type": "string" }]
}
]Used for business rule task DMN file selection.
GET /dmn-files
Response:
[
{
"id": "pricing-rules",
"name": "Pricing Rules",
"file_name": "pricing.dmn"
}
]Used for user task form selection.
GET /json-schema-files
Response:
[
{
"id": "customer-form",
"name": "Customer Form",
"file_name": "customer-schema.json"
}
]Used for call activity process selection.
GET /process-models?search={query}
Response:
[
{
"id": "subprocess-approval",
"display_name": "Approval Subprocess",
"description": "Handles approval workflow"
}
]Used for highlighting tasks in readonly view:
interface BasicTask {
id: number;
guid?: string;
process_instance_id?: number;
bpmn_identifier: string; // ID of the BPMN element
bpmn_name?: string;
bpmn_process_direct_parent_guid?: string;
bpmn_process_definition_identifier: string; // Process ID the task belongs to
typename: string; // e.g., "UserTask", "ScriptTask", "CallActivity"
state: string; // "COMPLETED", "READY", "WAITING", "STARTED", "CANCELLED", "ERROR"
runtime_info?: Record<string, any>;
properties_json?: Record<string, any>;
}interface ProcessModel {
id: string;
display_name: string;
description?: string;
primary_file_name?: string;
}Used for showing callers of a process:
interface ProcessReference {
relative_location: string; // e.g., "group/subgroup/process-id"
display_name: string;
}The package exports several utility functions:
import {
getBpmnProcessIdentifiers, // Extract process IDs from diagram
convertSvgElementToHtmlString, // Convert React SVG to HTML string
makeid, // Generate random ID
taskIsMultiInstanceChild, // Check if task is MI child
checkTaskCanBeHighlighted, // Check if task can be highlighted
} from "bpmn-js-spiffworkflow-react";The package includes necessary CSS. Import in your app:
import "bpmn-js-spiffworkflow-react";
// or specifically:
import "bpmn-js-spiffworkflow-react/src/styles/bpmn-js-properties-panel.css";Additional CSS from bpmn-js is imported automatically.
import React, { useRef, useState } from "react";
import {
BpmnEditor,
BpmnEditorRef,
BpmnApiService,
BasicTask,
} from "bpmn-js-spiffworkflow-react";
// Custom API service implementation
class MyBpmnApiService implements BpmnApiService {
private baseUrl: string;
private token: string;
constructor(baseUrl: string, token: string) {
this.baseUrl = baseUrl;
this.token = token;
}
private async request(path: string, options: RequestInit = {}) {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.token}`,
...options.headers,
},
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response;
}
async loadDiagramFile(processModelId: string, fileName: string) {
const response = await this.request(
`/models/${processModelId}/files/${fileName}`,
);
return response.json();
}
async saveDiagramFile(
processModelId: string,
fileName: string,
content: string,
) {
await this.request(`/models/${processModelId}/files/${fileName}`, {
method: "PUT",
body: JSON.stringify({ file_contents: content }),
});
}
async loadDiagramTemplate(templateName: string) {
const response = await fetch(`/templates/${templateName}`);
return response.text();
}
}
function ProcessEditor({
processModelId,
fileName,
}: {
processModelId: string;
fileName: string;
}) {
const editorRef = useRef<BpmnEditorRef>(null);
const [apiService] = useState(
() => new MyBpmnApiService("/api/v1", "my-token"),
);
const handleSave = async () => {
const xml = await editorRef.current?.getXML();
if (xml) {
await apiService.saveDiagramFile(processModelId, fileName, xml);
alert("Saved!");
}
};
const handleElementClick = (element: any, processIds: string[]) => {
console.log("Clicked:", element.id, "in processes:", processIds);
};
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<div style={{ padding: "10px" }}>
<button onClick={handleSave}>Save</button>
<button onClick={() => editorRef.current?.zoom(1)}>Zoom In</button>
<button onClick={() => editorRef.current?.zoom(-1)}>Zoom Out</button>
<button onClick={() => editorRef.current?.zoom(0)}>Fit</button>
</div>
<div style={{ flex: 1 }}>
<BpmnEditor
ref={editorRef}
apiService={apiService}
processModelId={processModelId}
diagramType="bpmn"
fileName={fileName}
onElementClick={handleElementClick}
/>
</div>
</div>
);
}
// Readonly viewer with task highlighting
function ProcessViewer({ tasks }: { tasks: BasicTask[] }) {
const editorRef = useRef<BpmnEditorRef>(null);
const [apiService] = useState(
() => new MyBpmnApiService("/api/v1", "my-token"),
);
return (
<BpmnEditor
ref={editorRef}
apiService={apiService}
processModelId="my-process"
diagramType="readonly"
fileName="diagram.bpmn"
tasks={tasks}
onCallActivityOverlayClick={(task, event) => {
console.log("Navigate to subprocess:", task);
}}
/>
);
}