├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── index.html └── index.js ├── client ├── bpmn-js-extension │ ├── context-pad │ │ ├── OpenProcessContextPad.js │ │ └── index.js │ └── multi-diagram │ │ ├── COPYING │ │ ├── DiagramSwitch.js │ │ ├── cmd │ │ ├── CreateDiagramHandler.js │ │ ├── DeleteDiagramHandler.js │ │ └── RenameDiagramHandler.js │ │ ├── index.js │ │ └── utils │ │ ├── DiagramUtil.js │ │ └── index.js ├── index.js ├── properties-provider │ ├── CallActivityPropertiesProvider.js │ ├── index.js │ └── props │ │ ├── CalledElementProps.js │ │ └── CalledTypeProps.js ├── react │ ├── DiagramButtonsOverlay.js │ ├── MultiDiagramButton.js │ └── modals │ │ └── rename-diagram │ │ └── RenameDiagramModal.js └── style.css ├── docs └── screencast.gif ├── index.js ├── index.prod.js ├── package-lock.json ├── package.json ├── resources ├── minus-solid.svg ├── newDiagram.bpmn ├── pencil-solid.svg ├── plus-solid.svg └── subprocess-collapsed.svg ├── styles └── app.less ├── webpack.config.js └── webpack.config.serve.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:bpmn-io/es6" 10 | ], 11 | "rules": { 12 | "indent": [ 2, 2, { 13 | "VariableDeclarator": { "var": 2, "let": 2, "const": 3 }, 14 | "FunctionDeclaration": { "body": 1, "parameters": 2 }, 15 | "FunctionExpression": { "body": 1, "parameters": 2 }, 16 | "ignoredNodes": [ "TemplateLiteral > *" ] 17 | } ], 18 | "no-bitwise": 0, 19 | "react/prop-types": 0, 20 | "react/react-in-jsx-scope": 0 21 | } 22 | } -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run all --if-present 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | public 3 | tmp 4 | node_modules 5 | 6 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | client 3 | webpack.config.js -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to the [camunda-modeler-plugin-multidiagram](https://github.com/sharedchains/camunda-modeler-plugin-multidiagram/) are documented here. We use [semantic versioning](http://semver.org/) for releases. 4 | 5 | ## Unreleased 6 | 7 | ___Note:__ Yet to be released changes appear here._ 8 | 9 | ## 2.0.2 10 | 11 | * `FEAT`: Supported Collaboration multi-diagram again 12 | 13 | ## 2.0.1 14 | 15 | * `FIX`: Fixed diagram delete behaviour and revert commands 16 | 17 | ## 2.0.0 18 | 19 | * `CHORE`: support Camunda Modeler 5.x.x+ 20 | 21 | ## 1.0.1 22 | 23 | * `FEAT`: Removes SwitchDiagram as a command 24 | * `FIX`: Fixes error switching between bpmn tabs 25 | 26 | ## 1.0.0 27 | 28 | * `CHORE`: support Camunda Modeler v4.x.x+ 29 | * `FEAT`: Manage multiple diagrams on a single bpmn file, according to BPMN specifications 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Shared 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 Modeler Multi-Diagram plug-in 2 | 3 | [![Compatible with Camunda Modeler version 5](https://img.shields.io/badge/Camunda%20Modeler-5.0+-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 | ## How to install the plugin 8 | 9 | Download the latest [release zip](https://github.com/sharedchains/camunda-modeler-plugin-multidiagram/releases/) and extract it to your camunda-modeler/resources/plugins folder. That's all! 10 | 11 | ## About 12 | 13 | This plug-in adds the ability to Camunda modeler to manage multiple diagrams on a single bpmn file, as intended on BPMN specifications. Then it's possible to link, on a multi-diagram bpmn, as Caller element for a Call Activity task, one of the processes inside the bpmn file itself. 14 | 15 | ![plug-in screencast](./docs/screencast.gif "plug-in in action") 16 | 17 | ## Development Setup 18 | 19 | Use [npm](https://www.npmjs.com/), the [Node.js](https://nodejs.org/en/) package manager to download and install required dependencies: 20 | 21 | ```sh 22 | npm install 23 | ``` 24 | 25 | 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. 26 | 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. 27 | 28 | Re-start the app in order to recognize the newly linked plug-in. 29 | 30 | 31 | ## Building the Plug-in 32 | 33 | You may spawn the development setup to watch source files and re-build the client plug-in on changes: 34 | 35 | ```sh 36 | npm run dev 37 | ``` 38 | 39 | 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 toos via `F12`. Then, within the development tools press the reload shortcuts `CTRL + R` or `CMD + R` to reload the app. 40 | 41 | 42 | To prepare the plug-in for release, executing all necessary steps, run: 43 | 44 | ```sh 45 | npm run all 46 | ``` 47 | 48 | ## Compatibility Notice 49 | 50 | This plugin is currently compatible with the following Camunda Modeler versions. 51 | 52 | | Camunda Modeler | MultiDiagram Plugin | 53 | |-----------------|---------------------| 54 | | 3.4 - 4.12 | 1.0.1 | 55 | | 5.x | 2.0 or newer | 56 | 57 | ## Additional Resources 58 | 59 | * [bpmn-js](https://github.com/sharedchains/bpmn-js/tree/feature/multipleDiagram) 60 | * [Camunda modeler](https://github.com/sharedchains/camunda-modeler/tree/feature/multiDiagrams) 61 | * [Plug-ins documentation](https://docs.camunda.io/docs/components/modeler/desktop-modeler/plugins/) 62 | 63 | ## Licence 64 | 65 | MIT 66 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bpmn-js-properties-panel extension demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | Drop BPMN diagram from your desktop or create a new diagram to get started. 17 |
18 |
19 | 20 |
21 |
22 |

Ooops, we could not display the BPMN 2.0 diagram.

23 | 24 |
25 | cause of the problem 26 |

