@uubato/uubato-bpmn-modeller
v0.6.1
Published
A React component for building BPMN 2.0 diagrams in the browser, built on [maxGraph](https://github.com/maxGraph/maxGraph). Supports the full standard BPMN 2.0 element set plus a broad range of execution-oriented properties (async continuations, job retry
Readme
BPMN Modeler
A React component for building BPMN 2.0 diagrams in the browser, built on maxGraph. Supports the full standard BPMN 2.0 element set plus a broad range of execution-oriented properties (async continuations, job retry/priority, external task workers, DMN business-rule references, connector configuration), exported via a custom XML extension namespace rather than tied to any specific workflow engine.
This package lives at BPMN/ in the bpmn-modeler monorepo,
which also holds Browser/ (the dev/demo
harness for this package), DMN/, and CMMN/
(placeholders for future DMN/CMMN modeling packages).
BpmnModeler is a persistence-free, controlled component: you give it
xml and get xml back via onChange. It has no import/export UI, no
"open recent" list, and no storage of its own — where diagrams live (a
server, localStorage, a filesystem) is entirely up to the app embedding
it.
Published on npm as
@uubato/uubato-bpmn-modeller. Seedocs/npm-package-migration.md/docs/npm-package-migration-status.mdfor the package-extraction history, or Developing against source below if you want to work against this monorepo's checkout directly instead of the published package.
Installation
npm install @uubato/uubato-bpmn-modellerPeer dependencies (install alongside it):
npm install [email protected] [email protected]Usage
import { useState } from 'react'
import { BpmnModeler } from '@uubato/uubato-bpmn-modeller'
import '@uubato/uubato-bpmn-modeller/style.css'
function DiagramEditor() {
const [xml, setXml] = useState(null) // null = start with a blank canvas
return (
<BpmnModeler
xml={xml}
onChange={setXml}
theme="light"
/>
)
}The package has no default export — everything is a named export (
BpmnModeler,createEmptyBpmnXml). This keepsimport/requirebehavior identical across every consumption path (ESM, CJS, bundler interop), with no.defaultunwrapping needed anywhere.
@uubato/uubato-bpmn-modeller/style.css includes both the component's own layout rules and
the color/shadow custom properties (--color-*, --shadow-*, …) they rely
on — that's the only stylesheet you need to import. box-sizing: border-box
is scoped to the component's own subtree, so it doesn't affect the rest of
your page. <BpmnModeler> fills whatever height its container gives it —
same contract as any full-size embedded widget — so make sure the element
it renders into has a definite height, e.g.:
<div style={{ height: '100vh' }}>
<BpmnModeler xml={xml} onChange={setXml} />
</div>It does not include a page-wide reset (html/body/#root margins, a
default font-family) — that's left to your own app, the same way any
other embedded component wouldn't rewrite your whole page's global styles.
Props
| Prop | Type | Description |
|---|---|---|
| xml | string \| null \| undefined | The diagram's BPMN XML. The component is controlled: null/undefined means "leave the current diagram alone" (no import happens). Setting it to a new string imports it. |
| onChange | (xml: string) => void | Called with the freshly-exported XML on every graph or process-data mutation. No built-in debounce — debounce/persist on your side if needed. |
| theme | 'light' \| 'dark' | Defaults to 'light'. Plain controlled prop — the component repaints to match it but owns no toggle UI and no persistence; there's no onThemeChange. |
| readOnly | boolean | Defaults to false. Disables every user-driven mutation — toolbar, canvas edits (move/resize/delete/connect/inline-rename), context menu, properties/documentation panel, XML view editing, and undo/redo. Selection, pan, zoom, and the xml/onChange controlled data flow keep working — the host can still swap diagrams while read-only. |
| availableDecisions | Array<{ id: string, name: string }> | Defaults to []. When non-empty, the business-rule-task Properties panel's Decision Ref field becomes a <select> populated from this list instead of a free-text input — for a host that also embeds @uubato/uubato-dmn-modeller and wants to resolve decisionRef against real, currently-open DMN decisions rather than a typed key. Pair with that package's listDecisions(xml) helper to derive the list from a DMN/DRD document's XML. |
| showPropertiesPanel | boolean | Defaults to true. Set to false to remove the right-hand panel (ElementPanel/ProcessPanel) and its collapse toggle from the UI entirely — for a stripped-down, canvas-only embed. Distinct from readOnly, which keeps the panel visible but locks its fields; this hides it altogether. |
createEmptyBpmnXml()
Also exported from the package root. Returns a well-formed, empty-process
BPMN XML string with a freshly generated id — pass it as xml to reset the
diagram to a blank canvas (a "New" action):
import { BpmnModeler, createEmptyBpmnXml } from '@uubato/uubato-bpmn-modeller'
// ...
<button onClick={() => setXml(createEmptyBpmnXml())}>New</button>What the host app owns
BpmnModeler only renders what's directly involved in modeling the one
open diagram (toolbar, canvas, properties panel, validation, undo/redo, the
live XML view). Everything else is left to you:
- Starting a new diagram, an "Open Recent" list, file import/export buttons, and any auto-save/persistence loop
- The theme toggle UI (and remembering the user's choice)
- The read-only toggle UI, if you want one — like
theme,readOnlyis a plain controlled prop with no toggle of its own; you decide when and how a user (or your own app logic) flips it - A keyboard-shortcuts trigger (
ShortcutOverlayis exported from the package root if you want to reuse it —import { ShortcutOverlay } from '@uubato/uubato-bpmn-modeller'— but nothing wires it up automatically)
Advanced usage
Most consumers only need the props table above. Hosts that need to trigger
undo/redo, zoom, fit-to-screen, or the XML/Validate panels from outside
a specific <BpmnModeler> instance — e.g. wiring a native OS menu in an
Electron app with several open tabs — can inject a store created via
createModelerStore() through an _internalStore prop, then read/write it
from sibling code with zustand's own useStore(store, selector):
import { useStore } from 'zustand'
import { BpmnModeler, createModelerStore } from '@uubato/uubato-bpmn-modeller'
const store = createModelerStore() // one per open diagram/tab
// <BpmnModeler xml={xml} onChange={onChange} _internalStore={store} />
// elsewhere, outside that component's subtree:
const undoManager = useStore(store, (s) => s.undoManager)createModelerStore, ModelerStoreContext, and useModelerStore are all
exported from the package root — the latter two only matter if you're
building components that live inside the <BpmnModeler> subtree a given
store was provided to (useModelerStore reads from React context; sibling
code outside that subtree needs the raw useStore(store, selector) form
above instead).
Two more exports round out this tier, for hosts doing their own rendering
around a <BpmnModeler> instance:
updateGraphTheme(graph, isDark)— re-applies every registered cell style's icon/color set after a theme change, without re-registering the whole stylesheet.<BpmnModeler>already calls this internally when its ownthemeprop changes; exported for a host that reaches a specific instance'sgraphvia_internalStoreand needs to trigger the same repaint from outside.getTypeName(cellValue)— returns a cell's human-readable BPMN type name (e.g."Service Task","Exclusive Gateway") from its rawcellValueobject. Useful for building your own export/print view (a PDF property table, say) without duplicating the type-name logicPropertiesPanelandContextMenualready use internally.
See the project's CLAUDE.md ("Advanced / low-level API" section) for the
full mechanism and an in-repo example (Browser/src/App.jsx +
DemoControls.jsx).
Browser/ in this monorepo is exactly that: a
reference host implementation, useful as a template if you're building your
own — including the "🔓 Editable / 🔒 Read-only" button in DemoControls.jsx
that demonstrates exactly this.
Developing against source
If you're working inside this monorepo (or a checkout/symlink of it) and
want to iterate on BPMN/src without going through a published version, you
can import the component by relative path instead of npm installing it:
import { useState } from 'react'
import BpmnModeler from '<relative-path>/bpmn-modeler/BPMN/src/BpmnModeler.jsx'
// No separate CSS import needed — BpmnModeler.jsx imports its own styles
// (src/styles/tokens.css) directly.
function DiagramEditor({ initialXml, onDiagramSaved, theme }) {
const [xml, setXml] = useState(initialXml)
return (
<BpmnModeler
xml={xml}
theme={theme}
onChange={(nextXml) => {
setXml(nextXml)
onDiagramSaved(nextXml) // caller owns debouncing/persistence entirely
}}
/>
)
}See docs/npm-package-migration-status.md
for more detail.
Contributing / local development
Prerequisites
- Node.js v18 or later
- npm v9 or later
Setup
Run from the repo root (this is an npm workspace, so a root install wires up
both BPMN/ and Browser/):
npm installDevelopment
There's no standalone dev server for this package alone — it's developed
against the Browser/ demo app, which embeds
BpmnModeler the same way an external consumer would (via the workspace
dependency, aliased to this package's source for instant hot-reload). From
the repo root:
npm run devTesting
Run this package's unit tests (Vitest):
npm test(equivalent to npm test -w BPMN from the repo root, or plain npm test
from inside BPMN/). End-to-end tests live in Browser/ since they
exercise the running demo app — see that package's README.
A pre-push git hook (managed by Husky, installed automatically via npm install
at the repo root) runs npm run test:all before every git push and blocks
the push if any test fails. To push despite a failure use git push --no-verify.
Library build
Build the installable package (ESM + CJS + CSS) to dist-lib/:
npm run build:libThis is what npm publish would ship — see the exports field in
package.json and docs/npm-package-migration.md
for how it's wired up.
CI / publishing
This repo is GitLab-hosted; the .github/workflows/ci.yml/publish.yml
files at the repo root are leftover GitHub Actions syntax that never
actually ran here. Real CI lives in .gitlab-ci.yml (repo root): a
unit_tests/e2e_tests job pair runs on every push/MR, plus a tag-triggered
(v*.*.*) publish job — verifies the tag matches this package's
package.json version, builds the library, previews the tarball with
npm pack --dry-run -w BPMN, and publishes with npm provenance
(npm publish -w BPMN --provenance) — and a manual publish_dry_run job for
exercising the pipeline without a real tag push.
Automated publishing isn't functional yet: it needs a protected NPM_TOKEN
CI/CD variable and protected v*/dmn-v* tag patterns in the GitLab
project settings, neither of which is set up. Until then, releases (this
package's first publish included) go out via a manual
npm publish --access public run from BPMN/. See
docs/npm-package-migration.md for the
full history.
Project Structure
BPMN/
├── package.json # the published package: name/exports/files/peerDeps
├── vite.config.js # library build only (dist-lib)
├── vitest.config.js
├── src/
│ ├── BpmnModeler.jsx # Package root component — re-exported by name (no default export) from index.js
│ ├── BpmnModeler.module.css
│ ├── index.js # Package entry point — exports { BpmnModeler, createEmptyBpmnXml } by name, no default export
│ ├── components/
│ │ ├── BpmnCanvas/ # maxGraph canvas host; events, keyboard shortcuts, tool modes
│ │ ├── ContextMenu/ # Right-click menu (delete, morph, add next element)
│ │ ├── Toolbar/ # Floating draggable tool palette
│ │ ├── ElementPanel/ # Wrapper panel shown when an element is selected
│ │ ├── PropertiesPanel/ # Element properties form
│ │ ├── DocumentationPanel/ # Free-text documentation textarea
│ │ ├── ProcessPanel/ # Side panel shown when nothing is selected
│ │ ├── MenuBar/ # Process name, Undo/Redo, "Show XML" toggle
│ │ ├── XmlView/ # Live inline XML editor panel
│ │ ├── ValidationPanel/ # Bottom-drawer BPMN validation panel
│ │ └── ShortcutOverlay/ # Keyboard shortcut reference modal (host-mountable, decoupled from the store)
│ ├── graph/
│ │ ├── graphSetup.js # initGraph() – maxGraph instance setup
│ │ ├── bpmnStyles.js # Cell style registration and dark-mode theme switching
│ │ ├── bpmnElements.js # add*() helpers – insert vertices/edges into graph
│ │ ├── bpmnTypes.js # BPMN type/subtype definitions + style-name helpers
│ │ ├── bpmnIcons.js # SVG icon imports (canvas data URIs + toolbar URLs)
│ │ ├── connectionValidation.js # Connection rule logic (getConnectionRejectionReason)
│ │ ├── xmlExport.js # exportToBpmn() → BPMN 2.0 XML string; createEmptyBpmnXml()
│ │ └── xmlImport.js # importFromBpmn() → populates graph from XML
│ ├── store/
│ │ └── modelerStore.js # Zustand store: graph ref, active tool, theme, readOnly, panel state
│ └── styles/
│ └── tokens.css # Color/shadow custom properties; imported directly by BpmnModeler.jsx (ships with the package)
├── tests/unit/
│ ├── helpers/fakeGraph.js # In-memory graph stub (no maxGraph required)
│ ├── bpmnTypes.test.js # Style-name helpers + getTypeName()
│ ├── connectionValidation.test.js # Connection rule tests
│ ├── validation.test.js # runValidation() logic tests
│ ├── xmlRoundtrip.test.js # XML import/export round-trip tests
│ └── modelerStoreIsolation.test.js # Per-instance store isolation (multiple <BpmnModeler> mounts don't share state)
└── docs/
├── bpmn-specification.pdf # Official BPMN 2.0 spec
├── bpmn-examples.pdf # Companion examples document
├── components.md # User-facing components/interactions reference
├── npm-package-migration.md # Package-extraction plan/checklist (history)
├── npm-package-migration-status.md # What's actually landed vs. still planned
└── desktop-app-migration-plan.md # History of Desktop/ becoming a real consumer of this published package instead of a forked copy of src/