├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client ├── bpmn-js-modules │ ├── LayerManager │ │ ├── LayerElements.js │ │ └── LayerManager.js │ ├── TogglePerspective │ │ └── TogglePerspective.js │ ├── index.js │ └── util │ │ └── EventHelper.js └── index.js ├── docs ├── control-flow.png ├── data-flow.png ├── global.png └── singlePoolBlackBox.png ├── index.js ├── index.prod.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── style └── style.css ├── test ├── .eslintrc ├── TestHelper.js ├── all.js ├── bpmn │ └── lanes.bpmn ├── spec │ └── LayerManager.Spec.js └── suite.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:bpmn-io/es6", 6 | "plugin:bpmn-io/mocha" 7 | ], 8 | "rules": { 9 | "react/prop-types": 0 10 | }, 11 | "env": { 12 | "browser": true, 13 | "es6": true 14 | } 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .history 3 | 4 | .idea 5 | /dist/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | client 3 | webpack.config.js -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | # 0.1.3 5 | 6 | - `FIX` : Error related to multi-diagram process 7 | - `FIX` : Reloading resource perspective 8 | 9 | # 0.1.2 10 | 11 | - Changed zooming behaviour 12 | - Moved panel to the right corner 13 | 14 | # 0.1.1 15 | 16 | - Fixed changing perspective on a single process -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Shared Technologies SRL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Camunda Layering Plugin 2 | 3 | [](https://github.com/camunda/camunda-modeler) 4 | 5 | A [Camunda Modeler](https://github.com/camunda/camunda-modeler) plug-in based on the [plug-in example](https://github.com/camunda/camunda-modeler-plugin-example). 6 | 7 | ## About 8 | 9 | This plug-in creates different perspectives to view the canvas: 10 | 11 | * **Global perspective** 12 |  13 | * **Control-flow perspective** 14 |  15 | * **Data perspective** 16 |  17 | * **Organizational perspective** 18 |  19 | 20 | 21 | ## Install 22 | 23 | Extract the [release zip file](https://github.com/sharedchains/camunda-layering-plugin/releases/tag/v0.1.0) to your camunda-modeler/resources/plugins folder. Super easy! 24 | 25 | 26 | ## Development Setup 27 | 28 | Use [npm](https://www.npmjs.com/), the [Node.js](https://nodejs.org/en/) package manager to download and install required dependencies: 29 | 30 | ```sh 31 | npm install 32 | ``` 33 | 34 | To make the Camunda Modeler aware of your plug-in you must link the plug-in to the [Camunda Modeler plug-in directory](https://github.com/camunda/camunda-modeler/tree/develop/docs/plugins#plugging-into-the-camunda-modeler) via a symbolic link. 35 | Available utilities to do that are [`mklink /d`](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/mklink) on Windows and [`ln -s`](https://linux.die.net/man/1/ln) on MacOS / Linux. 36 | 37 | Re-start the app in order to recognize the newly linked plug-in. 38 | 39 | 40 | ## Building the Plug-in 41 | 42 | You may spawn the development setup to watch source files and re-build the client plug-in on changes: 43 | 44 | ```sh 45 | npm run dev 46 | ``` 47 | 48 | Given you've setup and linked your plug-in [as explained above](#development-setup), you should be able to reload the modeler to pick up plug-in changes. To do so, open the app's built in development tools via `F12`. Then, within the development tools press the reload shortcuts `CTRL + R` or `CMD + R` to reload the app. 49 | 50 | 51 | To prepare the plug-in for release, executing all necessary steps, run: 52 | 53 | ```sh 54 | npm run all 55 | ``` 56 | 57 | ## Additional Resources 58 | 59 | * [List of existing plug-ins](https://github.com/camunda/camunda-modeler-plugins) 60 | * [Plug-ins documentation](https://github.com/camunda/camunda-modeler/tree/master/docs/plugins) 61 | 62 | 63 | ## TODO 64 | * Organizational perspective with black box pools 65 | 66 | ## Licence 67 | 68 | MIT 69 | -------------------------------------------------------------------------------- /client/bpmn-js-modules/LayerManager/LayerElements.js: -------------------------------------------------------------------------------- 1 | export const DataElements = [ 2 | 'bpmn:ItemAwareElement', 3 | 'bpmn:Artifact', 4 | 'bpmn:DataAssociation' 5 | ]; 6 | 7 | export const CollaborationElements = [ 8 | 'bpmn:Participant', 9 | 'bpmn:LaneSet', 10 | 'bpmn:Lane' 11 | ]; 12 | 13 | -------------------------------------------------------------------------------- /client/bpmn-js-modules/LayerManager/LayerManager.js: -------------------------------------------------------------------------------- 1 | import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil'; 2 | 3 | import { CollaborationElements, DataElements } from './LayerElements'; 4 | 5 | import { filter, find, flatMap, map, remove } from 'lodash'; 6 | 7 | import { UPDATE_RESOURCES } from '../util/EventHelper'; 8 | 9 | const LOW_PRIORITY = 100; 10 | 11 | function getHash(obj) { 12 | 13 | // Create a string representation of the object 14 | return Object.keys(obj) 15 | .sort() // Keys don't have to be sorted, we need to sort them 16 | .map(function(k) { 17 | return k + '_' + obj[k]; // concat each key with its value 18 | }) 19 | .join('_'); // separate key-value-pairs by a _ 20 | } 21 | 22 | function collectionAdd(collection, element) { 23 | 24 | const mapCollection = new Map(collection.map(v => [ getHash(v), v ])); 25 | if (!mapCollection.has(getHash(element))) { 26 | collection.push(element); 27 | } 28 | } 29 | 30 | export default function LayerManager(eventBus) { 31 | this.layers = { 32 | control: [], // element ids 33 | data: [], // element ids 34 | pools: [] 35 | 36 | /* hierarchy structure : { 37 | id: poolId, 38 | name: name, 39 | process: processId, 40 | elements: [] element ids, 41 | lanes: [] lane ids => element ids 42 | } */ 43 | }; 44 | 45 | let self = this; 46 | 47 | function getProcess(element) { 48 | let businessObject = getBusinessObject(element); 49 | 50 | if (is(businessObject, 'bpmn:Participant')) { 51 | return businessObject.processRef; 52 | } 53 | if (is(businessObject, 'bpmn:Process')) { 54 | return businessObject; 55 | } 56 | if (is(businessObject, 'bpmn:MessageFlow')) { 57 | return undefined; 58 | } 59 | 60 | let parent = businessObject; 61 | while (parent.$parent && !is(parent, 'bpmn:Process')) { 62 | parent = parent.$parent; 63 | } 64 | 65 | if (!is(parent, 'bpmn:Process')) { 66 | return undefined; 67 | } 68 | return parent; 69 | } 70 | 71 | function addLayerElement(element) { 72 | let isDataElement = DataElements.some(elementType => { 73 | return is(element, elementType); 74 | }); 75 | let isCollaborationElement = CollaborationElements.some(elementType => { 76 | return is(element, elementType); 77 | }); 78 | let isParticipant = is(element, 'bpmn:Participant'); 79 | 80 | if (isDataElement) { 81 | collectionAdd(self.layers.data, element.id); 82 | } else if (isCollaborationElement) { 83 | 84 | // if is a lane, do nothing here 85 | if (isParticipant) { 86 | let bo = getBusinessObject(element); 87 | collectionAdd(self.layers.pools, { 88 | id: element.id, 89 | name: bo.get('name'), 90 | process: bo.get('processRef') ? bo.get('processRef').id : undefined, 91 | elements: [], 92 | lanes: [] 93 | }); 94 | eventBus.fire(UPDATE_RESOURCES); 95 | } 96 | } else { 97 | collectionAdd(self.layers.control, element.id); 98 | } 99 | 100 | let isCollaboration = self.layers.pools.length > 0; 101 | if (isCollaboration && !isParticipant) { 102 | let rootElement = getProcess(element); 103 | if (rootElement) { 104 | let pool = find(self.layers.pools, { process: rootElement.id }); 105 | if (pool) { 106 | if (is(element, 'bpmn:Lane')) { 107 | let lane = {}; 108 | let bo = getBusinessObject(element); 109 | lane[element.id] = bo.flowNodeRef && bo.flowNodeRef.map(flowNode => flowNode.id) || []; 110 | collectionAdd(pool.lanes, lane); 111 | } else { 112 | collectionAdd(pool.elements, element.id); 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | function updateLayerPools(element, additions, removals) { 120 | removals.forEach(oldLane => { 121 | let lane = find( 122 | flatMap(self.layers.pools, 'lanes'), 123 | poolLane => { 124 | let [ laneId ] = Object.entries(poolLane)[0]; 125 | return oldLane.id === laneId; 126 | }); 127 | if (lane) { 128 | let [ laneId, elementsArray ] = Object.entries(lane)[0]; 129 | lane[laneId] = filter(elementsArray, id => id !== element.id); 130 | } 131 | }); 132 | additions.forEach(newLane => { 133 | let lane = find( 134 | flatMap(self.layers.pools, 'lanes'), 135 | poolLane => { 136 | let [ laneId ] = Object.entries(poolLane)[0]; 137 | return newLane.id === laneId; 138 | }); 139 | if (lane) { 140 | let [ laneId ] = Object.entries(lane)[0]; 141 | collectionAdd(lane[laneId], element.id); 142 | } 143 | 144 | }); 145 | } 146 | 147 | function removeLayerElement(element) { 148 | remove(self.layers.data, id => id === element.id); 149 | remove(self.layers.control, id => id === element.id); 150 | 151 | let isCollaboration = self.layers.pools.length > 0; 152 | if (isCollaboration) { 153 | if (is(element, 'bpmn:Participant')) { 154 | 155 | // Remove only the pool, the inner elements where already removed before 156 | remove(self.layers.pools, pool => pool.id === element.id); 157 | } else if (is(element, 'bpmn:Lane')) { 158 | 159 | // Remove only the lane, which is only logical 160 | let process = getProcess(element); 161 | let pool = find(self.layers.pools, { process: process.id }); 162 | remove(pool.lanes, lane => { 163 | let [ laneId ] = Object.entries(lane)[0]; 164 | return laneId === element.id; 165 | }); 166 | } else { 167 | let rootElement = getProcess(element); 168 | if (rootElement) { 169 | 170 | // Remove a single element from the pool hierarchy and, if present, from lanes 171 | let pool = find(self.layers.pools, { process: rootElement.id }); 172 | if (pool) { 173 | remove(pool.elements, id => id === element.id); 174 | if (pool.lanes.length > 0) { 175 | let occurredLane = find(pool.lanes, lane => { 176 | let [ , elementsArray ] = Object.entries(lane)[0]; 177 | return elementsArray.indexOf(element.id) !== -1; 178 | }); 179 | 180 | if (occurredLane) { 181 | let [ laneId, elementsArray ] = Object.entries(occurredLane)[0]; 182 | occurredLane[laneId] = filter(elementsArray, id => id !== element.id); 183 | } 184 | } 185 | } 186 | } 187 | } 188 | eventBus.fire(UPDATE_RESOURCES); 189 | } 190 | } 191 | 192 | eventBus.on([ 'shape.added', 'connection.added', 'commandStack.shape.create.postExecuted' ], LOW_PRIORITY, context => { 193 | let element = context.element || context.context.shape; 194 | addLayerElement(element); 195 | }); 196 | eventBus.on([ 'shape.removed', 'connection.removed' ], LOW_PRIORITY, context => { 197 | let element = context.element; 198 | removeLayerElement(element); 199 | }); 200 | 201 | eventBus.on([ 'commandStack.element.updateProperties.postExecuted' ], LOW_PRIORITY, context => { 202 | 203 | // If any element is updated (on id/name), I have to update my layers 204 | let updatedPropertiesObject = context.context.properties; 205 | let element = context.context.element; 206 | 207 | let isCollaboration = self.layers.pools.length > 0; 208 | let isNameUpdated = undefined; 209 | if (Object.prototype.hasOwnProperty.call(updatedPropertiesObject, 'id') || 210 | (is(element, 'bpmn:Participant') && (isNameUpdated = Object.prototype.hasOwnProperty.call(updatedPropertiesObject, 'name')))) { 211 | 212 | let isDataElement = DataElements.some(elementType => { 213 | return is(element, elementType); 214 | }); 215 | let isCollaborationElement = CollaborationElements.some(elementType => { 216 | return is(element, elementType); 217 | }); 218 | let oldProperties = context.context.oldProperties; 219 | 220 | let oldId = oldProperties.id; 221 | if (oldId) { 222 | let newId = updatedPropertiesObject.id; 223 | let indexElement; 224 | if (isDataElement) { 225 | indexElement = self.layers.data.indexOf(oldId); 226 | self.layers.data.splice(indexElement, 1, newId); 227 | } else { 228 | indexElement = self.layers.control.indexOf(oldId); 229 | if (indexElement !== -1) { 230 | self.layers.control.splice(indexElement, 1, newId); 231 | } else if (is(element, 'bpmn:Participant')) { 232 | let pool = find(self.layers.pools, { id: oldId }); 233 | pool.id = newId; 234 | } else if (is(element, 'bpmn:Lane')) { 235 | let process = getProcess(element); 236 | let pool = find(self.layers.pools, { process: process.id }); 237 | let poolLane = find(pool.lanes, lane => { 238 | let [ laneId ] = Object.entries(lane)[0]; 239 | return laneId === oldId; 240 | }); 241 | Object.defineProperty(poolLane, newId, Object.getOwnPropertyDescriptor(poolLane, oldId)); 242 | delete poolLane[oldId]; 243 | } 244 | } 245 | 246 | if (isCollaboration && !isCollaborationElement) { 247 | 248 | let rootElement = getProcess(element); 249 | if (rootElement) { 250 | 251 | // Update a single element from the pool hierarchy and, if present, from lanes 252 | let pool = find(self.layers.pools, { process: rootElement.id }); 253 | indexElement = pool.elements.indexOf(oldId); 254 | pool.elements.splice(indexElement, 1, newId); 255 | 256 | if (pool.lanes.length > 0) { 257 | let occurredLane = find(pool.lanes, lane => { 258 | let [ , elementsArray ] = Object.entries(lane)[0]; 259 | return elementsArray.indexOf(element.id) !== -1; 260 | }); 261 | 262 | if (occurredLane) { 263 | let [ , elementsArray ] = Object.entries(occurredLane)[0]; 264 | indexElement = elementsArray.indexOf(oldId); 265 | elementsArray.splice(indexElement, 1, newId); 266 | } 267 | } 268 | } 269 | } 270 | } 271 | 272 | if (is(element, 'bpmn:Participant') && isNameUpdated) { 273 | let pool = find(self.layers.pools, { id: element.id }); 274 | pool.name = updatedPropertiesObject.name; 275 | } 276 | 277 | // Update perspective layout 278 | if (isCollaboration) { 279 | eventBus.fire(UPDATE_RESOURCES); 280 | } 281 | } 282 | }); 283 | 284 | eventBus.on('commandStack.lane.updateRefs.postExecute', LOW_PRIORITY, context => { 285 | let ctx = context.context.updates; 286 | ctx.forEach(update => { 287 | let element = update.flowNode; 288 | if (update.add.length > 0 || update.remove.length > 0) { 289 | updateLayerPools(element, update.add, update.remove); 290 | } 291 | }); 292 | }); 293 | } 294 | 295 | LayerManager.$inject = [ 'eventBus' ]; 296 | 297 | LayerManager.prototype.getElements = function(type) { 298 | let returnedElements; 299 | switch (type) { 300 | case 'control': 301 | returnedElements = this.layers['data']; 302 | break; 303 | case 'data' : 304 | returnedElements = this.layers['control']; 305 | break; 306 | default: 307 | returnedElements = this.layers['data'].concat(this.layers['control']); 308 | } 309 | 310 | return returnedElements; 311 | }; 312 | 313 | LayerManager.prototype.getResources = function(getElements) { 314 | return map(this.layers.pools, pool => { 315 | let lanes = pool.lanes; 316 | if (!getElements) { 317 | lanes = map(pool.lanes, lane => { 318 | let [ laneId ] = Object.entries(lane)[0]; 319 | return laneId; 320 | }); 321 | } 322 | 323 | let returnObject = { 324 | id: pool.id, 325 | name: pool.name || pool.id, 326 | lanes: lanes 327 | }; 328 | if (getElements) { 329 | returnObject.elements = pool.elements; 330 | } 331 | return returnObject; 332 | }); 333 | }; -------------------------------------------------------------------------------- /client/bpmn-js-modules/TogglePerspective/TogglePerspective.js: -------------------------------------------------------------------------------- 1 | import { domify, event as domEvent } from 'min-dom'; 2 | import { UPDATE_RESOURCES } from '../util/EventHelper'; 3 | 4 | import { intersection } from 'lodash'; 5 | 6 | const ALL_POOLS = '0_all_pools'; 7 | 8 | export default function TogglePerspective(eventBus, canvas, elementRegistry, layerManager) { 9 | let self = this; 10 | 11 | this._initialized = false; 12 | this._eventBus = eventBus; 13 | this._canvas = canvas; 14 | this._elementRegistry = elementRegistry; 15 | this._layerManager = layerManager; 16 | this.oldElementsMarked = []; 17 | this.perspectiveType = 'global'; 18 | this.changedPerspectiveType = false; 19 | this.resourceValue = ALL_POOLS; 20 | this.changedResourceValue = false; 21 | this.isResourceLane = false; 22 | this.parentLaneId = undefined; 23 | 24 | this._eventBus.on('import.done', () => self._init()); 25 | this._eventBus.on(UPDATE_RESOURCES, () => { 26 | updateResources.call(self); 27 | }); 28 | } 29 | 30 | function getResourceOptions(layerManager) { 31 | let options = [``]; 32 | let resources = layerManager.getResources(); 33 | resources.forEach(resource => { 34 | 35 | options.push(``); 36 | if (resource.lanes.length > 0) { 37 | resource.lanes.forEach(lane => { 38 | options.push(``); 39 | }); 40 | } 41 | }); 42 | return options; 43 | } 44 | 45 | function updateResources() { 46 | if (this._initialized) { 47 | this.resource.innerHTML = ''; 48 | let options = getResourceOptions(this._layerManager); 49 | options.forEach(option => { 50 | this.resource.appendChild(domify(option)); 51 | }); 52 | } 53 | } 54 | 55 | TogglePerspective.prototype._init = function() { 56 | let self = this; 57 | self.globalContainer = domify('
'); 58 | 59 | self.perspectiveContainer = domify(` 60 |