├── .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 |
46 |
47 |
48 |
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 | [![CI](https://github.com/bpmn-io/bpmn-js-example-react-properties-panel/actions/workflows/CI.yml/badge.svg)](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 | ![Demo Screenshot](./resources/screenshot.png) 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 |
63 | 64 | { element.id } 65 |
66 | 67 |
68 | 69 | { 70 | this.updateName(event.target.value); 71 | } } /> 72 |
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 |
183 | 184 | { element.id } 185 |
186 | 187 |
188 | 189 | { 190 | updateName(event.target.value) 191 | } } /> 192 |
193 | 194 | { 195 | is(element, 'custom:TopicHolder') && 196 |
197 | 198 | { 199 | updateTopic(event.target.value) 200 | } } /> 201 |
202 | } 203 | 204 |
205 | 206 | 207 | { 208 | is(element, 'bpmn:Task') && !is(element, 'bpmn:ServiceTask') && 209 | 210 | } 211 | 212 | { 213 | is(element, 'bpmn:Event') && !hasDefinition(element, 'bpmn:MessageEventDefinition') && 214 | 215 | } 216 | 217 | { 218 | is(element, 'bpmn:Task') && !isTimeoutConfigured(element) && 219 | 220 | } 221 |
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 | } --------------------------------------------------------------------------------