├── .gitignore
├── resources
└── screenshot.png
├── src
├── properties-panel
│ ├── PropertiesView.css
│ ├── index.js
│ └── PropertiesView.js
├── moddle
│ └── custom.json
├── app.js
├── index.html
└── diagram.bpmn
├── .github
└── workflows
│ └── CI.yml
├── LICENSE
├── package.json
├── webpack.config.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 | *.iml
4 | .DS_Store
5 | npm-debug.log
6 | public/
7 |
--------------------------------------------------------------------------------
/resources/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bpmn-io/bpmn-js-example-react-properties-panel/HEAD/resources/screenshot.png
--------------------------------------------------------------------------------
/src/properties-panel/PropertiesView.css:
--------------------------------------------------------------------------------
1 | .element-properties label {
2 | font-weight: bold;
3 | }
4 |
5 | .element-properties label:after {
6 | content: ': ';
7 | }
8 |
9 | .element-properties button + button {
10 | margin-left: 10px;
11 | }
--------------------------------------------------------------------------------
/src/properties-panel/index.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom';
2 | import React from 'react';
3 |
4 | import PropertiesView from './PropertiesView';
5 |
6 |
7 | export default class PropertiesPanel {
8 |
9 | constructor(options) {
10 |
11 | const {
12 | modeler,
13 | container
14 | } = options;
15 |
16 | ReactDOM.render(
17 | ,
18 | container
19 | );
20 | }
21 | }
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/moddle/custom.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom",
3 | "uri": "http://custom/ns",
4 | "associations": [],
5 | "types": [
6 | {
7 | "name": "TopicHolder",
8 | "extends": [
9 | "bpmn:ServiceTask"
10 | ],
11 | "properties": [
12 | {
13 | "name": "topic",
14 | "isAttr": true,
15 | "type": "String"
16 | }
17 | ]
18 | }
19 | ],
20 | "prefix": "custom",
21 | "xml": {
22 | "tagAlias": "lowerCase"
23 | }
24 | }
--------------------------------------------------------------------------------
/.github/workflows/CI.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [ push, pull_request ]
3 | jobs:
4 | Build:
5 |
6 | strategy:
7 | matrix:
8 | os: [ ubuntu-latest ]
9 |
10 | runs-on: ${{ matrix.os }}
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v4
15 | - name: Use Node.js
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: 20
19 | cache: 'npm'
20 | - name: Install dependencies
21 | run: npm ci
22 | - name: Build
23 | run: npm run all
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import Modeler from 'bpmn-js/lib/Modeler';
2 |
3 | import PropertiesPanel from './properties-panel';
4 |
5 | import customModdleExtension from './moddle/custom.json';
6 |
7 | import diagramXML from './diagram.bpmn';
8 |
9 | const $modelerContainer = document.querySelector('#modeler-container');
10 | const $propertiesContainer = document.querySelector('#properties-container');
11 |
12 | const modeler = new Modeler({
13 | container: $modelerContainer,
14 | moddleExtensions: {
15 | custom: customModdleExtension
16 | },
17 | keyboard: {
18 | bindTo: document.body
19 | }
20 | });
21 |
22 | const propertiesPanel = new PropertiesPanel({
23 | container: $propertiesContainer,
24 | modeler
25 | });
26 |
27 | modeler.importXML(diagramXML);
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Camunda Services GmbH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | bpmn-js-react-properties-panel
5 |
6 |
7 |
8 |
9 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bpmn-js-example-react-properties-panel",
3 | "version": "0.0.0",
4 | "description": "A custom react properties panel example",
5 | "scripts": {
6 | "all": "webpack --mode production",
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "webpack-dev-server --open"
9 | },
10 | "author": {
11 | "name": "Niklas Kiefer",
12 | "url": "https://github.com/pinussilvestrus"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/bpmn-io/bpmn-js-example-react-properties-panel"
17 | },
18 | "license": "MIT",
19 | "devDependencies": {
20 | "@babel/core": "^7.23.9",
21 | "@babel/plugin-transform-react-jsx": "^7.23.4",
22 | "babel-loader": "^9.1.3",
23 | "copy-webpack-plugin": "^12.0.2",
24 | "css-loader": "^6.10.0",
25 | "file-loader": "^6.2.0",
26 | "raw-loader": "^4.0.2",
27 | "style-loader": "^3.3.4",
28 | "webpack": "^5.90.2",
29 | "webpack-cli": "^5.1.4",
30 | "webpack-dev-server": "^5.0.2"
31 | },
32 | "dependencies": {
33 | "bpmn-js": "^17.0.1",
34 | "downloadjs": "^1.4.7",
35 | "react": "^18.2.0",
36 | "react-dom": "^18.2.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const CopyWebpackPlugin = require('copy-webpack-plugin');
2 |
3 | module.exports = {
4 | entry: {
5 | bundle: ['./src/app.js']
6 | },
7 | output: {
8 | path: __dirname + '/public',
9 | filename: 'app.js'
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.[cm]?js$/,
15 | exclude: /node_modules/,
16 | use: {
17 | loader: 'babel-loader',
18 | options: {
19 | plugins: [
20 | '@babel/plugin-transform-react-jsx'
21 | ]
22 | }
23 | }
24 | },
25 | {
26 | oneOf: [
27 | {
28 | test: /\.css$/,
29 | use: [
30 | { loader: 'style-loader' },
31 | { loader: 'css-loader' }
32 | ]
33 | },
34 | {
35 | test: /\.bpmn$/,
36 | use: 'raw-loader',
37 | },
38 | {
39 | exclude: /\.([cm]?js|html|json)$/,
40 | loader: 'file-loader',
41 | options: {
42 | name: 'static/media/[name].[hash:8].[ext]',
43 | }
44 | }
45 | ]
46 | }
47 | ]
48 | },
49 | plugins: [
50 | new CopyWebpackPlugin({
51 | patterns: [
52 | { from: 'assets/**', to: 'vendor/bpmn-js', context: 'node_modules/bpmn-js/dist/' },
53 | { from: 'index.html', context: 'src/' }
54 | ]
55 | })
56 | ],
57 | mode: 'development',
58 | devtool: 'source-map'
59 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Properties Panel for bpmn-js
2 |
3 | [](https://github.com/bpmn-io/bpmn-js-example-react-properties-panel/actions/workflows/CI.yml)
4 |
5 | This example demonstrates a custom properties panel for [bpmn-js](https://github.com/bpmn-io/bpmn-js) written in [React](https://reactjs.org/).
6 |
7 | 
8 |
9 | ## About
10 |
11 | The component [`PropertiesView`](./src/properties-panel/PropertiesView.js) implements the properties panel.
12 |
13 | The component is mounted via standard React utilities and receives the BPMN modeler instance as props:
14 |
15 | ```js
16 | ReactDOM.render(
17 | ,
18 | container
19 | );
20 | ```
21 |
22 | As part of its life-cycle hooks it hooks up with bpmn-js change and selection events to react to editor changes:
23 |
24 | ```js
25 | class PropertiesView extends React.Component {
26 |
27 | ...
28 |
29 | componentDidMount() {
30 |
31 | const {
32 | modeler
33 | } = this.props;
34 |
35 | modeler.on('selection.changed', (e) => {
36 | this.setElement(e.newSelection[0]);
37 | });
38 |
39 | modeler.on('element.changed', (e) => {
40 | this.setElement(e.element);
41 | });
42 | }
43 |
44 | }
45 | ```
46 |
47 | Rendering the component we may display element properties and apply changes:
48 |
49 | ```js
50 | class PropertiesView extends React.Component {
51 |
52 | ...
53 |
54 | render() {
55 |
56 | const {
57 | element
58 | } = this.state;
59 |
60 | return (
61 |
62 |
66 |
67 |
73 |
74 | );
75 | }
76 |
77 | updateName(newName) {
78 |
79 | const {
80 | element
81 | } = this.state;
82 |
83 | const {
84 | modeler
85 | } = this.props;
86 |
87 | const modeling = modeler.get('modeling');
88 |
89 | modeling.updateLabel(element, newName);
90 | }
91 | }
92 | ```
93 |
94 |
95 | ## Run the Example
96 |
97 | ```sh
98 | npm install
99 |
100 | npm start
101 | ```
102 |
103 |
104 | ## License
105 |
106 | MIT
107 |
--------------------------------------------------------------------------------
/src/diagram.bpmn:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SequenceFlow_0b6cm13
6 |
7 |
8 |
9 | SequenceFlow_035kn8o
10 |
11 |
12 |
13 | SequenceFlow_17w8608
14 | SequenceFlow_035kn8o
15 |
16 |
17 |
18 | SequenceFlow_0b6cm13
19 | SequenceFlow_17w8608
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/properties-panel/PropertiesView.js:
--------------------------------------------------------------------------------
1 | import { is } from 'bpmn-js/lib/util/ModelUtil';
2 |
3 | import React, { Component } from 'react';
4 |
5 | import './PropertiesView.css';
6 |
7 |
8 | export default class PropertiesView extends Component {
9 |
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {
14 | selectedElements: [],
15 | element: null
16 | };
17 | }
18 |
19 | componentDidMount() {
20 |
21 | const {
22 | modeler
23 | } = this.props;
24 |
25 | modeler.on('selection.changed', (e) => {
26 |
27 | const {
28 | element
29 | } = this.state;
30 |
31 | this.setState({
32 | selectedElements: e.newSelection,
33 | element: e.newSelection[0]
34 | });
35 | });
36 |
37 |
38 | modeler.on('element.changed', (e) => {
39 |
40 | const {
41 | element
42 | } = e;
43 |
44 | const {
45 | element: currentElement
46 | } = this.state;
47 |
48 | if (!currentElement) {
49 | return;
50 | }
51 |
52 | // update panel, if currently selected element changed
53 | if (element.id === currentElement.id) {
54 | this.setState({
55 | element
56 | });
57 | }
58 |
59 | });
60 | }
61 |
62 | render() {
63 |
64 | const {
65 | modeler
66 | } = this.props;
67 |
68 | const {
69 | selectedElements,
70 | element
71 | } = this.state;
72 |
73 | return (
74 |
75 |
76 | {
77 | selectedElements.length === 1
78 | &&
79 | }
80 |
81 | {
82 | selectedElements.length === 0
83 | && Please select an element.
84 | }
85 |
86 | {
87 | selectedElements.length > 1
88 | && Please select a single element.
89 | }
90 |
91 | );
92 | }
93 |
94 | }
95 |
96 |
97 | function ElementProperties(props) {
98 |
99 | let {
100 | element,
101 | modeler
102 | } = props;
103 |
104 | if (element.labelTarget) {
105 | element = element.labelTarget;
106 | }
107 |
108 | function updateName(name) {
109 | const modeling = modeler.get('modeling');
110 |
111 | modeling.updateLabel(element, name);
112 | }
113 |
114 | function updateTopic(topic) {
115 | const modeling = modeler.get('modeling');
116 |
117 | modeling.updateProperties(element, {
118 | 'custom:topic': topic
119 | });
120 | }
121 |
122 | function makeMessageEvent() {
123 |
124 | const bpmnReplace = modeler.get('bpmnReplace');
125 |
126 | bpmnReplace.replaceElement(element, {
127 | type: element.businessObject.$type,
128 | eventDefinitionType: 'bpmn:MessageEventDefinition'
129 | });
130 | }
131 |
132 | function makeServiceTask(name) {
133 | const bpmnReplace = modeler.get('bpmnReplace');
134 |
135 | bpmnReplace.replaceElement(element, {
136 | type: 'bpmn:ServiceTask'
137 | });
138 | }
139 |
140 | function attachTimeout() {
141 | const modeling = modeler.get('modeling');
142 | const autoPlace = modeler.get('autoPlace');
143 | const selection = modeler.get('selection');
144 |
145 | const attrs = {
146 | type: 'bpmn:BoundaryEvent',
147 | eventDefinitionType: 'bpmn:TimerEventDefinition'
148 | };
149 |
150 | const position = {
151 | x: element.x + element.width,
152 | y: element.y + element.height
153 | };
154 |
155 | const boundaryEvent = modeling.createShape(attrs, position, element, { attach: true });
156 |
157 | const taskShape = append(boundaryEvent, {
158 | type: 'bpmn:Task'
159 | });
160 |
161 | selection.select(taskShape);
162 | }
163 |
164 | function isTimeoutConfigured(element) {
165 | const attachers = element.attachers || [];
166 |
167 | return attachers.some(e => hasDefinition(e, 'bpmn:TimerEventDefinition'));
168 | }
169 |
170 | function append(element, attrs) {
171 |
172 | const autoPlace = modeler.get('autoPlace');
173 | const elementFactory = modeler.get('elementFactory');
174 |
175 | var shape = elementFactory.createShape(attrs);
176 |
177 | return autoPlace.append(element, shape);
178 | };
179 |
180 | return (
181 |
182 |
186 |
187 |
193 |
194 | {
195 | is(element, 'custom:TopicHolder') &&
196 |
202 | }
203 |
204 |
222 |
223 | );
224 | }
225 |
226 |
227 | // helpers ///////////////////
228 |
229 | function hasDefinition(event, definitionType) {
230 |
231 | const definitions = event.businessObject.eventDefinitions || [];
232 |
233 | return definitions.some(d => is(d, definitionType));
234 | }
--------------------------------------------------------------------------------