├── .babelrc ├── .eslintignore ├── .gitignore ├── .npmrc ├── README.md ├── app ├── app.js ├── css │ └── app.css └── index.html ├── docs └── screenshot.png ├── package.json ├── resources ├── diagram.bpmn └── qa.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "env" ] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This example is part of our [:notebook: custom elements guide](https://github.com/bpmn-io/bpmn-js-examples/tree/master/custom-elements). Checkout the final result [here](https://github.com/bpmn-io/bpmn-js-example-custom-elements). 2 | 3 | 4 | # bpmn-js Example: Model Extension 5 | 6 | An example of creating a model extension for [bpmn-js](https://github.com/bpmn-io/bpmn-js). Model extensions allow you to read, modify and write BPMN 2.0 diagrams that contain extension attributes and elements. 7 | 8 | 9 | ## About 10 | 11 | This example allows you to read, modify and write BPMN 2.0 diagrams that contain `qa:suitable` extension attributes and `qa:analysisDetails` extension elements. You can set the suitability score of each element. 12 | 13 | ![Screenshot](docs/screenshot.png) 14 | 15 | Here's an example of a diagram: 16 | 17 | ```xml 18 | 19 | 27 | 28 | 29 | SequenceFlow_1 30 | 31 | 32 | 33 | Our operators always have a hard time to figure out, what they need to do here. 34 | 35 | 36 | I believe this can be split up in a number of activities and partly automated. 37 | 38 | 39 | 40 | 41 | ... 42 | 43 | 44 | ... 45 | 46 | 47 | ``` 48 | 49 | Check out the entire diagram [here](resources/diagram.bpmn). 50 | 51 | ### Creating a Model Extension 52 | 53 | Our extension of BPMN 2.0 will be defined in a JSON file: 54 | 55 | ```json 56 | { 57 | "name": "QualityAssurance", 58 | "uri": "http://some-company/schema/bpmn/qa", 59 | "prefix": "qa", 60 | "xml": { 61 | "tagAlias": "lowerCase" 62 | }, 63 | "types": [ 64 | { 65 | "name": "AnalyzedNode", 66 | "extends": [ 67 | "bpmn:FlowNode" 68 | ], 69 | "properties": [ 70 | { 71 | "name": "suitable", 72 | "isAttr": true, 73 | "type": "Float" 74 | } 75 | ] 76 | }, 77 | { 78 | "name": "AnalysisDetails", 79 | "superClass": [ "Element" ], 80 | "properties": [ 81 | { 82 | "name": "lastChecked", 83 | "isAttr": true, 84 | "type": "String" 85 | }, 86 | { 87 | "name": "nextCheck", 88 | "isAttr": true, 89 | "type": "String" 90 | }, 91 | { 92 | "name": "comments", 93 | "isMany": true, 94 | "type": "Comment" 95 | } 96 | ] 97 | }, 98 | ... 99 | ], 100 | ... 101 | } 102 | ``` 103 | 104 | Check out the entire extension [here](resources/qa.json). 105 | 106 | A few things are worth noting here: 107 | 108 | * You can extend existing types using the `"extends"` property. 109 | * If you want to add extension elements to `bpmn:ExtensionElements` they have to have `"superClass": [ "Element" ]`. 110 | 111 | For more information about model extensions head over to [moddle](https://github.com/bpmn-io/moddle). 112 | 113 | Next, let's add our model extension to bpmn-js. 114 | 115 | 116 | ### Adding the Model Extension to bpmn-js 117 | 118 | When creating a new instance of bpmn-js we need to add our model extension using the `moddleExtenions` property: 119 | 120 | ```javascript 121 | import BpmnModeler from 'bpmn-js/lib/Modeler'; 122 | 123 | import qaExtension from '../resources/qaPackage.json'; 124 | 125 | const bpmnModeler = new BpmnModeler({ 126 | moddleExtensions: { 127 | qa: qaExtension 128 | } 129 | }); 130 | ``` 131 | 132 | Our model extension will be used by [bpmn-moddle](https://github.com/bpmn-io/bpmn-moddle) which is part of bpmn-js. 133 | 134 | ### Modifying Extension Attributes and Elements 135 | 136 | bpmn-js can now read, modify and write extension attributes and elements that we defined in our model extension. 137 | 138 | After importing a diagram you could for instance read `qa:AnalysisDetails` extension elements of BPMN 2.0 elements: 139 | 140 | ```javascript 141 | function getExtensionElement(element, type) { 142 | if (!element.extensionElements) { 143 | return; 144 | } 145 | 146 | return element.extensionElements.values.filter((extensionElement) => { 147 | return extensionElement.$instanceOf(type); 148 | })[0]; 149 | } 150 | 151 | const businessObject = getBusinessObject(element); 152 | 153 | const analysisDetails = getExtensionElement(businessObject, 'qa:AnalysisDetails'); 154 | ``` 155 | 156 | In our example we can set the suitability score of each element: 157 | 158 | ```javascript 159 | const suitabilityScoreEl = document.getElementById('suitability-score'); 160 | 161 | const suitabilityScore = Number(suitabilityScoreEl.value); 162 | 163 | if (isNaN(suitabilityScore)) { 164 | return; 165 | } 166 | 167 | const extensionElements = businessObject.extensionElements || moddle.create('bpmn:ExtensionElements'); 168 | 169 | let analysisDetails = getExtensionElement(businessObject, 'qa:AnalysisDetails'); 170 | 171 | if (!analysisDetails) { 172 | analysisDetails = moddle.create('qa:AnalysisDetails'); 173 | 174 | extensionElements.get('values').push(analysisDetails); 175 | } 176 | 177 | analysisDetails.lastChecked = new Date().toISOString(); 178 | 179 | const modeling = bpmnModeler.get('modeling'); 180 | 181 | modeling.updateProperties(element, { 182 | extensionElements, 183 | suitable: suitabilityScore 184 | }); 185 | ``` 186 | 187 | Check out the entire code [here](app/app.js). 188 | 189 | ## Run the Example 190 | 191 | You need a [NodeJS](http://nodejs.org) development stack with [npm](https://npmjs.org) installed to build the project. 192 | 193 | To install all project dependencies execute 194 | 195 | ```sh 196 | npm install 197 | ``` 198 | 199 | To start the example execute 200 | 201 | ```sh 202 | npm start 203 | ``` 204 | 205 | To build the example into the `public` folder execute 206 | 207 | ```sh 208 | npm run all 209 | ``` 210 | 211 | 212 | ## License 213 | 214 | MIT 215 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import BpmnModeler from 'bpmn-js/lib/Modeler'; 2 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 3 | 4 | import diagramXML from '../resources/diagram.bpmn'; 5 | 6 | import qaExtension from '../resources/qa'; 7 | 8 | const HIGH_PRIORITY = 1500; 9 | 10 | const containerEl = document.getElementById('container'), 11 | qualityAssuranceEl = document.getElementById('quality-assurance'), 12 | suitabilityScoreEl = document.getElementById('suitability-score'), 13 | lastCheckedEl = document.getElementById('last-checked'), 14 | okayEl = document.getElementById('okay'), 15 | formEl = document.getElementById('form'), 16 | warningEl = document.getElementById('warning'); 17 | 18 | // hide quality assurance if user clicks outside 19 | window.addEventListener('click', (event) => { 20 | const { target } = event; 21 | 22 | if (target === qualityAssuranceEl || qualityAssuranceEl.contains(target)) { 23 | return; 24 | } 25 | 26 | qualityAssuranceEl.classList.add('hidden'); 27 | }); 28 | 29 | // create modeler 30 | const bpmnModeler = new BpmnModeler({ 31 | container: containerEl, 32 | moddleExtensions: { 33 | qa: qaExtension 34 | } 35 | }); 36 | 37 | // import XML 38 | bpmnModeler.importXML(diagramXML, (err) => { 39 | if (err) { 40 | console.error(err); 41 | } 42 | 43 | const moddle = bpmnModeler.get('moddle'), 44 | modeling = bpmnModeler.get('modeling'); 45 | 46 | let analysisDetails, 47 | businessObject, 48 | element, 49 | suitabilityScore; 50 | 51 | // validate suitability score 52 | function validate() { 53 | const { value } = suitabilityScoreEl; 54 | 55 | if (isNaN(value)) { 56 | warningEl.classList.remove('hidden'); 57 | okayEl.disabled = true; 58 | } else { 59 | warningEl.classList.add('hidden'); 60 | okayEl.disabled = false; 61 | } 62 | } 63 | 64 | // open quality assurance if user right clicks on element 65 | bpmnModeler.on('element.contextmenu', HIGH_PRIORITY, (event) => { 66 | event.originalEvent.preventDefault(); 67 | event.originalEvent.stopPropagation(); 68 | 69 | qualityAssuranceEl.classList.remove('hidden'); 70 | 71 | ({ element } = event); 72 | 73 | // ignore root element 74 | if (!element.parent) { 75 | return; 76 | } 77 | 78 | businessObject = getBusinessObject(element); 79 | 80 | let { suitable } = businessObject; 81 | 82 | suitabilityScoreEl.value = suitable ? suitable : ''; 83 | 84 | suitabilityScoreEl.focus(); 85 | 86 | analysisDetails = getExtensionElement(businessObject, 'qa:AnalysisDetails'); 87 | 88 | lastCheckedEl.textContent = analysisDetails ? analysisDetails.lastChecked : '-'; 89 | 90 | validate(); 91 | }); 92 | 93 | // set suitability core and last checked if user submits 94 | formEl.addEventListener('submit', (event) => { 95 | event.preventDefault(); 96 | event.stopPropagation(); 97 | 98 | suitabilityScore = Number(suitabilityScoreEl.value); 99 | 100 | if (isNaN(suitabilityScore)) { 101 | return; 102 | } 103 | 104 | const extensionElements = businessObject.extensionElements || moddle.create('bpmn:ExtensionElements'); 105 | 106 | if (!analysisDetails) { 107 | analysisDetails = moddle.create('qa:AnalysisDetails'); 108 | 109 | extensionElements.get('values').push(analysisDetails); 110 | } 111 | 112 | analysisDetails.lastChecked = new Date().toISOString(); 113 | 114 | modeling.updateProperties(element, { 115 | extensionElements, 116 | suitable: suitabilityScore 117 | }); 118 | 119 | qualityAssuranceEl.classList.add('hidden'); 120 | }); 121 | 122 | // close quality assurance if user presses escape 123 | formEl.addEventListener('keydown', (event) => { 124 | if (event.key === 'Escape') { 125 | qualityAssuranceEl.classList.add('hidden'); 126 | } 127 | }); 128 | 129 | // validate suitability score if user inputs value 130 | suitabilityScoreEl.addEventListener('input', validate); 131 | 132 | }); 133 | 134 | function getExtensionElement(element, type) { 135 | if (!element.extensionElements) { 136 | return; 137 | } 138 | 139 | return element.extensionElements.values.filter((extensionElement) => { 140 | return extensionElement.$instanceOf(type); 141 | })[0]; 142 | } -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | outline: none; 5 | padding: 0; 6 | } 7 | 8 | html, body, #container { 9 | height: 100%; 10 | } 11 | 12 | .hidden { 13 | display: none; 14 | } 15 | 16 | .panel { 17 | background-color: #fafafa; 18 | border: solid 1px #ccc; 19 | border-radius: 2px; 20 | font-family: 'Arial', sans-serif; 21 | padding: 10px; 22 | } 23 | 24 | #quality-assurance { 25 | color: #111; 26 | left: 50%; 27 | position: absolute; 28 | top: 50%; 29 | transform: translate(-50%, -50%); 30 | } 31 | 32 | #quality-assurance #form input { 33 | border: solid 1px #ccc; 34 | border-radius: 2px; 35 | font-family: 'Arial', sans-serif; 36 | padding: 10px; 37 | } 38 | 39 | #quality-assurance #form input[type=text] { 40 | width: 100%; 41 | } 42 | 43 | #quality-assurance #form #warning { 44 | background-color: rgba(255, 0, 0, 0.25); 45 | border-radius: 2px; 46 | padding: 10px; 47 | } 48 | 49 | #quality-assurance #form input[type=submit] { 50 | background-color: #FAFAFA; 51 | color: #111; 52 | } 53 | 54 | #hint { 55 | bottom: 20px; 56 | left: 20px; 57 | position: absolute; 58 | } -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bpmn-js-example-model-extension 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 37 | 38 |
Click right mouse button to edit properties.
39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpmn-io/bpmn-js-example-model-extension/029e7614dbe2fdf673c0795653be3d4dfb132d77/docs/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpmn-js-example-model-extension", 3 | "version": "0.0.0", 4 | "description": "An example of creating a model extension for bpmn-js", 5 | "scripts": { 6 | "all": "run-s lint build", 7 | "build": "webpack --mode production", 8 | "dev": "webpack-dev-server --content-base=public --open", 9 | "lint": "eslint .", 10 | "start": "run-s dev" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/bpmn-io/bpmn-js-example-model-extension" 15 | }, 16 | "keywords": [ 17 | "bpmnjs-example" 18 | ], 19 | "author": { 20 | "name": "Nico Rehwaldt", 21 | "url": "https://github.com/nikku" 22 | }, 23 | "contributors": [ 24 | { 25 | "name": "bpmn.io contributors", 26 | "url": "https://github.com/bpmn-io" 27 | } 28 | ], 29 | "license": "MIT", 30 | "devDependencies": { 31 | "copy-webpack-plugin": "^4.6.0", 32 | "eslint": "^5.0.1", 33 | "eslint-plugin-bpmn-io": "^0.5.3", 34 | "npm-run-all": "^4.1.3", 35 | "raw-loader": "^0.5.1", 36 | "webpack": "^4.15.1", 37 | "webpack-cli": "^3.0.8", 38 | "webpack-dev-server": "^3.1.14" 39 | }, 40 | "dependencies": { 41 | "bpmn-js": "^3.2.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /resources/diagram.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SequenceFlow_1 6 | 7 | 8 | 9 | Our operators always have a hard time to figure out, what they need to do here. 10 | 11 | 12 | I believe this can be split up in a number of activities and partly automated. 13 | 14 | 15 | 16 | 17 | 18 | 19 | SequenceFlow_1 20 | SequenceFlow_2 21 | SequenceFlow_5 22 | 23 | 24 | 25 | 26 | SequenceFlow_2 27 | 28 | 29 | 30 | SequenceFlow_5 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 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /resources/qa.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "QualityAssurance", 3 | "uri": "http://some-company/schema/bpmn/qa", 4 | "prefix": "qa", 5 | "xml": { 6 | "tagAlias": "lowerCase" 7 | }, 8 | "types": [ 9 | { 10 | "name": "AnalyzedNode", 11 | "extends": [ 12 | "bpmn:FlowNode" 13 | ], 14 | "properties": [ 15 | { 16 | "name": "suitable", 17 | "isAttr": true, 18 | "type": "Integer" 19 | } 20 | ] 21 | }, 22 | { 23 | "name": "AnalysisDetails", 24 | "superClass": [ "Element" ], 25 | "properties": [ 26 | { 27 | "name": "lastChecked", 28 | "isAttr": true, 29 | "type": "String" 30 | }, 31 | { 32 | "name": "nextCheck", 33 | "isAttr": true, 34 | "type": "String" 35 | }, 36 | { 37 | "name": "comments", 38 | "isMany": true, 39 | "type": "Comment" 40 | } 41 | ] 42 | }, 43 | { 44 | "name": "Comment", 45 | "properties": [ 46 | { 47 | "name": "author", 48 | "isAttr": true, 49 | "type": "String" 50 | }, 51 | { 52 | "name": "text", 53 | "isBody": true, 54 | "type": "String" 55 | } 56 | ] 57 | } 58 | ], 59 | "emumerations": [], 60 | "associations": [] 61 | } 62 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: ['./app/app.js'] 6 | }, 7 | output: { 8 | path: __dirname + '/public', 9 | filename: 'app.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.bpmn$/, 15 | use: 'raw-loader' 16 | } 17 | ] 18 | }, 19 | plugins: [ 20 | new CopyWebpackPlugin([ 21 | { from: 'assets/**', to: 'vendor/bpmn-js', context: 'node_modules/bpmn-js/dist/' }, 22 | { from: '**/*.{html,css}', context: 'app/' } 23 | ]) 24 | ], 25 | mode: 'development', 26 | devtool: 'source-map' 27 | }; --------------------------------------------------------------------------------