├── .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 | 
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 | };
--------------------------------------------------------------------------------