27 |         
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import BpmnModeler from 'bpmn-js/lib/Modeler'; 3 | 4 | import { 5 | BpmnPropertiesPanelModule, 6 | BpmnPropertiesProviderModule, 7 | CamundaPlatformPropertiesProviderModule 8 | } from 'bpmn-js-properties-panel'; 9 | 10 | import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda.json'; 11 | import CamundaModule from 'camunda-bpmn-moddle/lib'; 12 | 13 | import MultiDiagramFeaturesModule from '../client/bpmn-js-extension/multi-diagram'; 14 | import ProcessContextPadModule from '../client/bpmn-js-extension/context-pad'; 15 | import CallActivityExtModule from '../client/properties-provider'; 16 | 17 | import { 18 | debounce 19 | } from 'min-dash'; 20 | 21 | import diagramXML from '../resources/newDiagram.bpmn'; 22 | 23 | import '../styles/app.less'; 24 | 25 | var container = $('#js-drop-zone'); 26 | 27 | var bpmnModeler = new BpmnModeler({ 28 | container: '#js-canvas', 29 | propertiesPanel: { 30 | parent: '#js-properties-panel' 31 | }, 32 | additionalModules: [ 33 | BpmnPropertiesPanelModule, 34 | BpmnPropertiesProviderModule, 35 | MultiDiagramFeaturesModule, 36 | ProcessContextPadModule, 37 | CallActivityExtModule, 38 | CamundaPlatformPropertiesProviderModule, 39 | CamundaModule 40 | ], 41 | moddleExtensions: { 42 | camunda: camundaModdleDescriptor 43 | } 44 | }); 45 | 46 | function createNewDiagram() { 47 | openDiagram(diagramXML); 48 | } 49 | 50 | async function openDiagram(xml) { 51 | 52 | try { 53 | 54 | await bpmnModeler.importXML(xml); 55 | 56 | container 57 | .removeClass('with-error') 58 | .addClass('with-diagram'); 59 | } catch (err) { 60 | 61 | container 62 | .removeClass('with-diagram') 63 | .addClass('with-error'); 64 | 65 | container.find('.error pre').text(err.message); 66 | 67 | console.error(err); 68 | } 69 | } 70 | 71 | function registerFileDrop(container, callback) { 72 | 73 | function handleFileSelect(e) { 74 | e.stopPropagation(); 75 | e.preventDefault(); 76 | 77 | var files = e.dataTransfer.files; 78 | 79 | var file = files[0]; 80 | 81 | var reader = new FileReader(); 82 | 83 | reader.onload = function(e) { 84 | 85 | var xml = e.target.result; 86 | 87 | callback(xml); 88 | }; 89 | 90 | reader.readAsText(file); 91 | } 92 | 93 | function handleDragOver(e) { 94 | e.stopPropagation(); 95 | e.preventDefault(); 96 | 97 | e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy. 98 | } 99 | 100 | container.get(0).addEventListener('dragover', handleDragOver, false); 101 | container.get(0).addEventListener('drop', handleFileSelect, false); 102 | } 103 | 104 | 105 | // //// file drag / drop /////////////////////// 106 | 107 | // check file api availability 108 | if (!window.FileList || !window.FileReader) { 109 | window.alert( 110 | 'Looks like you use an older browser that does not support drag and drop. ' + 111 | 'Try using Chrome, Firefox or the Internet Explorer > 10.'); 112 | } else { 113 | registerFileDrop(container, openDiagram); 114 | } 115 | 116 | // bootstrap diagram functions 117 | 118 | $(function() { 119 | 120 | $('#js-create-diagram').click(function(e) { 121 | e.stopPropagation(); 122 | e.preventDefault(); 123 | 124 | createNewDiagram(); 125 | }); 126 | 127 | var downloadLink = $('#js-download-diagram'); 128 | var downloadSvgLink = $('#js-download-svg'); 129 | 130 | $('.buttons a').click(function(e) { 131 | if (!$(this).is('.active')) { 132 | e.preventDefault(); 133 | e.stopPropagation(); 134 | } 135 | }); 136 | 137 | function setEncoded(link, name, data) { 138 | var encodedData = encodeURIComponent(data); 139 | 140 | if (data) { 141 | link.addClass('active').attr({ 142 | 'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData, 143 | 'download': name 144 | }); 145 | } else { 146 | link.removeClass('active'); 147 | } 148 | } 149 | 150 | var exportArtifacts = debounce(async function() { 151 | 152 | try { 153 | 154 | const { svg } = await bpmnModeler.saveSVG(); 155 | 156 | setEncoded(downloadSvgLink, 'diagram.svg', svg); 157 | } catch (err) { 158 | 159 | console.error('Error happened saving SVG: ', err); 160 | 161 | setEncoded(downloadSvgLink, 'diagram.svg', null); 162 | } 163 | 164 | try { 165 | 166 | const { xml } = await bpmnModeler.saveXML({ format: true }); 167 | 168 | setEncoded(downloadLink, 'diagram.bpmn', xml); 169 | } catch (err) { 170 | 171 | console.error('Error happened saving diagram: ', err); 172 | 173 | setEncoded(downloadLink, 'diagram.bpmn', null); 174 | } 175 | }, 500); 176 | 177 | bpmnModeler.on('commandStack.changed', exportArtifacts); 178 | 179 | 180 | 181 | 182 | // HANDLE MULTI-DIAGRAM ////////////////////////////////// 183 | function switchDiagram(e) { 184 | let id = e.target.value; 185 | const bpmnjs = bpmnModeler.get('bpmnjs'); 186 | bpmnjs.open(id); 187 | } 188 | 189 | function addDiagram() { 190 | let diagramSwitch = bpmnModeler.get('diagramSwitch'); 191 | diagramSwitch.addDiagram(); 192 | populateDiagramCombo(); 193 | } 194 | 195 | function deleteDiagram() { 196 | let diagramSwitch = bpmnModeler.get('diagramSwitch'); 197 | diagramSwitch.deleteDiagram(); 198 | populateDiagramCombo(); 199 | } 200 | 201 | function renameDiagram(e) { 202 | let diagramSwitch = bpmnModeler.get('diagramSwitch'); 203 | diagramSwitch.renameDiagram(e.target.value); 204 | populateDiagramCombo(); 205 | } 206 | 207 | function populateDiagramCombo() { 208 | let diagramSwitch = bpmnModeler.get('diagramSwitch'); 209 | const select = $('.djs-select'); 210 | select.empty(); 211 | 212 | const currentDiagram = diagramSwitch._diagramUtil.currentDiagram(); 213 | const diagrams = diagramSwitch._diagramUtil.diagrams(); 214 | 215 | diagrams.forEach((diagram) => { 216 | const diagramName = diagram.name || diagram.id; 217 | select.append(` 218 | 223 | `); 224 | }); 225 | } 226 | 227 | function handleEndRenameEvent(e) { 228 | if (e.keyCode && e.keyCode !== 13) { 229 | return; 230 | } 231 | 232 | displaySelectInterface(); 233 | } 234 | 235 | function displayRenameInterface() { 236 | hideInterface(); 237 | 238 | let diagramSwitch = bpmnModeler.get('diagramSwitch'); 239 | const renameWrapper = document.querySelector('.djs-rename-wrapper'); 240 | renameWrapper.style.display = 'flex'; 241 | 242 | const renameInput = document.querySelector('.djs-rename'); 243 | const currentDiagram = diagramSwitch._diagramUtil.currentDiagram(); 244 | renameInput.value = currentDiagram.name || currentDiagram.id; 245 | renameInput.focus(); 246 | renameInput.select(); 247 | } 248 | 249 | function displaySelectInterface() { 250 | hideInterface(); 251 | 252 | populateDiagramCombo(); 253 | const selectWrapper = document.querySelector('.djs-select-wrapper'); 254 | selectWrapper.style.display = 'flex'; 255 | } 256 | 257 | function hideInterface() { 258 | const renameWrapper = document.querySelector('.djs-rename-wrapper'); 259 | renameWrapper.style.display = 'none'; 260 | 261 | const selectWrapper = document.querySelector('.djs-select-wrapper'); 262 | selectWrapper.style.display = 'none'; 263 | } 264 | 265 | let eventBus = bpmnModeler.get('eventBus'); 266 | eventBus.once('import.render.complete', populateDiagramCombo); 267 | 268 | $('.djs-palette').append(`
269 | 270 | 271 | 272 | 273 |
274 | 275 |
276 | 277 | 278 |
279 | 280 | `); 281 | 282 | $('.djs-select').on('change', switchDiagram); 283 | $('#add-diagram').on('click', addDiagram); 284 | $('#delete-diagram').on('click', deleteDiagram); 285 | 286 | $('#start-rename-diagram').on('click', displayRenameInterface); 287 | $('.djs-rename').on('change', renameDiagram); 288 | $('.djs-rename').on('keyup', handleEndRenameEvent); 289 | $('#end-rename-diagram').on('click', handleEndRenameEvent); 290 | }); 291 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/context-pad/OpenProcessContextPad.js: -------------------------------------------------------------------------------- 1 | import { getBusinessObject, is } from 'bpmn-js/lib/util/ModelUtil'; 2 | import { find } from 'min-dash'; 3 | 4 | export default class CustomContextPad { 5 | constructor(config, eventBus, contextPad, injector, translate) { 6 | this.translate = translate; 7 | this.eventBus = eventBus; 8 | 9 | if (config.diagramUtil !== false) { 10 | this.diagramUtil = injector.get('diagramUtil', false); 11 | } 12 | 13 | contextPad.registerProvider(this); 14 | } 15 | 16 | getContextPadEntries(element) { 17 | const { 18 | translate, 19 | diagramUtil, 20 | eventBus 21 | } = this; 22 | 23 | eventBus.on('element.dblclick', 1500, function(event) { 24 | if (isInternalCallActivity(event.element)) { 25 | 26 | // do your stuff here 27 | openProcess(event, event.element); 28 | 29 | // stop propagating the event to prevent the default behavior 30 | event.stopPropagation(); 31 | } 32 | }); 33 | 34 | function getDiagram(rootElementId) { 35 | return find(diagramUtil.diagrams(), function(diagram) { 36 | return diagram.plane.bpmnElement && diagram.plane.bpmnElement.id === rootElementId; 37 | }); 38 | } 39 | 40 | function openProcess(_event, element) { 41 | let bo = getBusinessObject(element); 42 | let calledElement = bo.get('calledElement'); 43 | if (calledElement.startsWith('inner:')) { 44 | let diagram = getDiagram(calledElement.replace(/^(inner:)/, '')); 45 | if (diagram) { 46 | eventBus.fire('diagram.switch', { diagram: diagram }); 47 | } 48 | } 49 | } 50 | 51 | function isInternalCallActivity(element) { 52 | let bo = getBusinessObject(element); 53 | 54 | return is(element, 'bpmn:CallActivity') 55 | && diagramUtil.diagrams().length > 1 56 | && (typeof bo.get('calledElement') !== 'undefined') 57 | && bo.get('calledElement').startsWith('inner:'); 58 | } 59 | 60 | let newContext = {}; 61 | if (isInternalCallActivity(element)) { 62 | newContext = { 63 | 'open.process': { 64 | group: 'model', 65 | className: 'bpmn-icon-hand-tool', 66 | title: translate('Open process'), 67 | action: { 68 | click: openProcess, 69 | } 70 | } 71 | }; 72 | } 73 | return newContext; 74 | } 75 | } 76 | 77 | CustomContextPad.$inject = [ 78 | 'config', 79 | 'eventBus', 80 | 'contextPad', 81 | 'injector', 82 | 'translate' 83 | ]; 84 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/context-pad/index.js: -------------------------------------------------------------------------------- 1 | import OpenProcessContextPad from './OpenProcessContextPad'; 2 | 3 | /** 4 | * A bpmn-js module, defining all extension services and their dependencies. 5 | * 6 | * 7 | */ 8 | export default { 9 | __init__: [ 'openProcessContextPad' ], 10 | openProcessContextPad: [ 'type', OpenProcessContextPad ] 11 | }; 12 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/multi-diagram/COPYING: -------------------------------------------------------------------------------- 1 | All code is copyright 2018 from chor-js project under the MIT license. 2 | 3 | MIT License 4 | 5 | Copyright (c) 2018 Jan Ladleif 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/multi-diagram/DiagramSwitch.js: -------------------------------------------------------------------------------- 1 | import CreateDiagramHandler from './cmd/CreateDiagramHandler'; 2 | import DeleteDiagramHandler from './cmd/DeleteDiagramHandler'; 3 | import RenameDiagramHandler from './cmd/RenameDiagramHandler'; 4 | 5 | export default function DiagramSwitch(eventBus, commandStack, diagramUtil) { 6 | this._eventBus = eventBus; 7 | this._commandStack = commandStack; 8 | this._diagramUtil = diagramUtil; 9 | 10 | // Bind this globally 11 | this.registerHandlers = registerHandlers.bind(this); 12 | 13 | // Register to events 14 | this._eventBus.on('diagram.init', this.registerHandlers); 15 | } 16 | 17 | function registerHandlers() { 18 | 19 | this._commandStack.registerHandler('diagram.create', CreateDiagramHandler); 20 | this._commandStack.registerHandler('diagram.delete', DeleteDiagramHandler); 21 | this._commandStack.registerHandler('diagram.rename', RenameDiagramHandler); 22 | } 23 | 24 | DiagramSwitch.$inject = [ 25 | 'eventBus', 26 | 'commandStack', 27 | 'diagramUtil' 28 | ]; 29 | 30 | DiagramSwitch.prototype.addDiagram = function() { 31 | this._commandStack.execute('diagram.create', {}); 32 | }; 33 | 34 | DiagramSwitch.prototype.deleteDiagram = function() { 35 | if (this._commandStack.canExecute('diagram.delete', {})) { 36 | this._commandStack.execute('diagram.delete', {}); 37 | } 38 | }; 39 | 40 | DiagramSwitch.prototype.renameDiagram = function(name) { 41 | if (this._commandStack.canExecute('diagram.rename', { newName: name })) { 42 | this._commandStack.execute('diagram.rename', { 43 | newName: name 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/multi-diagram/cmd/CreateDiagramHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handler which creates a new diagram in the same bpmn file. 3 | */ 4 | export default function CreateDiagramHandler(bpmnjs, bpmnFactory, diagramUtil, commandStack) { 5 | 6 | this._bpmnjs = bpmnjs; 7 | this._bpmnFactory = bpmnFactory; 8 | this._diagramUtil = diagramUtil; 9 | this._commandStack = commandStack; 10 | } 11 | 12 | CreateDiagramHandler.$inject = [ 13 | 'bpmnjs', 14 | 'bpmnFactory', 15 | 'diagramUtil', 16 | 'commandStack' 17 | ]; 18 | 19 | CreateDiagramHandler.prototype.createProcess = function() { 20 | const process = this._bpmnFactory.create('bpmn:Process', {}); 21 | process.$parent = this._diagramUtil.definitions(); 22 | return process; 23 | }; 24 | 25 | CreateDiagramHandler.prototype.createDiagram = function(rootElement) { 26 | const plane = this._bpmnFactory.createDiPlane(rootElement); 27 | const diagram = this._bpmnFactory.create('bpmndi:BPMNDiagram', { 28 | plane: plane 29 | }); 30 | plane.$parent = diagram; 31 | diagram.$parent = this._diagramUtil.definitions(); 32 | return diagram; 33 | }; 34 | 35 | CreateDiagramHandler.prototype.preExecute = function(context) { 36 | 37 | context.oldDiagramId = this._diagramUtil.currentDiagram().id; 38 | 39 | // create new semantic objects 40 | const newProcess = this.createProcess(); 41 | const newDiagram = this.createDiagram(newProcess); 42 | 43 | // store them in the context 44 | context.newProcess = newProcess; 45 | context.newDiagram = newDiagram; 46 | }; 47 | 48 | CreateDiagramHandler.prototype.execute = function(context) { 49 | this._diagramUtil.definitions().rootElements.push(context.newProcess); 50 | this._bpmnjs._definitions.diagrams.push(context.newDiagram); 51 | 52 | this._bpmnjs.open(context.newDiagram.id); 53 | }; 54 | 55 | CreateDiagramHandler.prototype.revert = function(context) { 56 | this._diagramUtil.removeDiagramById(context.newProcess.id); 57 | this._bpmnjs.open(context.oldDiagramId); 58 | }; 59 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/multi-diagram/cmd/DeleteDiagramHandler.js: -------------------------------------------------------------------------------- 1 | import { find } from 'min-dash'; 2 | 3 | /** 4 | * Handler which deletes the currently displayed diagram. 5 | */ 6 | export default function DeleteDiagramHandler(bpmnjs, commandStack, diagramUtil) { 7 | this._bpmnjs = bpmnjs; 8 | this._commandStack = commandStack; 9 | this._diagramUtil = diagramUtil; 10 | } 11 | 12 | DeleteDiagramHandler.$inject = [ 13 | 'bpmnjs', 14 | 'commandStack', 15 | 'diagramUtil' 16 | ]; 17 | 18 | // eslint-disable-next-line no-unused-vars 19 | DeleteDiagramHandler.prototype.canExecute = function(_context) { 20 | return this._diagramUtil.diagrams().length > 1; 21 | }; 22 | 23 | DeleteDiagramHandler.prototype.preExecute = function(context) { 24 | const diagrams = this._diagramUtil.diagrams(); 25 | if (context.diagram) { 26 | const diagramToRemove = find(diagrams, diagram => diagram.id === context.diagram); 27 | context.removedProcess = { ...diagramToRemove.plane.bpmnElement }; 28 | context.removedDiagram = diagramToRemove; 29 | } else { 30 | context.removedProcess = this._diagramUtil.currentRootElement(); 31 | context.removedDiagram = this._diagramUtil.currentDiagram(); 32 | } 33 | 34 | // switch to the first diagram in the list that is not to be deleted 35 | const otherDiagramId = find(diagrams, function(diagram) { 36 | return (diagram.id !== context.diagram); 37 | }).id; 38 | this._bpmnjs.open(otherDiagramId); 39 | }; 40 | 41 | DeleteDiagramHandler.prototype.execute = function(context) { 42 | context.indices = this._diagramUtil.removeDiagramById(context.removedProcess.id); 43 | }; 44 | 45 | DeleteDiagramHandler.prototype.revert = function(context) { 46 | 47 | // reinsert the rootElement and diagram 48 | this._bpmnjs._definitions.diagrams.push(context.removedDiagram); 49 | this._bpmnjs._definitions.rootElements.push(context.removedProcess); 50 | }; 51 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/multi-diagram/cmd/RenameDiagramHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handler which renames the currently displayed diagram. 3 | */ 4 | export default function RenameDiagramHandler(diagramUtil) { 5 | 6 | this._diagramUtil = diagramUtil; 7 | } 8 | 9 | RenameDiagramHandler.$inject = [ 10 | 'diagramUtil' 11 | ]; 12 | 13 | RenameDiagramHandler.prototype.canExecute = function(context) { 14 | return context.newName.length > 0; 15 | }; 16 | 17 | RenameDiagramHandler.prototype.preExecute = function(context) { 18 | context.oldName = this._diagramUtil.currentDiagram().id; 19 | }; 20 | 21 | RenameDiagramHandler.prototype.execute = function(context) { 22 | this._diagramUtil.currentDiagram().id = context.newName; 23 | }; 24 | 25 | RenameDiagramHandler.prototype.revert = function(context) { 26 | this._diagramUtil.currentDiagram().id = context.oldName; 27 | }; 28 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/multi-diagram/index.js: -------------------------------------------------------------------------------- 1 | import DiagramSwitch from './DiagramSwitch'; 2 | import DiagramUtil from './utils'; 3 | 4 | export default { 5 | __init__: [ 6 | 'diagramSwitch' 7 | ], 8 | __depends__: [ 9 | DiagramUtil 10 | ], 11 | diagramSwitch: [ 'type', DiagramSwitch ] 12 | }; 13 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/multi-diagram/utils/DiagramUtil.js: -------------------------------------------------------------------------------- 1 | import { find, findIndex } from 'min-dash'; 2 | 3 | export default function DiagramUtil(bpmnjs, canvas) { 4 | 5 | this._bpmnjs = bpmnjs; 6 | this._canvas = canvas; 7 | } 8 | 9 | DiagramUtil.$inject = [ 10 | 'bpmnjs', 11 | 'canvas' 12 | ]; 13 | 14 | DiagramUtil.prototype.currentRootElement = function() { 15 | return this._canvas.getRootElement().businessObject; 16 | }; 17 | 18 | DiagramUtil.prototype.currentDiagram = function() { 19 | const currentRootElement = this.currentRootElement(); 20 | if (currentRootElement) { 21 | return find(this.diagrams(), function(diagram) { 22 | return diagram.plane.bpmnElement && diagram.plane.bpmnElement.id === currentRootElement.id; 23 | }); 24 | } 25 | }; 26 | 27 | DiagramUtil.prototype.definitions = function() { 28 | return this._bpmnjs._definitions || []; 29 | }; 30 | 31 | DiagramUtil.prototype.isCollaboration = function() { 32 | return this.definitions()?.rootElements?.filter(rootElement => rootElement.$type === 'bpmn:Collaboration').length > 0; 33 | }; 34 | 35 | DiagramUtil.prototype.diagrams = function() { 36 | return this.definitions()?.diagrams || []; 37 | }; 38 | 39 | DiagramUtil.prototype.removeDiagramById = function(rootElementId) { 40 | const elementIndex = findIndex(this.definitions().rootElements, function(rootElement) { 41 | return rootElement.id === rootElementId; 42 | }); 43 | 44 | if (elementIndex >= 0) { 45 | this.definitions().rootElements.splice(elementIndex, 1); 46 | } else { 47 | throw new Error('could not find root element with ID ' + rootElementId); 48 | } 49 | 50 | const diagramIndex = findIndex(this.definitions().diagrams, function(diagram) { 51 | return diagram.plane.bpmnElement && diagram.plane.bpmnElement.id === rootElementId; 52 | }); 53 | 54 | if (diagramIndex >= 0) { 55 | this.definitions().diagrams.splice(diagramIndex, 1); 56 | } else { 57 | throw new Error('could not find diagram for ID ' + rootElementId); 58 | } 59 | 60 | return { 61 | elementIndex: elementIndex, 62 | diagramIndex: diagramIndex 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /client/bpmn-js-extension/multi-diagram/utils/index.js: -------------------------------------------------------------------------------- 1 | import DiagramUtil from './DiagramUtil'; 2 | 3 | export default { 4 | __init__: [ 5 | 'diagramUtil' 6 | ], 7 | diagramUtil: [ 'type', DiagramUtil ] 8 | }; 9 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | registerBpmnJSPlugin, 3 | registerClientExtension 4 | } from 'camunda-modeler-plugin-helpers'; 5 | 6 | import MultiDiagramButton from './react/MultiDiagramButton'; 7 | 8 | import MultiDiagramFeatures from './bpmn-js-extension/multi-diagram'; 9 | import ProcessContextPad from './bpmn-js-extension/context-pad'; 10 | import CallActivityExt from './properties-provider'; 11 | 12 | registerBpmnJSPlugin(MultiDiagramFeatures); 13 | registerBpmnJSPlugin(ProcessContextPad); 14 | registerBpmnJSPlugin(CallActivityExt); 15 | 16 | registerClientExtension(MultiDiagramButton); -------------------------------------------------------------------------------- /client/properties-provider/CallActivityPropertiesProvider.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { is, getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 4 | import { find } from 'min-dash'; 5 | 6 | import calledTypeProps from './props/CalledTypeProps'; 7 | import calledElementProps from './props/CalledElementProps'; 8 | 9 | function getCallableType(element) { 10 | const bo = getBusinessObject(element); 11 | 12 | const boCalledElement = bo.get('calledElement'), 13 | boCaseRef = bo.get('camunda:caseRef'); 14 | 15 | let callActivityType = ''; 16 | if (typeof boCalledElement !== 'undefined') { 17 | callActivityType = 'bpmn'; 18 | } else if (typeof boCaseRef !== 'undefined') { 19 | callActivityType = 'cmmn'; 20 | } 21 | 22 | return callActivityType; 23 | } 24 | 25 | function isInternal(element) { 26 | const bo = getBusinessObject(element); 27 | const boCalledElement = bo.get('calledElement'); 28 | return !!(typeof boCalledElement !== 'undefined' && 29 | boCalledElement.startsWith('inner:')); 30 | } 31 | 32 | /** 33 | * A provider for CallActivity elements, to open the global subprocess of the BPMN 34 | * @constructor 35 | */ 36 | export default class CallActivityPropertiesProvider { 37 | 38 | constructor(propertiesPanel, injector) { 39 | const eventBus = injector.get('eventBus'); 40 | const bpmnjs = injector.get('bpmnjs'); 41 | 42 | this.diagramUtil = injector.get('diagramUtil'); 43 | 44 | // Not sure it's the right place but whatever... 45 | eventBus.on('diagram.switch', 10000, (event) => { 46 | bpmnjs.open(event.diagram.id); 47 | }); 48 | 49 | propertiesPanel.registerProvider(200, this); 50 | } 51 | 52 | 53 | /** 54 | * Return the groups provided for the given element. 55 | * 56 | * @param {DiagramElement} element 57 | * 58 | * @return {(Object[]) => (Object[])} groups middleware 59 | */ 60 | getGroups(element) { 61 | 62 | /** 63 | * We return a middleware that modifies 64 | * the existing groups. 65 | * 66 | * @param {Object[]} groups 67 | * 68 | * @return {Object[]} modified groups 69 | */ 70 | return groups => { 71 | 72 | if (is(element, 'bpmn:CallActivity') && this.diagramUtil.diagrams().length > 1 && getCallableType(element) === 'bpmn') { 73 | 74 | let calledElement = find(groups, (entry) => entry.id === 'CamundaPlatform__CallActivity'); 75 | 76 | if (calledElement) { 77 | calledElement.entries.push(...calledTypeProps(element)); 78 | 79 | if (isInternal(element)) { 80 | calledElement.entries.push(...calledElementProps(element)); 81 | } 82 | } 83 | } 84 | 85 | return groups; 86 | }; 87 | 88 | }; 89 | 90 | 91 | } 92 | 93 | CallActivityPropertiesProvider.prototype.getCallableType = function(element) { 94 | return getCallableType(element); 95 | }; 96 | 97 | CallActivityPropertiesProvider.$inject = [ 'propertiesPanel', 'injector' ]; -------------------------------------------------------------------------------- /client/properties-provider/index.js: -------------------------------------------------------------------------------- 1 | import CallActivityPropertiesProvider from './CallActivityPropertiesProvider'; 2 | 3 | /** 4 | * A bpmn-js module, defining all extension services and their dependencies. 5 | * 6 | * 7 | */ 8 | export default { 9 | __init__: [ 'CallableProcessProvider' ], 10 | CallableProcessProvider: [ 'type', CallActivityPropertiesProvider ] 11 | }; 12 | -------------------------------------------------------------------------------- /client/properties-provider/props/CalledElementProps.js: -------------------------------------------------------------------------------- 1 | import { SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | 4 | export default function(element) { 5 | return [ 6 | { 7 | id: 'callableInnerElementRef', 8 | element, 9 | component: CalledElementRef, 10 | isEdited: isSelectEntryEdited 11 | } 12 | ]; 13 | } 14 | 15 | function CalledElementRef(props) { 16 | const { element, id } = props; 17 | 18 | const modeling = useService('modeling'); 19 | const translate = useService('translate'); 20 | const debounce = useService('debounceInput'); 21 | const diagramUtil = useService('diagramUtil'); 22 | 23 | const getValue = () => { 24 | return element.businessObject.calledElement.replace(/^(inner:)/, '') || ''; 25 | }; 26 | 27 | const setValue = value => { 28 | return modeling.updateProperties(element, { 29 | calledElement: 'inner:' + value || 'inner:' 30 | }); 31 | }; 32 | 33 | const getOptions = () => { 34 | return [ 35 | ...diagramUtil.definitions().rootElements 36 | .filter((rootElement) => rootElement.$type === 'bpmn:Process' && rootElement.id !== diagramUtil.currentRootElement().id) 37 | .map((rootElement) => { 38 | return { label: rootElement.id, value: rootElement.id }; 39 | }) 40 | ]; 41 | }; 42 | 43 | // const validate = (value) => { 44 | // const businessObject = getBusinessObject(element); 45 | // return 46 | // }; 47 | // return ; 57 | 58 | return SelectEntry({ 59 | element, 60 | id, 61 | label: translate('Called inner element reference'), 62 | getValue, 63 | setValue, 64 | getOptions, 65 | debounce 66 | }); 67 | } -------------------------------------------------------------------------------- /client/properties-provider/props/CalledTypeProps.js: -------------------------------------------------------------------------------- 1 | import { SelectEntry, isSelectEntryEdited } from '@bpmn-io/properties-panel'; 2 | import { useService } from 'bpmn-js-properties-panel'; 3 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 4 | 5 | export default function(element) { 6 | return [ 7 | { 8 | id: 'callableElementTypeRef', 9 | element, 10 | component: CalledElementType, 11 | isEdited: isSelectEntryEdited 12 | } 13 | ]; 14 | } 15 | 16 | function getCalledElementType(element) { 17 | const bo = getBusinessObject(element); 18 | const boCalledElement = bo.get('calledElement'); 19 | 20 | let calledElementType = 'external'; 21 | if (typeof boCalledElement !== 'undefined' && 22 | boCalledElement.startsWith('inner:')) { 23 | calledElementType = 'internal'; 24 | } 25 | 26 | return calledElementType; 27 | } 28 | 29 | function CalledElementType(props) { 30 | const { element, id } = props; 31 | 32 | const modeling = useService('modeling'); 33 | const translate = useService('translate'); 34 | const debounce = useService('debounceInput'); 35 | 36 | const getValue = () => { 37 | return getCalledElementType(element); 38 | }; 39 | 40 | const setValue = value => { 41 | let calledElement; 42 | if (value === 'internal') { 43 | calledElement = 'inner:'; 44 | } else if (value === 'external') { 45 | calledElement = ''; 46 | } 47 | return modeling.updateProperties(element, { 48 | calledElement: calledElement 49 | }); 50 | }; 51 | 52 | const getOptions = () => { 53 | return [ 54 | { label: 'INTERNAL', value: 'internal' }, 55 | { label: 'EXTERNAL', value: 'external' } 56 | ]; 57 | }; 58 | 59 | // return ; 69 | return SelectEntry({ 70 | element, 71 | id, 72 | label: translate('Called Element Type'), 73 | getValue, 74 | setValue, 75 | getOptions, 76 | debounce 77 | }); 78 | } -------------------------------------------------------------------------------- /client/react/DiagramButtonsOverlay.js: -------------------------------------------------------------------------------- 1 | import React from 'camunda-modeler-plugin-helpers/react'; 2 | import { Overlay, Section } from 'camunda-modeler-plugin-helpers/components'; 3 | 4 | const OFFSET = { right: 0 }; 5 | 6 | import classNames from 'classnames'; 7 | import PlusIcon from '../../resources/plus-solid.svg'; 8 | import MinusIcon from '../../resources/minus-solid.svg'; 9 | 10 | // we can even use hooks to render into the application 11 | export default function DiagramButtonsOverlay({ anchor, initValues, onClose, actions }) { 12 | 13 | return ( 14 | 15 |
16 | Global subprocesses configuration 17 | 18 |
19 | 20 | 25 | 26 | {initValues?.diagrams?.map((diagram, index) => ( 27 |
28 |
actions.switchDiagram(diagram)} 30 | className={classNames('btn', initValues.activeDiagram === diagram ? 'btn-primary' : 'btn-secondary', 'diagram-name')}> 31 | {diagram} 32 |
33 | 39 |
40 | ))} 41 |
42 |
43 |
44 |
45 | ); 46 | } -------------------------------------------------------------------------------- /client/react/MultiDiagramButton.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment, PureComponent } from 'camunda-modeler-plugin-helpers/react'; 2 | import { Fill } from 'camunda-modeler-plugin-helpers/components'; 3 | 4 | import SubProcessIcon from '../../resources/subprocess-collapsed.svg'; 5 | 6 | import classNames from 'classnames'; 7 | import { find } from 'min-dash'; 8 | import DiagramButtonsOverlay from './DiagramButtonsOverlay'; 9 | 10 | const defaultState = { 11 | modeler: null, 12 | tabModeler: [], 13 | diagrams: [], 14 | activeDiagram: null, 15 | multi: false, 16 | collaboration: false, 17 | configOpen: false 18 | }; 19 | 20 | export default class MultiDiagramButton extends PureComponent { 21 | 22 | constructor(props) { 23 | super(props); 24 | 25 | this.state = defaultState; 26 | 27 | this._multiDiagramButtonRef = React.createRef(); 28 | 29 | this.handleConfigClosed = this.handleConfigClosed.bind(this); 30 | } 31 | 32 | componentDidMount() { 33 | 34 | /** 35 | * The component props include everything the Application offers plugins, 36 | * which includes: 37 | * - config: save and retrieve information to the local configuration 38 | * - subscribe: hook into application events, like , ... 39 | * - triggerAction: execute editor actions, like , ... 40 | * - log: log information into the Log panel 41 | * - displayNotification: show notifications inside the application 42 | */ 43 | const { 44 | // eslint-disable-next-line react/prop-types 45 | subscribe 46 | } = this.props; 47 | 48 | subscribe('bpmn.modeler.created', ({ modeler, tab }) => { 49 | const { tabModeler } = this.state; 50 | this.setState({ 51 | modeler: modeler, 52 | tabModeler: [ ...tabModeler, { tabId: tab.id, modeler: modeler } ] 53 | }); 54 | 55 | const eventBus = modeler.get('eventBus'); 56 | const diagramUtil = modeler.get('diagramUtil'); 57 | eventBus.on('import.done', () => { 58 | let bpmnjs = modeler.get('bpmnjs'); 59 | let isMultiDiagram = bpmnjs._definitions && diagramUtil.diagrams.length > 1; 60 | let isCollaboration = bpmnjs._definitions && diagramUtil.isCollaboration(); 61 | this.setState({ 62 | activeDiagram: diagramUtil.currentDiagram(), 63 | diagrams: diagramUtil.diagrams().map(d => d.id), 64 | multi: isMultiDiagram, 65 | collaboration: isCollaboration 66 | }); 67 | }); 68 | 69 | eventBus.on('commandStack.diagram.create.executed', (command) => 70 | this.setState({ 71 | activeDiagram: command.context.newDiagram, 72 | diagrams: diagramUtil.diagrams().map(d => d.id) 73 | }) 74 | ); 75 | 76 | eventBus.on('commandStack.diagram.delete.executed', () => 77 | this.setState({ 78 | activeDiagram: diagramUtil.currentDiagram(), 79 | diagrams: diagramUtil.diagrams().map(d => d.id) 80 | }) 81 | ); 82 | 83 | eventBus.on([ 'commandStack.diagram.create.reverted', 'commandStack.diagram.delete.reverted' ], () => 84 | this.setState({ 85 | activeDiagram: diagramUtil.currentDiagram(), 86 | diagrams: diagramUtil.diagrams().map(d => d.id), 87 | multi: diagramUtil.diagrams.length > 1, 88 | collaboration: diagramUtil.isCollaboration() 89 | }) 90 | ); 91 | 92 | eventBus.on('diagram.switch', () => { 93 | this.setState({ activeDiagram: diagramUtil.currentDiagram() }); 94 | }); 95 | 96 | }); 97 | 98 | subscribe('app.activeTabChanged', (tab) => { 99 | const { 100 | tabModeler 101 | } = this.state; 102 | let activeTabId = tab.activeTab.id; 103 | const activeModeler = find(tabModeler, t => t.tabId === activeTabId); 104 | if (activeModeler) { 105 | let bpmnjs = activeModeler.modeler.get('bpmnjs'); 106 | let diagramUtil = activeModeler.modeler.get('diagramUtil'); 107 | let isMultiDiagram = bpmnjs._definitions && diagramUtil.diagrams().length > 1; 108 | let isCollaboration = bpmnjs._definitions && diagramUtil.isCollaboration(); 109 | let diagramNames = bpmnjs._definitions && diagramUtil.diagrams().map(d => d.id) || []; 110 | this.setState({ 111 | modeler: activeModeler.modeler, 112 | multi: isMultiDiagram, 113 | collaboration: isCollaboration, 114 | activeDiagram: diagramUtil.currentDiagram(), 115 | diagrams: diagramNames 116 | }); 117 | } else { 118 | this.setState(defaultState); 119 | } 120 | }); 121 | 122 | } 123 | 124 | addDiagram = () => { 125 | const { modeler } = this.state; 126 | const commandStack = modeler.get('commandStack'); 127 | commandStack.execute('diagram.create', {}); 128 | this.setState({ multi: true }); 129 | }; 130 | 131 | deleteDiagram = (diagramId) => { 132 | const { modeler } = this.state; 133 | const diagramUtil = modeler.get('diagramUtil'); 134 | 135 | if (diagramUtil.diagrams().length > 1) { 136 | let stillMulti = (diagramUtil.diagrams().length - 1) > 1; 137 | const commandStack = modeler.get('commandStack'); 138 | commandStack.execute('diagram.delete', { diagram: diagramId }); 139 | 140 | this.setState({ multi: stillMulti }); 141 | } 142 | }; 143 | 144 | switchDiagram = (diagramId) => { 145 | const { modeler } = this.state; 146 | const eventBus = modeler.get('eventBus'); 147 | 148 | eventBus.fire('diagram.switch', { diagram: { id: diagramId } }); 149 | }; 150 | 151 | handleConfigClosed = () => { 152 | this.setState({ configOpen: false }); 153 | }; 154 | 155 | /** 156 | * render any React component you like to extend the existing 157 | * Camunda Modeler application UI 158 | */ 159 | render() { 160 | const { configOpen, activeDiagram, diagrams } = this.state; 161 | let initValues = { activeDiagram: (activeDiagram ? activeDiagram.id : undefined), diagrams }; 162 | 163 | // we can use fills to hook React components into certain places of the UI 164 | return 165 | 166 | 173 | 174 | { 175 | configOpen && ( 176 | 185 | ) 186 | } 187 | ; 188 | } 189 | } -------------------------------------------------------------------------------- /client/react/modals/rename-diagram/RenameDiagramModal.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState } from 'camunda-modeler-plugin-helpers/react'; 4 | import { Modal } from 'camunda-modeler-plugin-helpers/components'; 5 | 6 | // polyfill upcoming structural components 7 | const Title = Modal.Title || (({ children }) =>

{ children }

); 8 | const Body = Modal.Body || (({ children }) =>
{ children }
); 9 | const Footer = Modal.Footer || (({ children }) =>
{ children }
); 10 | 11 | export default function RenameDiagramModal({ initValues, onRename, onClose }) { 12 | 13 | const [ activeDiagram, setActiveDiagram ] = useState(initValues.activeDiagram); 14 | const onSubmit = () => onRename({ activeDiagram }); 15 | 16 | return 17 | Rename current diagram 18 | 19 | 20 |
21 |

22 | 31 |

32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 |
; 41 | } 42 | -------------------------------------------------------------------------------- /client/style.css: -------------------------------------------------------------------------------- 1 | div.multi-diagram > div { 2 | margin-bottom: 5px; 3 | } 4 | 5 | div.multi-diagram div.btn + .btn { 6 | margin-left: unset; 7 | } 8 | 9 | div.multi-diagram .diagram-button { 10 | min-width: unset; 11 | } 12 | 13 | div.multi-diagram .add-new-diagram { 14 | margin: 0 0 10px 82%; 15 | } 16 | 17 | div.multi-diagram .diagram-entry { 18 | display: flex; 19 | } 20 | 21 | div.diagram-entry .diagram-name { 22 | width: 75%; 23 | margin-right: 5%; 24 | } 25 | 26 | div.diagram-entry .remove-diagram { 27 | width: 20%; 28 | } 29 | 30 | button.multi-diagram:disabled { 31 | background-color: rgba(19, 1, 1, 0.3) !important; 32 | color: rgba(255, 255, 255, 0.3) !important; 33 | border-color: rgba(195, 195, 195, 0.3) !important; 34 | } -------------------------------------------------------------------------------- /docs/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharedchains/camunda-modeler-plugin-multidiagram/204871e65353b421796da1b9209c13b6b8a9d09a/docs/screencast.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: 'Multi-diagram model Plug-in', 5 | script: './dist/client.js', 6 | style: './dist/style.css' 7 | }; 8 | -------------------------------------------------------------------------------- /index.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: 'Multi-diagram model Plug-in', 5 | script: './client/client.js', 6 | style: './client/style.css' 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "camunda-modeler-plugin-multidiagram", 3 | "version": "2.0.2", 4 | "description": "The Camunda Modeler multidiagram plug-in", 5 | "keywords": [ 6 | "camunda", 7 | "modeler", 8 | "plugin", 9 | "multi", 10 | "diagram", 11 | "multi-diagram" 12 | ], 13 | "main": "index.js", 14 | "scripts": { 15 | "all": "run-s build build:serve", 16 | "build": "webpack", 17 | "build:serve": "webpack --config webpack.config.serve.js", 18 | "start": "run-s build:serve serve", 19 | "dev": "run-p \"build:serve -- --watch\" serve", 20 | "serve": "sirv public --dev", 21 | "lint": "eslint . --fix", 22 | "test": "run-s lint all" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/sharedchains/camunda-modeler-plugin-multidiagram.git" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.17.7", 30 | "@babel/preset-react": "^7.16.7", 31 | "babel-loader": "^8.2.3", 32 | "camunda-modeler-plugin-helpers": "^5.1.0", 33 | "camunda-modeler-webpack-plugin": "^0.1.0", 34 | "copy-webpack-plugin": "^10.2.4", 35 | "css-loader": "^6.7.1", 36 | "eslint": "^7.32.0", 37 | "eslint-plugin-bpmn-io": "^0.13.0", 38 | "eslint-plugin-react": "^7.29.4", 39 | "file-loader": "^6.2.0", 40 | "less": "^4.1.2", 41 | "less-loader": "^10.2.0", 42 | "npm-run-all": "^4.1.5", 43 | "raw-loader": "^4.0.2", 44 | "react-svg-loader": "^3.0.3", 45 | "sirv-cli": "^2.0.2", 46 | "style-loader": "^3.3.1", 47 | "webpack": "^5.70.0", 48 | "webpack-cli": "^4.9.2", 49 | "webpack-sources": "^3.2.3", 50 | "zip-webpack-plugin": "^4.0.1" 51 | }, 52 | "dependencies": { 53 | "@bpmn-io/properties-panel": "^0.19.0", 54 | "bpmn-js": "^9.4.0", 55 | "bpmn-js-properties-panel": "^1.5.0", 56 | "camunda-bpmn-moddle": "^6.1.2", 57 | "classnames": "^2.3.1", 58 | "jquery": "^3.6.0", 59 | "min-dash": "^3.8.1", 60 | "min-dom": "^3.1.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /resources/minus-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/newDiagram.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/pencil-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/plus-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/subprocess-collapsed.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 38 | 40 | 48 | 53 | 54 | 55 | 57 | 58 | 60 | image/svg+xml 61 | 63 | 64 | 65 | 66 | 70 | 75 | 79 | 84 | 88 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /styles/app.less: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body, 6 | html { 7 | 8 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | 10 | font-size: 12px; 11 | 12 | height: 100%; 13 | max-height: 100%; 14 | padding: 0; 15 | margin: 0; 16 | } 17 | 18 | a:link { 19 | text-decoration: none; 20 | } 21 | 22 | .content { 23 | position: relative; 24 | width: 100%; 25 | height: 100%; 26 | 27 | > .message { 28 | width: 100%; 29 | height: 100%; 30 | text-align: center; 31 | display: table; 32 | 33 | font-size: 16px; 34 | color: #111; 35 | 36 | .note { 37 | vertical-align: middle; 38 | text-align: center; 39 | display: table-cell; 40 | } 41 | 42 | .error { 43 | .details { 44 | max-width: 500px; 45 | font-size: 12px; 46 | margin: 20px auto; 47 | text-align: left; 48 | } 49 | 50 | pre { 51 | border: solid 1px #CCC; 52 | background: #EEE; 53 | padding: 10px; 54 | } 55 | } 56 | } 57 | &:not(.with-error) .error, 58 | &.with-error .intro, 59 | &.with-diagram .intro { 60 | display: none; 61 | } 62 | 63 | .canvas { 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | right: 0; 68 | bottom: 0; 69 | } 70 | 71 | .canvas, 72 | &.with-error .canvas { 73 | visibility: hidden; 74 | } 75 | 76 | &.with-diagram .canvas { 77 | visibility: visible; 78 | } 79 | } 80 | 81 | 82 | .buttons { 83 | position: fixed; 84 | bottom: 20px; 85 | left: 20px; 86 | 87 | padding: 0; 88 | margin: 0; 89 | list-style: none; 90 | 91 | > li { 92 | display: inline-block; 93 | margin-right: 10px; 94 | 95 | > a { 96 | background: #DDD; 97 | border: solid 1px #666; 98 | display: inline-block; 99 | padding: 5px; 100 | } 101 | } 102 | 103 | a { 104 | opacity: 0.3; 105 | } 106 | 107 | a.active { 108 | opacity: 1.0; 109 | } 110 | } 111 | 112 | #js-properties-panel { 113 | position: absolute; 114 | top: 0; 115 | bottom: 0; 116 | right: 0; 117 | width: 260px; 118 | z-index: 10; 119 | border-left: 1px solid #ccc; 120 | overflow: auto; 121 | &:empty { 122 | display: none; 123 | } 124 | > .djs-properties-panel { 125 | padding-bottom: 70px; 126 | min-height:100%; 127 | } 128 | } 129 | 130 | 131 | .djs-select-wrapper, .djs-rename-wrapper { 132 | position: absolute; 133 | display: flex; 134 | left: 55px; 135 | top: 0px; 136 | padding: 8px; 137 | margin-top: -1px; 138 | height: 46px; 139 | background-color: #FAFAFA; 140 | border: 1px solid #CCCCCC; 141 | border-radius: 2px; 142 | } 143 | 144 | .two-column.open .djs-select-wrapper, 145 | .two-column.open .djs-rename-wrapper { 146 | left: 101px; 147 | } 148 | 149 | .djs-rename-wrapper { 150 | display: none; 151 | } 152 | 153 | .djs-select-wrapper > *:last-child, 154 | .djs-rename-wrapper > *:last-child { 155 | margin-right: 0; 156 | } 157 | 158 | .djs-select-wrapper > button { 159 | font-size: 20px; 160 | text-align: center; 161 | line-height: 50%; 162 | width: 30px; 163 | height: 30px; 164 | padding: 0px; 165 | 166 | background-color: transparent; 167 | border-style: none; 168 | } 169 | 170 | .djs-select-wrapper > button:hover { 171 | color: #FF7400; 172 | } 173 | 174 | .djs-rename, 175 | .djs-select { 176 | margin-right: 10px; 177 | } 178 | 179 | .djs-rename, 180 | .djs-select { 181 | width: 300px; 182 | height: 30px; 183 | padding-left: 10px; 184 | padding-right: 10px; 185 | border-radius: 0 !important; 186 | } 187 | 188 | .djs-select { 189 | -moz-appearance: none; 190 | -webkit-appearance: none; 191 | appearance: none; 192 | 193 | background-image: 194 | linear-gradient(45deg, transparent 50%, #A6A6A6 50%), 195 | linear-gradient(135deg, #A6A6A6 50%, transparent 50%); 196 | background-position: 197 | calc(100% - 15px) calc(1em + 0px), 198 | calc(100% - 10px) calc(1em + 0px); 199 | background-size: 200 | 5px 5px, 201 | 5px 5px; 202 | background-repeat: no-repeat; 203 | } 204 | 205 | .djs-select:focus { 206 | outline: none; 207 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const ZipPlugin = require('zip-webpack-plugin'); 4 | const CamundaModelerWebpackPlugin = require('camunda-modeler-webpack-plugin'); 5 | 6 | module.exports = { 7 | mode: 'development', 8 | entry: './client/index.js', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'client.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.svg$/, 17 | use: 'react-svg-loader' 18 | } 19 | ] 20 | }, 21 | devtool: 'cheap-module-source-map', 22 | plugins: [ 23 | new CamundaModelerWebpackPlugin(), 24 | new CopyPlugin({ 25 | patterns: [ 26 | { 27 | from: path.resolve(__dirname, './client/style.css'), 28 | to: path.resolve(__dirname, './dist/') 29 | }, 30 | { 31 | from: path.resolve(__dirname, './index.prod.js'), 32 | to: path.resolve(__dirname, './dist/index.js') 33 | } 34 | ] 35 | }), 36 | new ZipPlugin({ 37 | filename: process.env.npm_package_name + '-' + process.env.npm_package_version + '.zip', 38 | pathPrefix: process.env.npm_package_name + '/', 39 | pathMapper: function(assetPath) { 40 | if (assetPath.startsWith('client') || assetPath.startsWith('style')) { 41 | return path.join(path.dirname(assetPath), 'client', path.basename(assetPath)); 42 | } 43 | return assetPath; 44 | } 45 | }) 46 | ] 47 | }; -------------------------------------------------------------------------------- /webpack.config.serve.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require('copy-webpack-plugin'); 2 | 3 | const path = require('path'); 4 | const CamundaModelerWebpackPlugin = require('camunda-modeler-webpack-plugin'); 5 | 6 | const basePath = '.'; 7 | 8 | const absoluteBasePath = path.resolve(path.join(__dirname, basePath)); 9 | 10 | module.exports = { 11 | mode: 'development', 12 | entry: './app/index.js', 13 | output: { 14 | path: path.resolve(__dirname, 'public'), 15 | filename: 'index.js' 16 | }, 17 | devtool: 'source-map', 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.less$/i, 22 | use: [ 23 | 24 | // compiles Less to CSS 25 | 'style-loader', 26 | 'css-loader', 27 | 'less-loader' 28 | ] 29 | }, 30 | { 31 | test: /\.bpmn$/, 32 | use: { 33 | loader: 'raw-loader' 34 | } 35 | } 36 | ] 37 | }, 38 | resolve: { 39 | mainFields: [ 40 | 'browser', 41 | 'module', 42 | 'main' 43 | ], 44 | modules: [ 45 | 'node_modules', 46 | absoluteBasePath 47 | ] 48 | }, 49 | plugins: [ 50 | new CamundaModelerWebpackPlugin({ type: 'react' }), 51 | new CopyPlugin({ 52 | patterns: [ 53 | { from: 'app/index.html', to: '.' }, 54 | { from: 'node_modules/bpmn-js/dist/assets', to: 'vendor/bpmn-js/assets' }, 55 | { from: 'node_modules/bpmn-js-properties-panel/dist/assets', to: 'vendor/bpmn-js-properties-panel/assets' } 56 | ] 57 | }) 58 | ] 59 | }; --------------------------------------------------------------------------------