├── .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 | [![Compatible with Camunda Modeler version 3.4](https://img.shields.io/badge/Camunda%20Modeler-3.4+-blue.svg)](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 | ![Global](./docs/global.png) 13 | * **Control-flow perspective** 14 | ![Control-flow](./docs/control-flow.png) 15 | * **Data perspective** 16 | ![Data flow](./docs/data-flow.png) 17 | * **Organizational perspective** 18 | ![Single pool example](./docs/singlePoolBlackBox.png) 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 |
61 | 62 |
63 | `); 64 | let perspective = domify(``); 69 | 70 | domEvent.bind(perspective, 'change', (event) => { 71 | 72 | self.changedPerspectiveType = false; 73 | self.perspectiveType = event.target.value; 74 | updateView.call(self, event.target.value, self.resourceValue, self.isResourceLane, self.parentLaneId); 75 | }); 76 | 77 | self.organizationalContainer = domify(` 78 |
79 | 80 |
`); 81 | self.resource = domify(''); 82 | self._initialized = true; 83 | updateResources.call(this); 84 | 85 | domEvent.bind(self.resource, 'change', (event) => { 86 | let isLane = event.target.options[event.target.options.selectedIndex].getAttribute('data-isLane'); 87 | let parent = event.target.options[event.target.options.selectedIndex].getAttribute('data-parent'); 88 | 89 | self.changedResourceValue = true; 90 | self.resourceValue = event.target.value; 91 | self.isResourceLane = (isLane === 'true'); 92 | self.parentLaneId = parent; 93 | 94 | updateView.call(self, self.perspectiveType, event.target.value, (isLane === 'true'), parent); 95 | }); 96 | 97 | self.organizationalContainer.appendChild(self.resource); 98 | self.perspectiveContainer.appendChild(perspective); 99 | self.globalContainer.appendChild(self.perspectiveContainer); 100 | self.globalContainer.appendChild(self.organizationalContainer); 101 | self._canvas.getContainer().appendChild(self.globalContainer); 102 | }; 103 | 104 | function fitCanvasToElement(selectedId) { 105 | let bbox = this._elementRegistry.get(selectedId); 106 | let currentViewBox = this._canvas.viewbox(); 107 | 108 | let elementMid = { 109 | x: bbox.x + bbox.width / 2, 110 | y: bbox.y + bbox.height / 2 111 | }; 112 | this._canvas.viewbox({ 113 | x: elementMid.x - currentViewBox.width / 2, 114 | y: elementMid.y - currentViewBox.height / 2, 115 | width: currentViewBox.width, 116 | height: currentViewBox.height 117 | }); 118 | } 119 | 120 | function updateView(perspectiveType, selectedElementId, isLane, parentId) { 121 | this.oldElementsMarked.forEach(elementId => { 122 | this._canvas.removeMarker(elementId, 'disabled-element'); 123 | }); 124 | this.oldElementsMarked = []; 125 | 126 | let resources = this._layerManager.getResources(true); 127 | let elements = this._layerManager.getElements(perspectiveType) || []; 128 | 129 | resources.forEach(resource => { 130 | 131 | // for each pool => disable 132 | if (resource.id !== selectedElementId) { 133 | if (!isLane || resource.id !== parentId) { 134 | 135 | // Selected a pool or it's a lane of another pool 136 | this.oldElementsMarked.push(...resource.elements, resource.id); 137 | if (!isLane) { 138 | resource.lanes.forEach(lane => { 139 | let [laneId, elementsArray] = Object.entries(lane)[0]; 140 | this.oldElementsMarked.push(...elementsArray, laneId); 141 | }); 142 | } 143 | } else { 144 | resource.lanes.forEach(lane => { 145 | let [laneId, elementsArray] = Object.entries(lane)[0]; 146 | if (laneId !== selectedElementId) { 147 | this.oldElementsMarked.push(...elementsArray, laneId); 148 | } else if (this.changedResourceValue) { 149 | fitCanvasToElement.call(this, selectedElementId); 150 | } 151 | }); 152 | } 153 | } else if (this.changedResourceValue) { 154 | fitCanvasToElement.call(this, selectedElementId); 155 | } 156 | }); 157 | 158 | if (perspectiveType !== 'global' || selectedElementId !== ALL_POOLS) { 159 | let newArray; 160 | if (perspectiveType !== 'global' && selectedElementId !== ALL_POOLS) { 161 | newArray = this.oldElementsMarked.concat(elements); 162 | } else if (resources.length > 0) { 163 | newArray = intersection(this.oldElementsMarked, elements); 164 | } else { 165 | newArray = elements; 166 | } 167 | newArray.forEach(elementId => { 168 | this._canvas.addMarker(elementId, 'disabled-element'); 169 | }); 170 | this.oldElementsMarked = newArray; 171 | } 172 | if (this.changedResourceValue) { 173 | this._canvas.zoom('fit-viewport', 'auto'); 174 | } 175 | this.changedPerspectiveType = false; 176 | this.changedResourceValue = false; 177 | } 178 | 179 | TogglePerspective.$inject = ['eventBus', 'canvas', 'elementRegistry', 'layerManager']; -------------------------------------------------------------------------------- /client/bpmn-js-modules/index.js: -------------------------------------------------------------------------------- 1 | import LayerManager from './LayerManager/LayerManager'; 2 | import TogglePerspective from './TogglePerspective/TogglePerspective'; 3 | 4 | export default { 5 | __init__: ['layerManager', 'togglePerspective'], 6 | layerManager : [ 'type', LayerManager], 7 | togglePerspective : ['type', TogglePerspective] 8 | }; -------------------------------------------------------------------------------- /client/bpmn-js-modules/util/EventHelper.js: -------------------------------------------------------------------------------- 1 | const prefix = 'layering.'; 2 | 3 | export const UPDATE_RESOURCES = prefix + 'update'; -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | registerBpmnJSPlugin 3 | } from 'camunda-modeler-plugin-helpers'; 4 | 5 | import BpmnExtensionModule from './bpmn-js-modules'; 6 | 7 | registerBpmnJSPlugin(BpmnExtensionModule); -------------------------------------------------------------------------------- /docs/control-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharedchains/camunda-layering-plugin/9351c3d4004bdfa104b30a2a8805bf3353df747c/docs/control-flow.png -------------------------------------------------------------------------------- /docs/data-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharedchains/camunda-layering-plugin/9351c3d4004bdfa104b30a2a8805bf3353df747c/docs/data-flow.png -------------------------------------------------------------------------------- /docs/global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharedchains/camunda-layering-plugin/9351c3d4004bdfa104b30a2a8805bf3353df747c/docs/global.png -------------------------------------------------------------------------------- /docs/singlePoolBlackBox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharedchains/camunda-layering-plugin/9351c3d4004bdfa104b30a2a8805bf3353df747c/docs/singlePoolBlackBox.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: 'Layering', 5 | script: './dist/client.js', 6 | style: './dist/assets/styles/style.css' 7 | }; 8 | -------------------------------------------------------------------------------- /index.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: 'Layering', 5 | script: './client/client.js', 6 | style: './assets/styles/style.css' 7 | }; 8 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var absoluteBasePath = path.resolve(__dirname); 4 | 5 | // configures browsers to run test against 6 | // any of [ 'ChromeHeadless', 'Chrome', 'Firefox', 'IE', 'PhantomJS' ] 7 | var browsers = 8 | (process.env.TEST_BROWSERS || 'ChromeHeadless') 9 | .replace(/^\s+|\s+$/, '') 10 | .split(/\s*,\s*/g) 11 | .map(function(browser) { 12 | if (browser === 'ChromeHeadless') { 13 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 14 | } 15 | 16 | return browser; 17 | }); 18 | 19 | var suite = 'test/suite.js'; 20 | 21 | 22 | module.exports = function(karma) { 23 | karma.set({ 24 | 25 | frameworks: [ 26 | 'mocha', 27 | 'sinon-chai' 28 | ], 29 | 30 | files: [ 31 | suite 32 | ], 33 | 34 | preprocessors: { 35 | [suite]: [ 'webpack' ] 36 | }, 37 | 38 | reporters: [ 'progress' ], 39 | 40 | browsers: browsers, 41 | 42 | autoWatch: false, 43 | singleRun: true, 44 | 45 | webpack: { 46 | mode: 'development', 47 | module: { 48 | rules: [ 49 | { 50 | test: /\.js$/, 51 | exclude: /node_modules/, 52 | use: { 53 | loader: 'babel-loader', 54 | options: { 55 | presets: ['@babel/preset-react'] 56 | } 57 | } 58 | }, 59 | { 60 | test: /\.(css|bpmn)$/, 61 | use: 'raw-loader' 62 | }, 63 | { 64 | test: /\.png$/, 65 | use: 'url-loader' 66 | } 67 | ] 68 | }, 69 | resolve: { 70 | mainFields: [ 71 | 'dev:module', 72 | 'browser', 73 | 'module', 74 | 'main' 75 | ], 76 | modules: [ 77 | 'node_modules', 78 | absoluteBasePath 79 | ] 80 | } 81 | } 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "camunda-layering-plugin", 3 | "version": "0.1.3", 4 | "description": "View BPMN model in organized layers", 5 | "main": "index.js", 6 | "scripts": { 7 | "all": "run-s lint bundle", 8 | "bundle": "webpack", 9 | "dev": "webpack -w", 10 | "lint": "eslint .", 11 | "prepublishOnly": "run-s bundle", 12 | "test": "karma start" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://gitlab.sh4.red/luca.bonora/camunda-layering-plugin.git" 17 | }, 18 | "keywords": [ 19 | "camunda", 20 | "modeler", 21 | "plugin", 22 | "perspectives", 23 | "layers", 24 | "data", 25 | "control", 26 | "organizational", 27 | "resource", 28 | "flow" 29 | ], 30 | "author": "Luca Bonora ", 31 | "license": "MIT", 32 | "devDependencies": { 33 | "@babel/core": "^7.13.1", 34 | "@babel/plugin-proposal-class-properties": "^7.13.0", 35 | "@babel/preset-react": "^7.12.13", 36 | "babel-loader": "^8.2.2", 37 | "camunda-modeler-plugin-helpers": "^3.4.0-alpha.1", 38 | "chai": "^4.3.0", 39 | "copy-webpack-plugin": "^7.0.0", 40 | "css-loader": "^5.0.2", 41 | "eslint": "^7.20.0", 42 | "eslint-plugin-bpmn-io": "^0.12.0", 43 | "eslint-plugin-react": "^7.22.0", 44 | "file-loader": "^6.2.0", 45 | "karma": "^6.1.1", 46 | "karma-chrome-launcher": "^3.1.0", 47 | "karma-mocha": "^2.0.1", 48 | "karma-sinon-chai": "^2.0.2", 49 | "karma-webpack": "^5.0.0", 50 | "mocha": "^8.3.0", 51 | "mocha-test-container-support": "^0.2.0", 52 | "npm-run-all": "^4.1.5", 53 | "puppeteer": "^7.1.0", 54 | "raw-loader": "^4.0.2", 55 | "sinon": "^9.2.4", 56 | "sinon-chai": "^3.5.0", 57 | "style-loader": "^2.0.0", 58 | "webpack": "^5.24.0", 59 | "webpack-cli": "^4.5.0", 60 | "zip-webpack-plugin": "^4.0.1" 61 | }, 62 | "dependencies": { 63 | "bpmn-js": "^8.2.0", 64 | "bpmn-js-properties-panel": "^0.40.0", 65 | "classnames": "^2.2.6", 66 | "diagram-js": "^7.2.3", 67 | "inherits": "^2.0.4", 68 | "lodash": "^4.17.21", 69 | "min-dom": "^3.1.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /style/style.css: -------------------------------------------------------------------------------- 1 | .perspective-palette { 2 | position: absolute; 3 | bottom: 20px; 4 | right: 20px; 5 | background-color: var(--palette-background-color, #FAFAFA); 6 | padding: 6px; 7 | font-size: 16px; 8 | color: #212121; 9 | border: solid 1px var(--palette-border-color, #CCC); 10 | border-radius: 2px; 11 | display: flex; 12 | } 13 | 14 | .perspective-palette > * { 15 | padding: 3px; 16 | } 17 | 18 | .disabled-element .djs-visual > * { 19 | fill-opacity: 0.4; 20 | stroke-opacity: 0.4; 21 | opacity: 0.4; 22 | } -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plugin:bpmn-io/mocha" 3 | } -------------------------------------------------------------------------------- /test/TestHelper.js: -------------------------------------------------------------------------------- 1 | export * from 'bpmn-js/test/helper'; 2 | 3 | import { 4 | insertCSS 5 | } from 'bpmn-js/test/helper'; 6 | 7 | insertCSS('style.css', [ 8 | '@import "/base/assets/css/normalize.css";', 9 | '@import "/base/assets/css/bpmn-js-token-simulation.css"' 10 | ].join('\n')); 11 | 12 | insertCSS('diagram-js.css', require('diagram-js/assets/diagram-js.css')); 13 | 14 | insertCSS('bpmn-font.css', require('bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css')); 15 | 16 | insertCSS('diagram-js-testing.css', 17 | 'body .test-container { height: auto }' + 18 | 'body .test-container .test-content-container { height: 90vmin; }' 19 | ); -------------------------------------------------------------------------------- /test/all.js: -------------------------------------------------------------------------------- 1 | var allTests = require.context('.', true, /Spec\.js$/); 2 | 3 | allTests.keys().forEach(allTests); 4 | 5 | var allSources = require.context('../client', true, /.*\.js$/); 6 | 7 | allSources.keys().forEach(allSources); -------------------------------------------------------------------------------- /test/bpmn/lanes.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | StartEvent 12 | Activity_1 13 | EndEvent_1 14 | 15 | 16 | ThrowEvent_1 17 | Activity_2 18 | 19 | 20 | 21 | Flow_1vj7bhb 22 | 23 | 24 | Flow_1vj7bhb 25 | Flow_1vf9ih0 26 | 27 | 28 | DataInput_1 29 | Property_0mzmpn2 30 | 31 | 32 | 33 | Flow_1vf9ih0 34 | Flow_1qd6sx9 35 | 36 | 37 | 38 | 39 | 40 | 41 | Flow_14fe82s 42 | 43 | 44 | Flow_1qd6sx9 45 | Flow_14fe82s 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Flow_15dahbi 54 | 55 | 56 | 57 | 58 | Flow_1o8ksih 59 | 60 | 61 | Flow_15dahbi 62 | Flow_1o8ksih 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 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /test/spec/LayerManager.Spec.js: -------------------------------------------------------------------------------- 1 | import Modeler from 'bpmn-js/lib/Modeler'; 2 | 3 | import LayersModule from '../../client/bpmn-js-modules'; 4 | 5 | import { 6 | bootstrapModeler, 7 | inject 8 | } from 'test/TestHelper'; 9 | 10 | import lanesDiagram from '../bpmn/lanes.bpmn'; 11 | 12 | import { find } from 'lodash'; 13 | 14 | describe('LayerManager test', () => { 15 | 16 | function bootstrapDiagram(diagram) { 17 | beforeEach(bootstrapModeler(diagram, { 18 | additionalModules: [].concat(Modeler.prototype._modules).concat([ 19 | LayersModule 20 | ]), 21 | keyboard: { 22 | bindTo: document 23 | } 24 | })); 25 | } 26 | 27 | describe('Lanes diagram', () => { 28 | bootstrapDiagram(lanesDiagram); 29 | 30 | it('should load layerManager', function(done) { 31 | inject(function(layerManager) { 32 | 33 | // given the lanes diagram 34 | // when 35 | let elements = layerManager.getElements(); 36 | expect(elements).to.be.an('array').that.is.not.empty; 37 | 38 | done(); 39 | })(); 40 | }); 41 | 42 | it('should return only data elements (to hide, selecting control-flow)', function(done) { 43 | inject(function(layerManager) { 44 | 45 | // given the lanes diagram 46 | // when 47 | let elements = layerManager.getElements('control'); 48 | expect(elements).to.be.an('array').that.has.length(3); 49 | 50 | done(); 51 | })(); 52 | }); 53 | 54 | it('should return only control-flow elements (to hide, selecting data)', function(done) { 55 | inject(function(layerManager) { 56 | 57 | // given the lanes diagram 58 | // when 59 | let elements = layerManager.getElements('data'); 60 | expect(elements).to.be.an('array').that.has.length(15); 61 | 62 | done(); 63 | })(); 64 | }); 65 | 66 | it('should return pool hierarchy - no elements', function(done) { 67 | inject(function(layerManager) { 68 | 69 | // given the lanes diagram 70 | // when 71 | let pools = layerManager.getResources(); 72 | expect(pools).to.be.an('array').that.has.length(2); 73 | 74 | let alfaPool = find(pools, { id: 'Alfa' }); 75 | expect(alfaPool.lanes).to.be.an('array').that.include.members(['A', 'B']); 76 | 77 | let betaPool = find(pools, { id: 'Beta' }); 78 | expect(betaPool.lanes).to.be.an('array').that.is.empty; 79 | 80 | done(); 81 | })(); 82 | }); 83 | 84 | it('should return pool hierarchy - with elements', function(done) { 85 | inject(function(layerManager) { 86 | 87 | // given the lanes diagram 88 | // when 89 | let pools = layerManager.getResources(true); 90 | expect(pools).to.be.an('array').that.has.length(2); 91 | 92 | let alfaPool = find(pools, { id: 'Alfa' }); 93 | expect(alfaPool.lanes).to.be.an('array').that.deep.includes({ 'A': ['StartEvent', 'Activity_1', 'EndEvent_1'] }); 94 | expect(alfaPool.lanes).to.be.an('array').that.deep.includes({ 'B': ['ThrowEvent_1', 'Activity_2'] }); 95 | 96 | let betaPool = find(pools, { id: 'Beta' }); 97 | expect(betaPool.lanes).to.be.an('array').that.is.empty; 98 | 99 | done(); 100 | })(); 101 | }); 102 | 103 | // TODO: tests adding and removing elements 104 | }); 105 | 106 | }); -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | var allTests = require.context('.', true, /Spec\.js$/); 2 | 3 | allTests.keys().forEach(allTests); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const ZipPlugin = require('zip-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | entry: './client/index.js', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'client.js' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-react'] 21 | } 22 | } 23 | }, { 24 | test: /\.css$/i, 25 | use: ['style-loader', 'css-loader'], 26 | }, 27 | { test: /\.(png|svg|jpe?g|gif|woff2?|ttf|eot)$/, use: ['file-loader'] } 28 | ] 29 | }, 30 | resolve: { 31 | alias: { 32 | react: 'camunda-modeler-plugin-helpers/react' 33 | }, 34 | fallback: { 35 | 'util': false, 36 | 'assert': false 37 | } 38 | }, 39 | plugins: [ 40 | new CopyPlugin({ 41 | patterns: [ 42 | { 43 | from: 'style/style.css', 44 | to: 'assets/styles' 45 | }, 46 | { 47 | from: path.resolve(__dirname, './index.prod.js'), 48 | to: path.resolve(__dirname, './dist/index.js') 49 | } 50 | ], 51 | }), 52 | new ZipPlugin({ 53 | filename: 'camunda-layering-plugin-' + process.env.npm_package_version + '.zip', 54 | pathPrefix: 'camunda-layering-plugin/', 55 | pathMapper: function(assetPath) { 56 | if (assetPath.startsWith('client') || assetPath.startsWith('style')) { 57 | return path.join(path.dirname(assetPath), 'client', path.basename(assetPath)); 58 | } 59 | return assetPath; 60 | } 61 | }) 62 | ], 63 | devtool: 'cheap-module-source-map' 64 | }; --------------------------------------------------------------------------------