├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── Dockerfile ├── README.md ├── app ├── app.js ├── diagrams │ ├── EventBasedGateway.bpmn │ ├── ManyErrorsAndWarnings.bpmn │ ├── multiple.bpmn │ ├── newDiagram.bpmn │ ├── pizzaDelivery.bpmn │ ├── subChoreographies.bpmn │ └── subChoreographies.png ├── favicon.ico ├── icons │ ├── LICENSE.txt │ ├── config.json │ ├── css │ │ └── chor-editor.css │ └── font │ │ ├── chor-editor.eot │ │ ├── chor-editor.svg │ │ ├── chor-editor.ttf │ │ ├── chor-editor.woff │ │ └── chor-editor.woff2 ├── index.html ├── lib │ ├── properties-provider │ │ ├── ChorPropertiesProvider.js │ │ ├── MessageDefinition.js │ │ └── index.js │ └── validator │ │ ├── Validator.js │ │ ├── constraints │ │ ├── CallChoreoParticipantsBijectivityConstraint.js │ │ ├── EventBasedGatewayConstraint.js │ │ ├── NoCyclicCallChoreosConstraint.js │ │ ├── ParticipantNameConstraint.js │ │ ├── SimpleFlowConstraint.js │ │ └── SubChoreoParticipantsConstraint.js │ │ └── util │ │ └── ValidatorUtil.js └── styles │ └── app.less ├── package-lock.json └── package.json /.dockerignore: -------------------------------------------------------------------------------- 1 | README.md 2 | .gitignore 3 | 4 | .DS_Store 5 | node_modules 6 | dist 7 | build 8 | .cache 9 | .idea/ 10 | .eslintignore 11 | .eslintrc 12 | .travis.yml -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | build 4 | .cache -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:bpmn-io/es6", 3 | "env": { 4 | "browser": true, 5 | "es6": true 6 | } 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | *.log 5 | .idea/ 6 | .cache/ 7 | build 8 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - deploy 4 | 5 | buildImage: 6 | stage: build 7 | tags: 8 | - dockerbuilder 9 | script: 10 | - docker build -t bptlab/chor-js-demo:master . 11 | - docker push bptlab/chor-js-demo:master 12 | 13 | deployOnDockerHost: 14 | stage: deploy 15 | only: 16 | - master 17 | image: alpine:3.12 18 | before_script: 19 | - apk update 20 | # configure ssh tools 21 | - 'which ssh-agent || ( apk add openssh-client )' 22 | - mkdir -p ~/.ssh 23 | - eval $(ssh-agent -s) 24 | # take care to deploy private key in GitLab as corresponding env var 25 | - echo "$identitykey" | ssh-add - 26 | script: 27 | - ssh -o StrictHostKeyChecking=no $deployuser@$targethost '/var/docker-routines/chor-js-demo/deploy.sh' 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | script: 10 | - npm run lint 11 | - npm run build -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 as builder 2 | 3 | WORKDIR /usr/src 4 | # copy both package and package-lock 5 | COPY package*.json ./ 6 | 7 | COPY app ./app 8 | RUN npm install 9 | RUN npm run build 10 | 11 | # copy built files over from first stage 12 | FROM node:current-alpine 13 | EXPOSE 9013 14 | COPY --from=builder /usr/src/build /usr/src/build 15 | WORKDIR /usr/src 16 | RUN npm install http-server -g 17 | CMD http-server ./build -p 9013 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chor-js-demo 2 | 3 | __[:rocket: Live Version :rocket:](https://bpt-lab.org/chor-js-demo/)__ 4 | 5 | A simple demo application showing the usage of the _npm package_ of [`chor-js`](https://github.com/bptlab/chor-js) to view and edit BPMN 2.0 choreography diagrams in the browser. 6 | 7 | The demo also adds some features such as diagram upload and download, and a [validator](./app/lib/validator). 8 | 9 | > For an example on how to use the pre-packaged version of chor-js, please refer to the [README there](https://github.com/bptlab/chor-js). 10 | 11 | ## Local Usage 12 | 13 | ### Node 14 | 15 | You can install and run the demo locally using Node.js. 16 | 17 | #### Run Only 18 | 19 | ```shell 20 | npm install 21 | npm run dev 22 | ``` 23 | 24 | You can also build it using `npm run build`. 25 | 26 | The demo is then served to `http://localhost:9013`. 27 | We use [Parcel](https://parceljs.org) as a build tool. 28 | Thus, unless you set up the project as a development environment (see below), chor-js will not be transpiled and polyfilled, which should be no problem for modern browsers. 29 | 30 | #### Development Environment 31 | 32 | If you want to use the demo while developing [chor-js](https://github.com/bptlab/chor-js), you can link the two repositories: 33 | 34 | ```shell 35 | git clone https://github.com/bptlab/chor-js.git 36 | cd chor-js 37 | npm install 38 | npm link 39 | 40 | cd .. 41 | git clone https://github.com/bptlab/chor-js-demo.git 42 | cd chor-js-demo 43 | npm install 44 | npm link chor-js 45 | npm run dev 46 | ``` 47 | 48 | ### Docker 49 | 50 | We also provide a `Dockerfile` to use with Docker. 51 | 52 | ```shell 53 | docker build . -t chor-js-demo 54 | docker run --rm -p 9013:9013 --name chor-js-demo -it chor-js-demo 55 | ``` 56 | 57 | The demo is then served to `http://localhost:9013` as a production build using the latest version of chor-js (see Dockerfile). 58 | 59 | ## License 60 | 61 | MIT 62 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import ChoreoModeler from 'chor-js/lib/Modeler'; 2 | import PropertiesPanelModule from 'bpmn-js-properties-panel'; 3 | 4 | import Reporter from './lib/validator/Validator.js'; 5 | import PropertiesProviderModule from './lib/properties-provider'; 6 | 7 | import xml from './diagrams/pizzaDelivery.bpmn'; 8 | import blankXml from './diagrams/newDiagram.bpmn'; 9 | 10 | let lastFile; 11 | let isValidating = false; 12 | let isDirty = false; 13 | 14 | // create and configure a chor-js instance 15 | const modeler = new ChoreoModeler({ 16 | container: '#canvas', 17 | propertiesPanel: { 18 | parent: '#properties-panel' 19 | }, 20 | // remove the properties' panel if you use the Viewer 21 | // or NavigatedViewer modules of chor-js 22 | additionalModules: [ 23 | PropertiesPanelModule, 24 | PropertiesProviderModule 25 | ], 26 | keyboard: { 27 | bindTo: document 28 | } 29 | }); 30 | 31 | // display the given model (XML representation) 32 | async function renderModel(newXml) { 33 | await modeler.importXML(newXml); 34 | isDirty = false; 35 | } 36 | 37 | // returns the file name of the diagram currently being displayed 38 | function diagramName() { 39 | if (lastFile) { 40 | return lastFile.name; 41 | } 42 | return 'diagram.bpmn'; 43 | } 44 | 45 | document.addEventListener('DOMContentLoaded', () => { 46 | // download diagram as XML 47 | const downloadLink = document.getElementById('js-download-diagram'); 48 | downloadLink.addEventListener('click', async e => { 49 | const result = await modeler.saveXML({ format: true }); 50 | downloadLink['href'] = 'data:application/bpmn20-xml;charset=UTF-8,' + encodeURIComponent(result.xml); 51 | downloadLink['download'] = diagramName(); 52 | isDirty = false; 53 | }); 54 | 55 | // download diagram as SVG 56 | const downloadSvgLink = document.getElementById('js-download-svg'); 57 | downloadSvgLink.addEventListener('click', async e => { 58 | const result = await modeler.saveSVG(); 59 | downloadSvgLink['href'] = 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(result.svg); 60 | downloadSvgLink['download'] = diagramName() + '.svg'; 61 | }); 62 | 63 | // open file dialog 64 | document.getElementById('js-open-file').addEventListener('click', e => { 65 | document.getElementById('file-input').click(); 66 | }); 67 | 68 | // toggle side panels 69 | const panels = Array.prototype.slice.call( 70 | document.getElementById('panel-toggle').children 71 | ); 72 | panels.forEach(panel => { 73 | panel.addEventListener('click', () => { 74 | panels.forEach(otherPanel => { 75 | if (panel === otherPanel && !panel.classList.contains('active')) { 76 | // show clicked panel if it is not already active, otherwise hide it as well 77 | panel.classList.add('active'); 78 | document.getElementById(panel.dataset.togglePanel).classList.remove('hidden'); 79 | } else { 80 | // hide all other panels 81 | otherPanel.classList.remove('active'); 82 | document.getElementById(otherPanel.dataset.togglePanel).classList.add('hidden'); 83 | } 84 | }); 85 | }); 86 | }); 87 | 88 | // create new diagram 89 | const newDiagram = document.getElementById('js-new-diagram'); 90 | newDiagram.addEventListener('click', async e => { 91 | await renderModel(blankXml); 92 | lastFile = false; 93 | }); 94 | 95 | // load diagram from disk 96 | const loadDiagram = document.getElementById('file-input'); 97 | loadDiagram.addEventListener('change', e => { 98 | const file = loadDiagram.files[0]; 99 | if (file) { 100 | const reader = new FileReader(); 101 | lastFile = file; 102 | reader.addEventListener('load', async () => { 103 | await renderModel(reader.result); 104 | loadDiagram.value = null; // allows reloading the same file 105 | }, false); 106 | reader.readAsText(file); 107 | } 108 | }); 109 | 110 | // drag & drop file 111 | const dropZone = document.body; 112 | dropZone.addEventListener('dragover', e => { 113 | e.preventDefault(); 114 | dropZone.classList.add('is-dragover'); 115 | }); 116 | dropZone.addEventListener('dragleave', e => { 117 | e.preventDefault(); 118 | dropZone.classList.remove('is-dragover'); 119 | }); 120 | dropZone.addEventListener('drop', e => { 121 | e.preventDefault(); 122 | dropZone.classList.remove('is-dragover'); 123 | const file = e.dataTransfer.files[0]; 124 | if (file) { 125 | const reader = new FileReader(); 126 | lastFile = file; 127 | reader.addEventListener('load', () => { 128 | renderModel(reader.result); 129 | }, false); 130 | reader.readAsText(file); 131 | } 132 | }); 133 | 134 | // validation logic and toggle 135 | const reporter = new Reporter(modeler); 136 | const validateButton = document.getElementById('js-validate'); 137 | validateButton.addEventListener('click', e => { 138 | isValidating = !isValidating; 139 | if (isValidating) { 140 | reporter.validateDiagram(); 141 | validateButton.classList.add('selected'); 142 | validateButton['title'] = 'Disable checking'; 143 | } else { 144 | reporter.clearAll(); 145 | validateButton.classList.remove('selected'); 146 | validateButton['title'] = 'Check diagram for problems'; 147 | } 148 | }); 149 | modeler.on('commandStack.changed', () => { 150 | if (isValidating) { 151 | reporter.validateDiagram(); 152 | } 153 | isDirty = true; 154 | }); 155 | modeler.on('import.render.complete', () => { 156 | if (isValidating) { 157 | reporter.validateDiagram(); 158 | } 159 | }); 160 | }); 161 | 162 | // expose bpmnjs to window for debugging purposes 163 | window.bpmnjs = modeler; 164 | 165 | window.addEventListener('beforeunload', function(e) { 166 | if (isDirty) { 167 | // see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload 168 | e.preventDefault(); 169 | e.returnValue = ''; 170 | } 171 | }); 172 | 173 | renderModel(xml); 174 | -------------------------------------------------------------------------------- /app/diagrams/EventBasedGateway.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | legal 29 | 30 | 31 | 32 | legal 33 | 34 | 35 | 36 | illegal 37 | 38 | 39 | 40 | illegal 41 | 42 | 43 | 44 | SequenceFlow_1c4wf17 45 | 46 | 47 | SequenceFlow_1c4wf17 48 | SequenceFlow_1lbfc23 49 | SequenceFlow_02087ww 50 | 51 | 52 | 53 | SequenceFlow_1lbfc23 54 | Participant_1isukd8 55 | Participant_124muor 56 | MessageFlow_0dc6hmu 57 | 58 | 59 | 60 | SequenceFlow_02087ww 61 | Participant_0jezgqe 62 | Participant_1isukd8 63 | MessageFlow_0eiw5dl 64 | 65 | 66 | 67 | SequenceFlow_0j0dqfm 68 | 69 | 70 | SequenceFlow_0j0dqfm 71 | SequenceFlow_00jp6wv 72 | SequenceFlow_1yii3fr 73 | 74 | 75 | SequenceFlow_00jp6wv 76 | Participant_0jezgqe 77 | Participant_124muor 78 | MessageFlow_0r68mz5 79 | 80 | 81 | SequenceFlow_1yii3fr 82 | Participant_0jezgqe 83 | Participant_1isukd8 84 | MessageFlow_0ad17b6 85 | 86 | 87 | 88 | 89 | 90 | SequenceFlow_1sd0oqv 91 | 92 | 93 | SequenceFlow_1sd0oqv 94 | SequenceFlow_1cov845 95 | SequenceFlow_12hef02 96 | 97 | 98 | SequenceFlow_1cov845 99 | Participant_0jezgqe 100 | Participant_124muor 101 | MessageFlow_0ocgtn8 102 | 103 | 104 | SequenceFlow_12hef02 105 | Participant_124muor 106 | Participant_1isukd8 107 | MessageFlow_1srv3ec 108 | 109 | 110 | 111 | 112 | 113 | SequenceFlow_1urrpzv 114 | 115 | 116 | SequenceFlow_1sw62kj 117 | SequenceFlow_04jomg4 118 | SequenceFlow_1i3d4j0 119 | 120 | 121 | SequenceFlow_04jomg4 122 | Participant_1isukd8 123 | Participant_124muor 124 | MessageFlow_07qn4nx 125 | 126 | 127 | SequenceFlow_1i3d4j0 128 | Participant_0jezgqe 129 | Participant_1isukd8 130 | MessageFlow_00wphp8 131 | 132 | 133 | 134 | 135 | SequenceFlow_1urrpzv 136 | SequenceFlow_1sw62kj 137 | Participant_1a04puk 138 | Participant_1isukd8 139 | MessageFlow_02o85v4 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | -------------------------------------------------------------------------------- /app/diagrams/ManyErrorsAndWarnings.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | SequenceFlow_04tj6o0 31 | SequenceFlow_1iybzm8 32 | Participant_0jezgqe 33 | Participant_1isukd8 34 | 35 | SequenceFlow_18sxbve 36 | SequenceFlow_168agkb 37 | Participant_124muor 38 | Participant_1isukd8 39 | MessageFlow_0nost86 40 | 41 | 42 | SequenceFlow_12b2b03 43 | SequenceFlow_1gmotny 44 | Participant_1a04puk 45 | Participant_0jezgqe 46 | MessageFlow_1m2evxt 47 | MessageFlow_1ylyir1 48 | 49 | 50 | SequenceFlow_18sxbve 51 | 52 | 53 | 54 | SequenceFlow_1qaxlut 55 | 56 | 57 | SequenceFlow_1gmotny 58 | SequenceFlow_1qaxlut 59 | Participant_124muor 60 | Participant_1isukd8 61 | MessageFlow_17gh7ts 62 | 63 | 64 | 65 | 66 | SequenceFlow_168agkb 67 | SequenceFlow_12b2b03 68 | 69 | 70 | 71 | 72 | 73 | 74 | SequenceFlow_1iybzm8 75 | Participant_124muor 76 | Participant_1isukd8 77 | MessageFlow_08uc6pw 78 | 79 | 80 | 81 | Participant_0jezgqe 82 | Participant_1isukd8 83 | 84 | SequenceFlow_1hwnh6h 85 | SequenceFlow_0xgktu9 86 | Participant_124muor 87 | Participant_1isukd8 88 | MessageFlow_1mtov6b 89 | 90 | 91 | SequenceFlow_1hwnh6h 92 | 93 | 94 | SequenceFlow_0xgktu9 95 | SequenceFlow_0ivfzg6 96 | 97 | 98 | 99 | SequenceFlow_0ivfzg6 100 | SequenceFlow_0hb20f3 101 | Participant_1by7wp9 102 | Participant_0jezgqe 103 | MessageFlow_1nv6vrb 104 | MessageFlow_1gjxdkn 105 | 106 | 107 | SequenceFlow_0hb20f3 108 | SequenceFlow_1f7qlm1 109 | Participant_124muor 110 | Participant_1isukd8 111 | MessageFlow_085mwey 112 | 113 | 114 | SequenceFlow_1f7qlm1 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | SequenceFlow_04tj6o0 124 | Participant_124muor 125 | Participant_1isukd8 126 | MessageFlow_0nwc7sx 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /app/diagrams/multiple.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | SequenceFlow_1exndm5 16 | 17 | 18 | SequenceFlow_1exndm5 19 | SequenceFlow_09um5u6 20 | Participant_0bln1au 21 | Participant_0a8eu9l 22 | MessageFlow_0e0tqvf 23 | 24 | 25 | 26 | SequenceFlow_09um5u6 27 | SequenceFlow_1bdfjst 28 | Participant_0a8eu9l 29 | Participant_0bln1au 30 | MessageFlow_1ylcbke 31 | 32 | 33 | 34 | SequenceFlow_1bdfjst 35 | SequenceFlow_0b9zcqb 36 | SequenceFlow_1gw3sl2 37 | 38 | 39 | 40 | 41 | SequenceFlow_1gw3sl2 42 | 43 | 44 | 45 | SequenceFlow_0b9zcqb 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | SequenceFlow_1 63 | SequenceFlow_2 64 | Participant_1 65 | Participant_2 66 | MessageFlow_1 67 | 68 | 69 | SequenceFlow_2 70 | SequenceFlow_3 71 | Participant_3 72 | Participant_2 73 | Participant_5 74 | Participant_4 75 | Participant_1 76 | 77 | 78 | Participant_3 79 | Participant_2 80 | Participant_5 81 | Participant_4 82 | Participant_1 83 | 84 | 85 | SequenceFlow_1 86 | 87 | 88 | 89 | 90 | SequenceFlow_3 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /app/diagrams/newDiagram.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/diagrams/pizzaDelivery.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | SequenceFlow_0dy5er9 15 | 16 | 17 | SequenceFlow_0dy5er9 18 | SequenceFlow_036yo13 19 | Participant_06qakjv 20 | Participant_0mgz9si 21 | MessageFlow_0nzo41l 22 | 23 | 24 | 25 | SequenceFlow_036yo13 26 | SequenceFlow_0esuuaj 27 | Participant_0mgz9si 28 | Participant_0537iz2 29 | MessageFlow_0dnembp 30 | 31 | 32 | 33 | SequenceFlow_0esuuaj 34 | SequenceFlow_0xamnn2 35 | Participant_0537iz2 36 | Participant_06qakjv 37 | MessageFlow_03drbju 38 | 39 | 40 | 41 | SequenceFlow_0xamnn2 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 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /app/diagrams/subChoreographies.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | SequenceFlow_1 20 | SequenceFlow_2 21 | Participant_1 22 | Participant_2 23 | MessageFlow_1 24 | 25 | 26 | SequenceFlow_2 27 | SequenceFlow_3 28 | Participant_3 29 | Participant_2 30 | Participant_5 31 | Participant_4 32 | Participant_1 33 | 34 | 35 | SequenceFlow_1 36 | 37 | 38 | 39 | 40 | SequenceFlow_3 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 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /app/diagrams/subChoreographies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptlab/chor-js-demo/6b1c04b329d69618af8002b5b3afdca0c8c15d91/app/diagrams/subChoreographies.png -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptlab/chor-js-demo/6b1c04b329d69618af8002b5b3afdca0c8c15d91/app/favicon.ico -------------------------------------------------------------------------------- /app/icons/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Entypo 5 | 6 | Copyright (C) 2012 by Daniel Bruce 7 | 8 | Author: Daniel Bruce 9 | License: SIL (http://scripts.sil.org/OFL) 10 | Homepage: http://www.entypo.com 11 | 12 | 13 | ## Elusive 14 | 15 | Copyright (C) 2013 by Aristeides Stathopoulos 16 | 17 | Author: Aristeides Stathopoulos 18 | License: SIL (http://scripts.sil.org/OFL) 19 | Homepage: http://aristeides.com/ 20 | 21 | 22 | ## Font Awesome 23 | 24 | Copyright (C) 2016 by Dave Gandy 25 | 26 | Author: Dave Gandy 27 | License: SIL () 28 | Homepage: http://fortawesome.github.com/Font-Awesome/ 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/icons/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chor-editor", 3 | "css_prefix_text": "icon-", 4 | "css_use_suffix": false, 5 | "hinting": true, 6 | "units_per_em": 1000, 7 | "ascent": 850, 8 | "glyphs": [ 9 | { 10 | "uid": "0ccb084ddeeae372673793ed0b45bb4a", 11 | "css": "folder", 12 | "code": 59393, 13 | "src": "entypo" 14 | }, 15 | { 16 | "uid": "9e0404ba55575a540164db9a5ad511df", 17 | "css": "doc-new", 18 | "code": 59394, 19 | "src": "elusive" 20 | }, 21 | { 22 | "uid": "26613a2e6bc41593c54bead46f8c8ee3", 23 | "css": "file-code", 24 | "code": 61897, 25 | "src": "fontawesome" 26 | }, 27 | { 28 | "uid": "3c961c1a8d874815856fc6637dc5a13c", 29 | "css": "file-image", 30 | "code": 61893, 31 | "src": "fontawesome" 32 | }, 33 | { 34 | "uid": "7432077e6a2d6aa19984ca821bb6bbda", 35 | "css": "bug", 36 | "code": 61832, 37 | "src": "fontawesome" 38 | }, 39 | { 40 | "uid": "0f6a2573a7b6df911ed199bb63717e27", 41 | "css": "github-circled", 42 | "code": 61595, 43 | "src": "fontawesome" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /app/icons/css/chor-editor.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'chor-editor'; 3 | src: url('../font/chor-editor.eot?66206553'); 4 | src: url('../font/chor-editor.eot?66206553#iefix') format('embedded-opentype'), 5 | url('../font/chor-editor.woff2?66206553') format('woff2'), 6 | url('../font/chor-editor.woff?66206553') format('woff'), 7 | url('../font/chor-editor.ttf?66206553') format('truetype'), 8 | url('../font/chor-editor.svg?66206553#chor-editor') format('svg'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 13 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 14 | /* 15 | @media screen and (-webkit-min-device-pixel-ratio:0) { 16 | @font-face { 17 | font-family: 'chor-editor'; 18 | src: url('../font/chor-editor.svg?66206553#chor-editor') format('svg'); 19 | } 20 | } 21 | */ 22 | 23 | [class^="icon-"]:before, [class*=" icon-"]:before { 24 | font-family: "chor-editor"; 25 | font-style: normal; 26 | font-weight: normal; 27 | speak: none; 28 | 29 | display: inline-block; 30 | text-decoration: inherit; 31 | width: 1em; 32 | text-align: center; 33 | /* opacity: .8; */ 34 | 35 | /* For safety - reset parent styles, that can break glyph codes*/ 36 | font-variant: normal; 37 | text-transform: none; 38 | 39 | /* fix buttons height, for twitter bootstrap */ 40 | line-height: 1em; 41 | 42 | /* you can be more comfortable with increased icons size */ 43 | /* font-size: 120%; */ 44 | 45 | /* Font smoothing. That was taken from TWBS */ 46 | -webkit-font-smoothing: antialiased; 47 | -moz-osx-font-smoothing: grayscale; 48 | 49 | /* Uncomment for 3D effect */ 50 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 51 | } 52 | 53 | .icon-folder:before { content: '\e801'; } /* '' */ 54 | .icon-doc-new:before { content: '\e802'; } /* '' */ 55 | .icon-github-circled:before { content: '\f09b'; } /* '' */ 56 | .icon-bug:before { content: '\f188'; } /* '' */ 57 | .icon-file-image:before { content: '\f1c5'; } /* '' */ 58 | .icon-file-code:before { content: '\f1c9'; } /* '' */ -------------------------------------------------------------------------------- /app/icons/font/chor-editor.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptlab/chor-js-demo/6b1c04b329d69618af8002b5b3afdca0c8c15d91/app/icons/font/chor-editor.eot -------------------------------------------------------------------------------- /app/icons/font/chor-editor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2019 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/icons/font/chor-editor.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptlab/chor-js-demo/6b1c04b329d69618af8002b5b3afdca0c8c15d91/app/icons/font/chor-editor.ttf -------------------------------------------------------------------------------- /app/icons/font/chor-editor.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptlab/chor-js-demo/6b1c04b329d69618af8002b5b3afdca0c8c15d91/app/icons/font/chor-editor.woff -------------------------------------------------------------------------------- /app/icons/font/chor-editor.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bptlab/chor-js-demo/6b1c04b329d69618af8002b5b3afdca0c8c15d91/app/icons/font/chor-editor.woff2 -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | [chor-js] Demo 19 | 20 | 21 | 22 |
23 |
24 | 32 |
33 |
34 |
Properties
35 |
36 | 37 |
38 | 39 |
40 | 41 | 42 |
43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/lib/properties-provider/ChorPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | import BpmnPropertiesProvider from 'bpmn-js-properties-panel/lib/provider/bpmn/BpmnPropertiesProvider.js'; 2 | import inherits from 'inherits'; 3 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 4 | import cmdHelper from 'bpmn-js-properties-panel/lib/helper/CmdHelper'; 5 | import entryFactory from 'bpmn-js-properties-panel/lib/factory/EntryFactory'; 6 | import eventDefinitionHelper from 'bpmn-js-properties-panel/lib/helper/EventDefinitionHelper'; 7 | import conditionalProps from 'bpmn-js-properties-panel/lib/provider/camunda/parts/ConditionalProps.js'; 8 | import messageDefinition from './MessageDefinition'; 9 | 10 | export default function ChorPropertiesProvider(injector, bpmnFactory) { 11 | 12 | injector.invoke(BpmnPropertiesProvider, this); 13 | 14 | const superGetTabs = this.getTabs; 15 | 16 | this.getTabs = function(element) { 17 | let generalTab = superGetTabs.call(this, element); 18 | const detailsGroup = generalTab[0].groups.filter(g => g.id === 'details')[0]; 19 | if (is(element, 'bpmn:Event')) { 20 | // Conditional Events show Camunda specific options, we have to filter those 21 | if (element.businessObject.eventDefinitions) { 22 | const definition = element.businessObject.eventDefinitions[0]; 23 | if (definition.$type === 'bpmn:ConditionalEventDefinition') { 24 | detailsGroup.entries = []; 25 | this.conditionalEvent(detailsGroup, element); 26 | } 27 | } 28 | } 29 | conditionalProps(detailsGroup, element, bpmnFactory, e => e); 30 | if (is(element, 'bpmn:Message')) { 31 | messageDefinition(detailsGroup, element, bpmnFactory, element.businessObject); 32 | } 33 | return generalTab; 34 | }; 35 | 36 | ChorPropertiesProvider.prototype.conditionalEvent = function(group, element) { 37 | const getValue = function(conditionalEvent, node) { 38 | 39 | const conditionalEventDefinition = eventDefinitionHelper.getConditionalEventDefinition(conditionalEvent); 40 | return { 41 | condition: conditionalEventDefinition.condition.body 42 | }; 43 | }; 44 | 45 | const setValue = function(conditionalEvent, values) { 46 | 47 | const conditionalEventDefinition = eventDefinitionHelper.getConditionalEventDefinition(conditionalEvent); 48 | const condition = conditionalEventDefinition.condition; 49 | 50 | return cmdHelper.updateBusinessObject(conditionalEvent, condition, { body: values.condition }); 51 | }; 52 | 53 | group.entries.push(entryFactory.textField({ 54 | id: 'condition', 55 | label: 'Condition Expression', 56 | modelProperty: 'condition', 57 | 58 | get: getValue, 59 | set: setValue 60 | })); 61 | }; 62 | 63 | } 64 | 65 | inherits(ChorPropertiesProvider, BpmnPropertiesProvider); 66 | ChorPropertiesProvider.$inject = [ 67 | 'injector', 68 | 'bpmnFactory' 69 | ]; 70 | 71 | -------------------------------------------------------------------------------- /app/lib/properties-provider/MessageDefinition.js: -------------------------------------------------------------------------------- 1 | import eventDefinitionReference from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/implementation/EventDefinitionReference'; 2 | import elementReferenceProperty from 'bpmn-js-properties-panel/lib/provider/bpmn/parts/implementation/ElementReferenceProperty'; 3 | import entryFactory from 'bpmn-js-properties-panel/lib/factory/EntryFactory'; 4 | import cmdHelper from 'bpmn-js-properties-panel/lib/helper/CmdHelper'; 5 | 6 | export default function MessageDefinition(group, element, bpmnFactory, messageEventDefinition) { 7 | 8 | // Technically, the eventDefinitionReference function is only meant for events, however, it works well for our purpose, too. 9 | // I currently see no reason to duplicate ~100 lines of code, however, it might break in future versions of the panel-provider 10 | group.entries = group.entries.concat(eventDefinitionReference(element, messageEventDefinition, bpmnFactory, { 11 | label: 'Item Definition', 12 | elementName: 'item-def', 13 | elementType: 'bpmn:ItemDefinition', 14 | referenceProperty: 'itemRef', 15 | newElementIdPrefix: 'ItemDef_' 16 | })); 17 | 18 | group.entries = group.entries.concat(elementReferenceProperty(element, messageEventDefinition, bpmnFactory, { 19 | id: 'Item-Def-element-name', 20 | label: 'Item Definition Name', 21 | referenceProperty: 'itemRef', 22 | modelProperty: 'name', 23 | shouldValidate: false 24 | })); 25 | 26 | group.entries = group.entries.concat(createStructureRefTextField()); 27 | 28 | } 29 | 30 | function createStructureRefTextField() { 31 | const modelProperty = 'structureRef'; 32 | let entry = entryFactory.textField({ 33 | id: 'structure-ref', 34 | label: 'Data Structure', 35 | modelProperty: modelProperty, 36 | 37 | get: function(element, node) { 38 | var reference = element.businessObject.itemRef; 39 | var props = {}; 40 | props[modelProperty] = reference && reference.get(modelProperty); 41 | return props; 42 | }, 43 | 44 | set: function(element, values, node) { 45 | var reference = element.businessObject.itemRef; 46 | var props = {}; 47 | props[modelProperty] = values[modelProperty] || undefined; 48 | return cmdHelper.updateBusinessObject(element, reference, props); 49 | }, 50 | 51 | hidden: function(element, node) { 52 | return !element.businessObject.itemRef; 53 | } 54 | }); 55 | return [ entry ]; 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/lib/properties-provider/index.js: -------------------------------------------------------------------------------- 1 | import ChorPropertiesProvider from './ChorPropertiesProvider'; 2 | 3 | module.exports = { 4 | __init__: [ 'propertiesProvider' ], 5 | propertiesProvider: [ 'type', ChorPropertiesProvider] 6 | }; -------------------------------------------------------------------------------- /app/lib/validator/Validator.js: -------------------------------------------------------------------------------- 1 | import { heightOfTopBands } from 'chor-js/lib/util/BandUtil'; 2 | 3 | import { 4 | isChoreoActivity 5 | } from './util/ValidatorUtil'; 6 | 7 | import eventBasedGatewayConstraint from './constraints/EventBasedGatewayConstraint'; 8 | import participantNameConstraint from './constraints/ParticipantNameConstraint'; 9 | import simpleFlowConstraint from './constraints/SimpleFlowConstraint'; 10 | import subChoreoParticipantsConstraint from './constraints/SubChoreoParticipantsConstraint'; 11 | import callChoreoParticipantsBijectivityConstraint from './constraints/CallChoreoParticipantsBijectivityConstraint'; 12 | import noCyclicCallChoreosConstraint from './constraints/NoCyclicCallChoreosConstraint'; 13 | 14 | const CONSTRAINTS = [ 15 | eventBasedGatewayConstraint, 16 | participantNameConstraint, 17 | simpleFlowConstraint, 18 | subChoreoParticipantsConstraint, 19 | callChoreoParticipantsBijectivityConstraint, 20 | noCyclicCallChoreosConstraint, 21 | 22 | ]; 23 | 24 | const LEVEL = { 25 | WARNING: 0, 26 | ERROR: 1 27 | }; 28 | 29 | const LEVEL_STRING = [ 30 | 'warning', 31 | 'error' 32 | ]; 33 | 34 | export default function Reporter(viewer) { 35 | this.overlays = viewer.get('overlays'); 36 | this.elementRegistry = viewer.get('elementRegistry'); 37 | this.annotations = []; // List of all annotations 38 | this.shapeAnnotations = {}; // Mapping from shape id to annotations 39 | this.overlayIDs = []; 40 | } 41 | 42 | Reporter.prototype.validateDiagram = function() { 43 | this.clearAll(); 44 | this.elementRegistry.forEach(shape => CONSTRAINTS.forEach(constraint => constraint(shape, this))); 45 | this.showAnnotations(); 46 | }; 47 | 48 | Reporter.prototype.addAnnotationToShape = function(shape, annotation) { 49 | if (this.shapeAnnotations[shape.id]) { 50 | this.shapeAnnotations[shape.id].push(annotation); 51 | } else { 52 | this.shapeAnnotations[shape.id] = [ annotation ]; 53 | } 54 | }; 55 | 56 | Reporter.prototype.report = function(shape, level, text) { 57 | this.annotations.push({ level: level, text: text, shape: shape }); 58 | }; 59 | 60 | Reporter.prototype.warn = function(shape, text) { 61 | this.report(shape, LEVEL.WARNING, text); 62 | }; 63 | 64 | Reporter.prototype.error = function(shape, text) { 65 | this.report(shape, LEVEL.ERROR, text); 66 | }; 67 | 68 | Reporter.prototype.showAnnotations = function() { 69 | function findVisibleParent(shape) { 70 | while (shape.hidden) { 71 | shape = shape.parent; 72 | } 73 | return shape; 74 | } 75 | 76 | this.annotations.forEach(annotation => this.addAnnotationToShape( 77 | findVisibleParent(annotation.shape), 78 | annotation 79 | )); 80 | Object.keys(this.shapeAnnotations).forEach(id => { 81 | const shape = this.elementRegistry.get(id); 82 | this.displayOnShape(shape, this.shapeAnnotations[id]); 83 | }); 84 | }; 85 | 86 | /** 87 | * Display a list of annotations on their shape. 88 | * @param annotations {Array} 89 | */ 90 | Reporter.prototype.displayOnShape = function(shape, annotations) { 91 | let childAnnotations = annotations.filter(annotation => annotation.shape !== shape); 92 | let parentAnnotations = annotations 93 | .filter(annotation => annotation.shape === shape) 94 | .sort((a,b) => b.level - a.level); 95 | 96 | const topOffset = isChoreoActivity(shape) ? heightOfTopBands(shape) : -7; 97 | const level = Math.max(...annotations.map(annotation => annotation.level)); 98 | const count = annotations.length; 99 | 100 | let infoText = ''; 101 | parentAnnotations.forEach(annotation => { 102 | infoText += '
  • ' + annotation.text + '
  • '; 103 | }); 104 | if (childAnnotations.length > 0) { 105 | let errorCount = childAnnotations.filter(annotation => annotation.level === LEVEL.ERROR).length; 106 | let warningCount = childAnnotations.filter(annotation => annotation.level === LEVEL.WARNING).length; 107 | infoText += '
  • '; 108 | if (errorCount > 0) { 109 | infoText += '' + errorCount + ' nested error'; 110 | if (errorCount > 1) { 111 | infoText += 's'; 112 | } 113 | if (warningCount > 0) { 114 | infoText += ','; 115 | } 116 | } 117 | if (warningCount > 0) { 118 | infoText += warningCount + ' nested warning'; 119 | if (warningCount > 1) { 120 | infoText += 's'; 121 | } 122 | } 123 | infoText += '
  • '; 124 | } 125 | 126 | let html = '
    '; 127 | if (count > 1) { 128 | html += '
    ' + count + '
    '; 129 | } 130 | html += ''; 131 | html += '
    '; 132 | 133 | const newOverlayId = this.overlays.add(shape.id, { 134 | position: { 135 | top: topOffset, 136 | left: shape.width - 12 137 | }, 138 | html: html 139 | }); 140 | this.overlayIDs.push(newOverlayId); 141 | }; 142 | 143 | Reporter.prototype.clearAll = function() { 144 | this.overlayIDs.forEach(id => this.overlays.remove(id)); 145 | this.shapeAnnotations = {}; 146 | this.annotations = []; 147 | }; 148 | -------------------------------------------------------------------------------- /app/lib/validator/constraints/CallChoreoParticipantsBijectivityConstraint.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | /** 4 | * The mapping between the CallChoreo's participants and the participants of the called choreography should be bijective. 5 | * @param shape 6 | * @param reporter {Reporter} 7 | */ 8 | export default function callChoreoParticipantsBijectivityConstraint(shape, reporter) { 9 | if (is(shape, 'bpmn:CallChoreography') && shape.businessObject.calledChoreographyRef) { 10 | const callChoreoBO = shape.businessObject; 11 | const bandShapes = shape.bandShapes || []; 12 | const participantAssociations = callChoreoBO.participantAssociations || []; 13 | 14 | bandShapes.forEach(band => { 15 | const association = participantAssociations.find(p => p.outerParticipantRef === band.businessObject); 16 | const bandsInnerParticipant = association ? association.innerParticipantRef : undefined; 17 | checkFunctionProperty(band, bandsInnerParticipant, reporter); 18 | checkInjectivityProperty(band, bandsInnerParticipant, participantAssociations, reporter); 19 | }); 20 | 21 | checkSurjectivityProperty(participantAssociations, callChoreoBO, shape, reporter); 22 | } 23 | } 24 | 25 | 26 | function checkFunctionProperty(band, bandsInnerParticipant, reporter) { 27 | if (!bandsInnerParticipant) { 28 | reporter.warn(band, '' + band.businessObject.name + ' ' + 29 | 'does not yet reference an inner participant'); 30 | } 31 | } 32 | 33 | function checkInjectivityProperty(band, bandsInnerParticipant, participantAssociations, reporter) { 34 | const injectivityViolators = participantAssociations 35 | .filter(pa => pa.innerParticipantRef === bandsInnerParticipant && pa.outerParticipantRef !== band.businessObject) 36 | .map(pa => pa.outerParticipantRef); 37 | 38 | if (injectivityViolators.length > 0) { 39 | const violatorString = injectivityViolators.map(bo => bo.name).join(', '); 40 | reporter.warn(band, '' + band.businessObject.name + ' references the same inner participant as ' 41 | + violatorString + ''); 42 | } 43 | } 44 | 45 | function checkSurjectivityProperty(participantAssociations, callChoreoBO, shape, reporter) { 46 | const participantCodomain = callChoreoBO.calledChoreographyRef.participants || []; 47 | const surjectivityViolators = participantCodomain 48 | .filter(p => !participantAssociations.some(a => a.innerParticipantRef === p)); 49 | if (surjectivityViolators.length > 0) { 50 | const violationString = surjectivityViolators.map(bo => bo.name).join(', '); 51 | const calleeName = callChoreoBO.calledChoreographyRef.name || callChoreoBO.calledChoreographyRef.id; 52 | reporter.warn(shape, 53 | '' + callChoreoBO.name + ' does not reference participants ' 54 | + violationString + ' of ' + calleeName + ''); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/lib/validator/constraints/EventBasedGatewayConstraint.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | import { 4 | getConnectedElements, 5 | isInitiating, 6 | isChoreoActivity, 7 | flat 8 | } from '../util/ValidatorUtil'; 9 | 10 | /** 11 | * Checks rules for event-based gateways. See chapter 11.7.2 12 | * @param shape 13 | * @param reporter {Reporter} 14 | */ 15 | export default function eventBasedGatewayConstraint(shape, reporter) { 16 | if (is(shape, 'bpmn:EventBasedGateway')) { 17 | const following = getConnectedElements(shape, 'outgoing', isChoreoActivity); 18 | const senders = flat(following.map(a => a.bandShapes.filter(p => isInitiating(p)))); 19 | const receivers = flat(following.map(a => a.bandShapes.filter(p => !isInitiating(p)))); 20 | if (!(senders.every((s, i, a) => a[0].businessObject.id === s.businessObject.id) || 21 | receivers.every((r, i, a) => a[0].businessObject.id === r.businessObject.id))) { 22 | reporter.error(shape, 'After an event-based gateway, all senders or all receivers must be the same'); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/lib/validator/constraints/NoCyclicCallChoreosConstraint.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { getAllElementsOfType } from 'chor-js/lib/util/DiagramWalkerUtil'; 3 | 4 | /** 5 | * Call choreographies might lead to cyclic calls. This can be viable, but a warning should be issued as this 6 | * can lead to infinite recursions. 7 | * 8 | * @param shape 9 | * @param reporter {Reporter} 10 | */ 11 | export default function noCyclicCallChoreosConstraint(shape, reporter) { 12 | if (is(shape, 'bpmn:CallChoreography')) { 13 | const callChoreoBO = shape.businessObject; 14 | const calledChoreo = callChoreoBO.calledChoreographyRef; 15 | const parentChoreography = getContainingChoreo(callChoreoBO); 16 | const cycles = dfs(calledChoreo, [parentChoreography], [callChoreoBO.name]); 17 | 18 | for (const cycle of cycles) { 19 | const nodes = cycle.nodes; 20 | const edges = cycle.edges; 21 | const cycleString = nodes.map(choreo => (choreo.name || choreo.id)) 22 | .map((name,i) => { 23 | if (i < edges.length) { 24 | return name + ' -[' + edges[i] + ']-> '; 25 | } 26 | return name; 27 | }).join(''); 28 | reporter.warn(shape, 29 | '' +callChoreoBO.name +' is part of a cyclic call via:
    '+cycleString+''); 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * Do a depth first search starting at node. 36 | * @param node {BusinessObject} Called Choreo to investigate 37 | * @param nodePath {Array} 38 | * @param edgePath {Array} 39 | * @return cycles {Array} Elementary cycles that include the node 40 | */ 41 | function dfs(node, nodePath, edgePath) { 42 | nodePath = nodePath.concat([node]); 43 | const outgoingEdges = getAllCallChoreosFromChoreo(node); 44 | let cycles = []; 45 | for (let edge of outgoingEdges) { 46 | let target = edge.calledChoreographyRef; 47 | if (nodePath.indexOf(target) === -1) { 48 | cycles = cycles.concat(dfs(target, nodePath, edgePath.concat([edge.name]))); 49 | } else { 50 | if (target === nodePath[0]) { 51 | cycles = cycles.concat([{ nodes: nodePath.concat([target]), edges: edgePath.concat([edge.name]) }]); 52 | } 53 | } 54 | } 55 | return cycles; 56 | } 57 | 58 | function getAllCallChoreosFromChoreo(choreo) { 59 | if (!choreo) { 60 | return []; 61 | } 62 | return getAllElementsOfType('bpmn:CallChoreography', [choreo]); 63 | } 64 | 65 | function getContainingChoreo(businessObject) { 66 | let parent = businessObject; 67 | while (!is(parent, 'bpmn:Choreography')) { 68 | parent = businessObject.$parent; 69 | } 70 | return parent; 71 | } -------------------------------------------------------------------------------- /app/lib/validator/constraints/ParticipantNameConstraint.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | /** 4 | * Checks if a name is used for different participants. 5 | * @param shape 6 | * @param reporter {Reporter} 7 | */ 8 | export default function participantNameConstraint(shape, reporter) { 9 | // Check if it is a band shape 10 | if (is(shape, 'bpmn:Participant')) { 11 | const id = shape.businessObject.id; 12 | const name = shape.businessObject.name; 13 | const isUnique = reporter.elementRegistry.filter(elem => elem.diBand) 14 | .every(elem => elem.businessObject.name !== name || elem.businessObject.id === id); 15 | if (!isUnique) { 16 | reporter.warn(shape, 'Multiple different participants share the name ' + name + 17 | ', which should be unique'); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/lib/validator/constraints/SimpleFlowConstraint.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | import { 4 | getConnectedElements, 5 | isInitiating, 6 | getParticipants, 7 | isChoreoActivity, 8 | isIntermediateTimerCatchEvent, 9 | getTimerDefinitionType 10 | } from '../util/ValidatorUtil'; 11 | 12 | /** 13 | * Checks the basic sequence flow constraint. 14 | * Compare with Chapter "11.5.6 The Sequencing of Activities" in the BPMN standard. 15 | * 16 | * @param shape 17 | * @param reporter 18 | */ 19 | export default function simpleFlowConstraint(shape, reporter) { 20 | if (is(shape, 'bpmn:Participant')) { 21 | if (isInitiating(shape)) { 22 | const participant = shape.businessObject; 23 | 24 | // Get all relevant predecessors. We also stop traversing the model once we encounter 25 | // an absolute timer intermediate catch event, since those suspend the simple flow 26 | // constraint (see p. 341 in the standard). 27 | const predecessors = getConnectedElements(shape, 'incoming', e => { 28 | return isChoreoActivity(e) || ( 29 | isIntermediateTimerCatchEvent(e) && 30 | getTimerDefinitionType(e) === 'timeDate' 31 | ); 32 | }).filter(isChoreoActivity); 33 | 34 | // For the remaining choreography tasks, check whether they include this participant. 35 | let simpleConstraint = true; 36 | predecessors.forEach(e => { 37 | simpleConstraint = simpleConstraint && getParticipants(e).includes(participant); 38 | }); 39 | if (!simpleConstraint) { 40 | reporter.error(shape, 'The initiator ' + shape.businessObject.name + ' needs to be part of all directly preceding activities'); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/lib/validator/constraints/SubChoreoParticipantsConstraint.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | import { 4 | flat, 5 | isChoreoActivity 6 | } from '../util/ValidatorUtil'; 7 | 8 | /** 9 | * Checks if all participants in a subchoreo are shown as bands on the subchoreo. 10 | * @param shape 11 | * @param reporter {Reporter} 12 | */ 13 | export default function subChoreoParticipantsConstraint(shape, reporter) { 14 | if (is(shape, 'bpmn:SubChoreography')) { 15 | const allParticipants = new Set(flat(shape.children.filter(isChoreoActivity) 16 | .map(act => act.bandShapes)).map(bs => bs.businessObject)); 17 | const shownParticipants = new Set(shape.bandShapes.map(bs => bs.businessObject)); 18 | const notShown = Array.from(allParticipants) 19 | .filter(part => !shownParticipants.has(part)).map(bo => bo.name); 20 | if (notShown.length > 0) { 21 | reporter.warn(shape, 'Participants ' + notShown.join(', ') + ' are used within the sub-choreography, but are not displayed as bands'); 22 | } 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/lib/validator/util/ValidatorUtil.js: -------------------------------------------------------------------------------- 1 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | /** 4 | * Get connected elements 5 | * @param shape The shape to start from 6 | * @param direction 'incoming' || 'outgoing' if to check connected shapes from incoming or outgoing 7 | * @param hasRequiredType {function} function to determine type of connected elements 8 | * @returns {Array} 9 | */ 10 | export function getConnectedElements(shape, direction, hasRequiredType) { 11 | if (direction !== 'incoming' && direction !== 'outgoing') { 12 | // This would currently reload the page due to debounce perhaps? 13 | throw new Error('Illegal Argument: ' + direction); 14 | } 15 | if (is(shape, 'bpmn:Participant')) { 16 | shape = shape.parent; 17 | } 18 | 19 | if (!is(shape, 'bpmn:FlowNode')) { 20 | return []; 21 | } 22 | 23 | let visited = []; 24 | let connected = []; 25 | 26 | function track(nodeShape, direction) { 27 | const flowDirection = direction === 'incoming' ? 'source' : 'target'; 28 | // avoid loops 29 | if (visited.includes(nodeShape)) { 30 | return; 31 | } 32 | visited.push(nodeShape); 33 | 34 | // add to connected if we have reached an activity 35 | if (shape !== nodeShape && hasRequiredType(nodeShape)) { 36 | connected.push(nodeShape); 37 | return; 38 | } 39 | 40 | // iterate through all incoming or outgoing sequence flows and 41 | // recurse into the sources or targets 42 | nodeShape[direction].forEach(flow => { 43 | track(flow[flowDirection], direction); 44 | }); 45 | } 46 | 47 | track(shape, direction); 48 | return connected; 49 | } 50 | 51 | export function getParticipants(shape) { 52 | if (is(shape, 'bpmn:Participant')) { 53 | return [shape.businessObject]; 54 | } 55 | 56 | if (is(shape, 'bpmn:ChoreographyActivity')) { 57 | return shape.bandShapes.map(bandShape => bandShape.businessObject); 58 | } 59 | 60 | return []; 61 | } 62 | 63 | export function isInitiating(shape) { 64 | if (is(shape, 'bpmn:Participant')) { 65 | return !shape.diBand.participantBandKind.endsWith('non_initiating'); 66 | } 67 | return false; 68 | } 69 | 70 | export function getInitiatingParticipants(shapes) { 71 | return flat(shapes.filter(s => isChoreoActivity(s)) 72 | .map(act => act.bandShapes)).filter(part => isInitiating(part)); 73 | } 74 | 75 | export function isChoreoActivity(shape) { 76 | return is(shape, 'bpmn:ChoreographyActivity'); 77 | } 78 | 79 | export function getTimerDefinitionType(shape) { 80 | const def = shape.businessObject.eventDefinitions[0]; 81 | if (def.timeDate) { 82 | return 'timeDate'; 83 | } else if (def.timeDuration) { 84 | return 'timeDuration'; 85 | } else if (def.timeCycle) { 86 | return 'timeCycle'; 87 | } 88 | } 89 | 90 | export function isIntermediateTimerCatchEvent(shape) { 91 | return is(shape, 'bpmn:IntermediateCatchEvent') 92 | && shape.businessObject.eventDefinitions[0].$type === 'bpmn:TimerEventDefinition'; 93 | } 94 | 95 | export function participatesIn(participant, shape) { 96 | return getParticipants(shape).includes(participant); 97 | } 98 | 99 | /** 100 | * Stand-in function for flattening a array of depth 1. 101 | * @param {Array} array 102 | * @returns {Array} 103 | */ 104 | export function flat(array) { 105 | // Todo: Replace this with the real flat or flatMap when we have an updated 106 | // build pipeline which allows polyfills. 107 | return array.reduce((acc, value) => acc.concat(value), []); 108 | } 109 | -------------------------------------------------------------------------------- /app/styles/app.less: -------------------------------------------------------------------------------- 1 | @import url("../../node_modules/chor-js/assets/styles/chor-js.css"); 2 | @import url("../icons/css/chor-editor.css"); 3 | @import url("../../node_modules/bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css"); 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | body, 10 | html { 11 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 12 | color: rgb(51, 51, 51); 13 | font-size: 12px; 14 | 15 | height: 100%; 16 | max-height: 100%; 17 | padding: 0; 18 | margin: 0; 19 | } 20 | 21 | div#panel-toggle { 22 | flex-shrink: 0; 23 | width: 20px; 24 | overflow-x: hidden; 25 | 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | align-items: center; 30 | 31 | background-color: #FAFAFA; 32 | border-left: solid 1px #CCC; 33 | 34 | & > div { 35 | height: 95px; 36 | width: 20px; 37 | 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | 42 | border-bottom: solid 1px #CCC; 43 | cursor: pointer; 44 | 45 | &:first-child { 46 | border-top: solid 1px #CCC; 47 | } 48 | 49 | & > span { 50 | font-size: 11px; 51 | transform: rotate(-90deg); 52 | white-space: nowrap; 53 | -moz-user-select: none; 54 | -webkit-user-select: none; 55 | -ms-user-select: none; 56 | user-select: none; 57 | } 58 | 59 | &.active { 60 | border-left: 2px solid #4d90ff; 61 | } 62 | 63 | &:hover { 64 | background-color: #eee; 65 | } 66 | } 67 | } 68 | 69 | .side-panel { 70 | background-color: #FAFAFA; 71 | max-width: 250px; 72 | min-width: 250px; 73 | min-height: 100%; 74 | border-left: 1px solid #CCC; 75 | 76 | > .bpp-properties-panel { 77 | background-color: #FAFAFA; 78 | } 79 | } 80 | 81 | .hidden { 82 | display: none; 83 | } 84 | 85 | .content { 86 | position: relative; 87 | width: 100%; 88 | height: 100%; 89 | display: flex; 90 | 91 | #canvas { 92 | flex-grow: 1; 93 | position: relative; 94 | } 95 | 96 | > .message { 97 | width: 100%; 98 | height: 100%; 99 | text-align: center; 100 | display: table; 101 | 102 | font-size: 16px; 103 | color: #111; 104 | 105 | .note { 106 | vertical-align: middle; 107 | text-align: center; 108 | display: table-cell; 109 | } 110 | 111 | &.error { 112 | .details { 113 | max-width: 500px; 114 | font-size: 12px; 115 | margin: 20px auto; 116 | text-align: left; 117 | color: #BD2828; 118 | } 119 | 120 | pre { 121 | border: solid 1px #BD2828; 122 | background: #fefafa; 123 | padding: 10px; 124 | color: #BD2828; 125 | } 126 | } 127 | } 128 | 129 | &:not(.with-error) .error, 130 | &.with-error .intro, 131 | &.with-diagram .intro { 132 | display: none; 133 | } 134 | 135 | .canvas { 136 | display: block; 137 | position: absolute; 138 | top: 0; 139 | left: 0; 140 | right: 0; 141 | bottom: 0; 142 | } 143 | } 144 | 145 | .buttons { 146 | display: flex; 147 | position: fixed; 148 | bottom: 20px; 149 | left: 20px; 150 | padding: 0; 151 | margin: 0; 152 | 153 | * { 154 | margin-right: 10px; 155 | height: 40px; 156 | text-align: center; 157 | line-height: 40px; 158 | font-size: 26px; 159 | } 160 | 161 | .divider { 162 | width: 1px; 163 | border-left: 1px solid #DDD; 164 | } 165 | 166 | a, button { 167 | display: block; 168 | width: 40px; 169 | padding: 0px; 170 | background-color: var(--palette-background-color); 171 | border: solid 1px var(--palette-border-color); 172 | border-radius: 2px; 173 | text-decoration: none; 174 | color: var(--palette-entry-color); 175 | } 176 | 177 | a:hover, button:hover { 178 | color: var(--palette-entry-hover-color); 179 | cursor: pointer; 180 | } 181 | 182 | .selected { 183 | color: var(--palette-entry-selected-color); 184 | } 185 | 186 | button { 187 | cursor: pointer; 188 | } 189 | } 190 | 191 | ul.corner-links { 192 | display: block; 193 | position: absolute; 194 | right: 80px; 195 | bottom: 10px; 196 | margin: 0; 197 | z-index: 100; 198 | 199 | display: flex; 200 | list-style: none; 201 | align-items: center; 202 | 203 | > li { 204 | margin-left: 10px; 205 | 206 | > a { 207 | font-size: 10px; 208 | color: #404040; 209 | } 210 | 211 | > a.icon-github-circled { 212 | font-size: 30px; 213 | text-decoration: none; 214 | } 215 | } 216 | } 217 | 218 | .validation-annotation { 219 | position: relative; 220 | border-width: 2px; 221 | border-color: #FFFFFF; 222 | border-style: solid; 223 | border-radius: 50%; 224 | padding: 2px; 225 | width: 20px; 226 | height: 20px; 227 | 228 | &:hover > .validation-info { 229 | display: block; 230 | } 231 | 232 | .validation-info { 233 | display: none; 234 | position: absolute; 235 | top: -2px; 236 | left: 6px; 237 | list-style: none; 238 | padding-left: 0; 239 | min-width: 180px; 240 | max-height: 300px; 241 | overflow-y: auto; 242 | z-index: 4; 243 | 244 | li { 245 | padding: 4px; 246 | border-style: solid; 247 | border-width: 2px; 248 | border-top-width: 0px; 249 | } 250 | 251 | li:first-child { 252 | border-top-left-radius: 5px; 253 | border-top-right-radius: 5px; 254 | border-top-width: 2px; 255 | } 256 | 257 | li:last-child { 258 | border-bottom-left-radius: 5px; 259 | border-bottom-right-radius: 5px; 260 | } 261 | 262 | .li-error { 263 | background-color: #FDF2F5; 264 | border-color: #EC1E0D; 265 | } 266 | 267 | .li-warning { 268 | background-color: #FFFBD5; 269 | border-color: #FB9500; 270 | } 271 | 272 | .li-note { 273 | background-color: rgb(245, 245, 245); 274 | border-color: rgb(132, 140, 148); 275 | } 276 | } 277 | 278 | .validation-count { 279 | position: absolute; 280 | font-weight: bold; 281 | color: #111111; 282 | left: 2px; 283 | top: -6px; 284 | text-shadow: 1px 1px 1px #FFFFFF, -1px -1px 1px #FFFFFF, 1px -1px 1px #FFFFFF, -1px 1px 1px #FFFFFF; 285 | } 286 | } 287 | 288 | .val-error { 289 | .validation-annotation; 290 | background-color: #EC1E0D; 291 | } 292 | 293 | .val-warning { 294 | .validation-annotation; 295 | background-color: #FB9500; 296 | } 297 | 298 | .is-dragover { 299 | background-color: #eeeeee; 300 | opacity: 0.25; 301 | } 302 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chor-js-demo", 3 | "version": "1.1.0", 4 | "description": "A demo showcasing the chor-js BPMN 2.0 choreography diagram modeler.", 5 | "main": "app/app.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "build": "parcel build app/index.html -d build --public-url ./", 9 | "dev": "parcel app/index.html -p 9013" 10 | }, 11 | "keywords": [ 12 | "choreography-modeler" 13 | ], 14 | "author": { 15 | "name": "Jan Ladleif", 16 | "url": "https://github.com/jan-ladleif" 17 | }, 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/bptlab/chor-js-demo" 22 | }, 23 | "assetTypesToStringify": [ 24 | "bpmn" 25 | ], 26 | "browserslist": [ 27 | "last 2 Firefox versions", 28 | "last 2 Chrome versions", 29 | "last 2 Safari versions", 30 | "last 2 Edge versions", 31 | "last 2 Opera versions", 32 | "last 2 Baidu versions" 33 | ], 34 | "devDependencies": { 35 | "@babel/core": "^7.8.7", 36 | "eslint": "^4.19.1", 37 | "eslint-plugin-bpmn-io": "^0.7.0", 38 | "less": "^3.11.1", 39 | "parcel-bundler": "^1.12.4", 40 | "parcel-plugin-stringify-anything": "^1.2.0" 41 | }, 42 | "dependencies": { 43 | "bpmn-js-properties-panel": "^0.35.0", 44 | "chor-js": "0.7.1", 45 | "inherits": "^2.0.4" 46 | } 47 | } 48 | --------------------------------------------------------------------------------