├── .gitignore ├── codecov.yml ├── resources └── screenshot.png ├── test ├── testBundle.js ├── coverageBundle.js ├── spec │ ├── create-append-anything │ │ ├── create-menu │ │ │ ├── CreateMenuProvider.bpmn │ │ │ ├── CreatePaletteProvider.spec.js │ │ │ └── CreateMenuProvider.spec.js │ │ ├── append-menu │ │ │ ├── AppendMenuProvider.bpmn │ │ │ ├── AppendRules.bpmn │ │ │ ├── AppendContextPadProvider.spec.js │ │ │ ├── AppendRules.spec.js │ │ │ └── AppendMenuProvider.spec.js │ │ ├── CreateAppendAnything.spec.js │ │ ├── editor-actions │ │ │ └── CreateAppendEditorActions.spec.js │ │ └── keyboard-bindings │ │ │ └── CreateAppendKeyboardBindings.spec.js │ └── element-templates │ │ ├── create-menu │ │ ├── ElementTemplatesCreateProvider.bpmn │ │ └── ElementTemplatesCreateProvider.spec.js │ │ ├── append-menu │ │ ├── ElementTemplatesAppendProvider.bpmn │ │ └── ElementTemplatesAppendProvider.spec.js │ │ ├── replace-menu │ │ ├── RemoveTemplateReplaceProvider.bpmn │ │ ├── ElementTemplatesReplaceProvider.bpmn │ │ ├── RemoveTemplateReplaceProvider.spec.js │ │ ├── RemoveTemplateReplaceProvider.element-templates.json │ │ └── ElementTemplatesReplaceProvider.spec.js │ │ └── CreateAppendElementTemplates.spec.js ├── test.css ├── fixtures │ ├── simple.bpmn │ └── element-templates.json └── TestHelper.js ├── renovate.json ├── lib ├── util │ ├── ReplaceOptionsUtil.js │ └── CreateOptionsUtil.js ├── index.js ├── element-templates │ ├── append-menu │ │ ├── index.js │ │ └── ElementTemplatesAppendProvider.js │ ├── create-menu │ │ ├── index.js │ │ └── ElementTemplatesCreateProvider.js │ ├── remove-templates │ │ ├── index.js │ │ └── RemoveTemplateReplaceProvider.js │ ├── replace-menu │ │ ├── index.js │ │ └── ElementTemplatesReplaceProvider.js │ └── index.js ├── icons │ ├── resources │ │ ├── append.svg │ │ └── create.svg │ └── Icons.js └── create-append-anything │ ├── create-menu │ ├── index.js │ ├── CreatePaletteProvider.js │ └── CreateMenuProvider.js │ ├── index.js │ ├── editor-actions │ ├── index.js │ └── EditorActions.js │ ├── keyboard-bindings │ ├── index.js │ └── KeyboardBindings.js │ └── append-menu │ ├── index.js │ ├── AppendRules.js │ ├── AppendContextPadProvider.js │ └── AppendMenuProvider.js ├── rollup.config.js ├── .github └── workflows │ └── CI.yml ├── eslint.config.mjs ├── LICENSE ├── README.md ├── CHANGELOG.md ├── karma.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | 3 | parsers: 4 | javascript: 5 | enable_partials: yes -------------------------------------------------------------------------------- /resources/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bpmn-io/bpmn-js-create-append-anything/HEAD/resources/screenshot.png -------------------------------------------------------------------------------- /test/testBundle.js: -------------------------------------------------------------------------------- 1 | const allTests = require.context('.', true, /.spec\.js$/); 2 | 3 | allTests.keys().forEach(allTests); -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>bpmn-io/renovate-config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/coverageBundle.js: -------------------------------------------------------------------------------- 1 | const allTests = require.context('.', true, /.spec\.js$/); 2 | 3 | allTests.keys().forEach(allTests); 4 | 5 | const allSources = require.context('../lib', true, /.*\.js$/); 6 | 7 | allSources.keys().forEach(allSources); -------------------------------------------------------------------------------- /lib/util/ReplaceOptionsUtil.js: -------------------------------------------------------------------------------- 1 | import * as replaceOptions from 'bpmn-js/lib/features/replace/ReplaceOptions'; 2 | 3 | const ALL_OPTIONS = Object.values(replaceOptions); 4 | 5 | export function getReplaceOptionGroups() { 6 | return ALL_OPTIONS; 7 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export { default as CreateAppendAnythingModule } from './create-append-anything'; 2 | export { default as CreateAppendElementTemplatesModule } from './element-templates'; 3 | export { default as RemoveTemplatesModule } from './element-templates/remove-templates'; -------------------------------------------------------------------------------- /lib/element-templates/append-menu/index.js: -------------------------------------------------------------------------------- 1 | import ElementTemplatesAppendProvider from './ElementTemplatesAppendProvider'; 2 | 3 | export default { 4 | __init__: [ 'elementTemplatesAppendProvider' ], 5 | elementTemplatesAppendProvider: [ 'type', ElementTemplatesAppendProvider ] 6 | }; -------------------------------------------------------------------------------- /lib/element-templates/create-menu/index.js: -------------------------------------------------------------------------------- 1 | import ElementTemplatesCreateProvider from './ElementTemplatesCreateProvider'; 2 | 3 | export default { 4 | __init__: [ 'elementTemplatesCreateProvider' ], 5 | elementTemplatesCreateProvider: [ 'type', ElementTemplatesCreateProvider ] 6 | }; -------------------------------------------------------------------------------- /lib/element-templates/remove-templates/index.js: -------------------------------------------------------------------------------- 1 | import RemoveTemplateReplaceProvider from './RemoveTemplateReplaceProvider'; 2 | 3 | export default { 4 | __init__: [ 'removeTemplateReplaceProvider' ], 5 | removeTemplateReplaceProvider: [ 'type', RemoveTemplateReplaceProvider ] 6 | }; -------------------------------------------------------------------------------- /lib/element-templates/replace-menu/index.js: -------------------------------------------------------------------------------- 1 | import ElementTemplatesReplaceProvider from './ElementTemplatesReplaceProvider'; 2 | 3 | export default { 4 | __init__: [ 5 | 'elementTemplatesReplaceProvider' 6 | ], 7 | elementTemplatesReplaceProvider: [ 'type', ElementTemplatesReplaceProvider ] 8 | }; -------------------------------------------------------------------------------- /lib/icons/resources/append.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/icons/resources/create.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/create-append-anything/create-menu/index.js: -------------------------------------------------------------------------------- 1 | import CreateMenuProvider from './CreateMenuProvider'; 2 | import CreatePaletteProvider from './CreatePaletteProvider'; 3 | 4 | export default { 5 | __init__: [ 6 | 'createMenuProvider', 7 | 'createPaletteProvider' 8 | ], 9 | createMenuProvider: [ 'type', CreateMenuProvider ], 10 | createPaletteProvider: [ 'type', CreatePaletteProvider ] 11 | }; 12 | -------------------------------------------------------------------------------- /lib/create-append-anything/index.js: -------------------------------------------------------------------------------- 1 | import AppendMenuModule from './append-menu'; 2 | import CreateMenuModule from './create-menu'; 3 | import EditorActionsModule from './editor-actions'; 4 | import KeyboardBindingsModule from './keyboard-bindings'; 5 | 6 | export default { 7 | __depends__: [ 8 | AppendMenuModule, 9 | CreateMenuModule, 10 | EditorActionsModule, 11 | KeyboardBindingsModule 12 | ], 13 | }; -------------------------------------------------------------------------------- /lib/create-append-anything/editor-actions/index.js: -------------------------------------------------------------------------------- 1 | import AppendMenuModule from '../append-menu'; 2 | import CreateMenuModule from '../create-menu'; 3 | 4 | import CreateAppendEditorActions from './EditorActions'; 5 | 6 | export default { 7 | __depends__: [ 8 | AppendMenuModule, 9 | CreateMenuModule 10 | ], 11 | __init__: [ 12 | 'createAppendEditorActions' 13 | ], 14 | createAppendEditorActions: [ 'type', CreateAppendEditorActions ] 15 | }; -------------------------------------------------------------------------------- /lib/element-templates/index.js: -------------------------------------------------------------------------------- 1 | import AppendElementTemplatesModule from './append-menu'; 2 | import CreateElementTemplatesModule from './create-menu'; 3 | import ReplaceElementTemplatesModule from './replace-menu'; 4 | import RemoveTemplatesModule from './remove-templates'; 5 | 6 | export default { 7 | __depends__: [ 8 | AppendElementTemplatesModule, 9 | CreateElementTemplatesModule, 10 | ReplaceElementTemplatesModule, 11 | RemoveTemplatesModule 12 | ] 13 | }; -------------------------------------------------------------------------------- /lib/create-append-anything/keyboard-bindings/index.js: -------------------------------------------------------------------------------- 1 | import AppendMenuModule from '../append-menu'; 2 | import CreateMenuModule from '../create-menu'; 3 | 4 | import CreateAppendKeyboardBindings from './KeyboardBindings'; 5 | 6 | export default { 7 | __depends__: [ 8 | AppendMenuModule, 9 | CreateMenuModule 10 | ], 11 | __init__: [ 12 | 'createAppendKeyboardBindings' 13 | ], 14 | createAppendKeyboardBindings: [ 'type', CreateAppendKeyboardBindings ] 15 | }; -------------------------------------------------------------------------------- /lib/create-append-anything/append-menu/index.js: -------------------------------------------------------------------------------- 1 | import AppendMenuProvider from './AppendMenuProvider'; 2 | import AppendContextPadProvider from './AppendContextPadProvider'; 3 | import AppendRules from './AppendRules'; 4 | 5 | export default { 6 | __init__: [ 7 | 'appendMenuProvider', 8 | 'appendContextPadProvider', 9 | 'appendRules' 10 | ], 11 | appendMenuProvider: [ 'type', AppendMenuProvider ], 12 | appendContextPadProvider: [ 'type', AppendContextPadProvider ], 13 | appendRules: [ 'type', AppendRules ] 14 | }; 15 | -------------------------------------------------------------------------------- /test/spec/create-append-anything/create-menu/CreateMenuProvider.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const commonjs = require('@rollup/plugin-commonjs'); 3 | const json = require('@rollup/plugin-json'); 4 | const nodeResolve = require('@rollup/plugin-node-resolve'); 5 | 6 | const pkg = require('./package.json'); 7 | const nonbundledDependencies = Object.keys({ ...pkg.dependencies }); 8 | 9 | module.exports = { 10 | input: 'lib/index.js', 11 | output: [ { 12 | file: pkg.main, 13 | format: 'cjs' 14 | }, 15 | { 16 | file: pkg.module, 17 | format: 'esm' 18 | } ], 19 | plugins: [ 20 | commonjs(), 21 | json(), 22 | nodeResolve(), 23 | ], 24 | external: nonbundledDependencies 25 | }; 26 | -------------------------------------------------------------------------------- /test/spec/element-templates/create-menu/ElementTemplatesCreateProvider.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/icons/Icons.js: -------------------------------------------------------------------------------- 1 | const appendIcon = ` 2 | 3 | `; 4 | const createIcon = ` 5 | 6 | `; 7 | 8 | export { 9 | appendIcon, 10 | createIcon 11 | }; -------------------------------------------------------------------------------- /test/test.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&display=swap'); 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | height: 100%; 10 | margin: 0; 11 | font-family: 'IBM Plex Sans', sans-serif; 12 | } 13 | 14 | .test-container { 15 | display: flex; 16 | flex-direction: column; 17 | height: 100vh !important; 18 | } 19 | 20 | .test-content-container { 21 | display: flex; 22 | flex: 1; 23 | flex-direction: row; 24 | overflow: hidden; 25 | } 26 | 27 | .modeler-container { 28 | flex: 1; 29 | } 30 | 31 | .properties-container { 32 | flex: none; 33 | width: 300px; 34 | border-left: solid 1px #cccccc; 35 | } 36 | 37 | .properties-container .bio-properties-panel { 38 | --font-family: 'IBM Plex Sans', sans-serif !important; 39 | } -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | Build: 5 | strategy: 6 | matrix: 7 | os: [ ubuntu-latest ] 8 | node-version: [ 20 ] 9 | 10 | runs-on: ${{ matrix.os }} 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v6 15 | - name: Use Node.js 16 | uses: actions/setup-node@v6 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | cache: 'npm' 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Setup project 23 | uses: bpmn-io/actions/setup@latest 24 | - name: Build with coverage 25 | run: COVERAGE=1 npm run all 26 | - name: Upload Coverage 27 | uses: codecov/codecov-action@v5 28 | with: 29 | fail_ci_if_error: true 30 | env: 31 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 32 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import bpmnIoPlugin from 'eslint-plugin-bpmn-io'; 2 | 3 | const files = { 4 | build: [ 5 | '*.js', 6 | '*.mjs' 7 | ], 8 | ignored: [ 9 | 'dist', 10 | 'coverage' 11 | ], 12 | test: [ 13 | 'test/**/*.js' 14 | ] 15 | }; 16 | 17 | 18 | export default [ 19 | { 20 | ignores: files.ignored 21 | }, 22 | 23 | // build 24 | ...bpmnIoPlugin.configs.node.map(config => { 25 | return { 26 | ...config, 27 | files: files.build 28 | }; 29 | }), 30 | 31 | // lib + test 32 | ...bpmnIoPlugin.configs.browser.map(config => { 33 | return { 34 | ...config, 35 | ignores: files.build 36 | }; 37 | }), 38 | 39 | // test 40 | ...bpmnIoPlugin.configs.mocha.map(config => { 41 | return { 42 | ...config, 43 | files: files.test 44 | }; 45 | }), 46 | { 47 | languageOptions: { 48 | globals: { 49 | require: false, 50 | sinon: false 51 | } 52 | }, 53 | files: files.test 54 | } 55 | ]; 56 | -------------------------------------------------------------------------------- /test/spec/element-templates/append-menu/ElementTemplatesAppendProvider.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present Camunda Services GmbH 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/spec/create-append-anything/create-menu/CreatePaletteProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapModeler, 3 | getBpmnJS, 4 | inject 5 | } from 'test/TestHelper'; 6 | 7 | import CreateMenuModule from 'lib/create-append-anything/create-menu'; 8 | 9 | import { createMoveEvent } from 'diagram-js/lib/features/mouse/Mouse'; 10 | 11 | 12 | describe('features/palette', function() { 13 | 14 | const diagramXML = require('./CreateMenuProvider.bpmn').default; 15 | 16 | beforeEach(bootstrapModeler(diagramXML, { 17 | additionalModules: [ CreateMenuModule ] 18 | })); 19 | 20 | describe('create', function() { 21 | 22 | it('should trigger create menu', inject(function(popupMenu, canvas) { 23 | 24 | // given 25 | const createSpy = sinon.spy(popupMenu, 'open'); 26 | 27 | // when 28 | triggerPaletteEntry('create'); 29 | 30 | // then 31 | const args = createSpy.getCall(0).args; 32 | 33 | expect(createSpy).to.have.been.called; 34 | expect(args[0]).to.eq(canvas.getRootElement()); 35 | expect(args[1]).to.eq('bpmn-create'); 36 | })); 37 | 38 | }); 39 | 40 | }); 41 | 42 | 43 | // helpers ////////// 44 | 45 | function triggerPaletteEntry(id) { 46 | getBpmnJS().invoke(function(palette) { 47 | const entry = palette.getEntries()[ id ]; 48 | 49 | if (entry && entry.action && entry.action.click) { 50 | entry.action.click(createMoveEvent(0, 0)); 51 | } 52 | }); 53 | } -------------------------------------------------------------------------------- /lib/create-append-anything/editor-actions/EditorActions.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'min-dash'; 2 | 3 | /** 4 | * Registers and executes BPMN specific editor actions. 5 | * 6 | * @param {Injector} injector 7 | */ 8 | export default function CreateAppendEditorActions(injector) { 9 | this._injector = injector; 10 | 11 | this.registerActions(); 12 | } 13 | 14 | CreateAppendEditorActions.$inject = [ 15 | 'injector' 16 | ]; 17 | 18 | /** 19 | * Register actions. 20 | * 21 | * @param {Injector} injector 22 | */ 23 | CreateAppendEditorActions.prototype.registerActions = function() { 24 | const editorActions = this._injector.get('editorActions', false); 25 | const selection = this._injector.get('selection', false); 26 | const contextPad = this._injector.get('contextPad', false); 27 | const palette = this._injector.get('palette', false); 28 | const popupMenu = this._injector.get('popupMenu', false); 29 | 30 | const actions = {}; 31 | 32 | // append 33 | if (selection && contextPad && palette && popupMenu && palette) { 34 | assign(actions, { 35 | 'appendElement': function(event) { 36 | const selected = selection && selection.get(); 37 | 38 | if (selected.length == 1 && !popupMenu.isEmpty(selected[0], 'bpmn-append')) { 39 | contextPad.triggerEntry('append', 'click', event); 40 | } else { 41 | palette.triggerEntry('create', 'click', event); 42 | } 43 | } 44 | }); 45 | } 46 | 47 | // create 48 | if (palette) { 49 | assign(actions, { 50 | 'createElement': function(event) { 51 | palette.triggerEntry('create', 'click', event); 52 | } } 53 | ); 54 | } 55 | 56 | editorActions && editorActions.register(actions); 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /lib/create-append-anything/append-menu/AppendRules.js: -------------------------------------------------------------------------------- 1 | import { 2 | find, 3 | } from 'min-dash'; 4 | 5 | import inherits from 'inherits-browser'; 6 | 7 | import { 8 | is, 9 | isAny, 10 | getBusinessObject 11 | } from 'bpmn-js/lib/util/ModelUtil'; 12 | 13 | import { 14 | isLabel 15 | } from 'bpmn-js/lib/util/LabelUtil'; 16 | 17 | import RuleProvider from 'diagram-js/lib/features/rules/RuleProvider'; 18 | 19 | 20 | /** 21 | * Append anything modeling rules 22 | */ 23 | export default function AppendRules(eventBus) { 24 | RuleProvider.call(this, eventBus); 25 | } 26 | 27 | inherits(AppendRules, RuleProvider); 28 | 29 | AppendRules.$inject = [ 30 | 'eventBus' 31 | ]; 32 | 33 | AppendRules.prototype.init = function() { 34 | this.addRule('shape.append', function(context) { 35 | 36 | const source = context.element; 37 | 38 | const businessObject = getBusinessObject(source); 39 | 40 | if (isLabel(source)) { 41 | return false; 42 | } 43 | 44 | if (isAny(source, [ 45 | 'bpmn:EndEvent', 46 | 'bpmn:Group', 47 | 'bpmn:TextAnnotation', 48 | 'bpmn:Lane', 49 | 'bpmn:Participant', 50 | 'bpmn:DataStoreReference', 51 | 'bpmn:DataObjectReference' 52 | ])) { 53 | return false; 54 | } 55 | 56 | if (isConnection(source)) { 57 | return false; 58 | } 59 | 60 | if (is(source, 'bpmn:IntermediateThrowEvent') && hasEventDefinition(source, 'bpmn:LinkEventDefinition')) { 61 | return false; 62 | } 63 | 64 | if (is(source, 'bpmn:SubProcess') && businessObject.triggeredByEvent) { 65 | return false; 66 | } 67 | }); 68 | 69 | }; 70 | 71 | 72 | // helpers ////////////// 73 | function hasEventDefinition(element, eventDefinition) { 74 | const bo = getBusinessObject(element); 75 | 76 | return !!find(bo.eventDefinitions || [], function(definition) { 77 | return is(definition, eventDefinition); 78 | }); 79 | } 80 | 81 | function isConnection(element) { 82 | return element.waypoints; 83 | } -------------------------------------------------------------------------------- /test/spec/create-append-anything/append-menu/AppendMenuProvider.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_0zr9ajj 6 | 7 | 8 | Flow_00dqsyf 9 | Flow_0zr9ajj 10 | 11 | 12 | Flow_00dqsyf 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/spec/element-templates/replace-menu/RemoveTemplateReplaceProvider.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bpmn-js-create-append-anything 2 | 3 | [![CI](https://github.com/bpmn-io/bpmn-js-create-append-anything/actions/workflows/CI.yml/badge.svg)](https://github.com/bpmn-io/bpmn-js-create-append-anything/actions/workflows/CI.yml) 4 | 5 | This module extends [bpmn-js](https://github.com/bpmn-io/bpmn-js) with a create and append anything modeling experience. 6 | 7 | ![screenshot](./resources/screenshot.png) 8 | 9 | 10 | ## Features 11 | 12 | * Create any BPMN element from the palette 13 | * Append any BPMN element from the context pad 14 | * Fully keyboard navigatable (`N` and `A` to open the menus) 15 | * Integration with [element templates](https://github.com/bpmn-io/element-templates) through dedicated extension 16 | * Create or append templated elements 17 | * Apply template through the replace menu 18 | 19 | 20 | ## Installation 21 | 22 | Install via npm: 23 | 24 | ```sh 25 | npm install bpmn-js-create-append-anything 26 | ``` 27 | 28 | 29 | ## Usage 30 | 31 | Use as an extension for [bpmn-js](https://github.com/bpmn-io/bpmn-js): 32 | 33 | ```javascript 34 | import { 35 | CreateAppendAnythingModule 36 | } from 'bpmn-js-create-append-anything'; 37 | 38 | const modeler = new BpmnModeler({ 39 | additionalModules: [ 40 | ..., 41 | CreateAppendAnythingModule, 42 | CreateAppendElementTemplatesModule 43 | ] 44 | }); 45 | ``` 46 | 47 | If desired, integrate with [element templates](https://github.com/bpmn-io/element-templates): 48 | 49 | ```javascript 50 | import { 51 | CreateAppendAnythingModule, 52 | CreateAppendElementTemplatesModule 53 | } from 'bpmn-js-create-append-anything'; 54 | 55 | const modeler = new BpmnModeler({ 56 | additionalModules: [ 57 | ..., 58 | CreateAppendAnythingModule, 59 | CreateAppendElementTemplatesModule 60 | ] 61 | }); 62 | ``` 63 | 64 | This relies on `elementTemplates` to be provided via an external module, i.e. [bpmn-js-element-templates](https://github.com/bpmn-io/bpmn-js-element-templates). 65 | 66 | 67 | ## Run locally 68 | 69 | To get the development setup make sure to have [NodeJS](https://nodejs.org/en/download/) installed. 70 | As soon as you are set up, clone the project and execute 71 | 72 | ```sh 73 | # install dependencies 74 | npm install 75 | 76 | # start a bpmn-js instance with the extension 77 | npm start 78 | 79 | # for regular BPMN elements only 80 | npm run start:bpmn 81 | ``` 82 | 83 | 84 | ## License 85 | 86 | MIT 87 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to [bpmn-js-create-append-anything](https://github.com/bpmn-io/bpmn-js-create-append-anything) 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 | ## 1.0.1 10 | 11 | * `FIX`: trigger create mode if auto place of element with template not possible ([#56](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/56)) 12 | 13 | ## 1.0.0 14 | 15 | * `FEAT`: source element template `keywords` during search ([#50](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/50)) 16 | * `DEPS`: depend on `diagram-js>=15.3.0` ([#50](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/50)) 17 | 18 | ### Breaking Changes 19 | 20 | * Require `diagram-js>=15.3.0` as a peer dependency ([#50](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/50)) 21 | 22 | ## 0.6.0 23 | 24 | * `FEAT`: add ad-hoc subprocess entry ([#47](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/47)) 25 | 26 | ## 0.5.2 27 | 28 | * `FIX`: correct expanded subprocess icon ([#33](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/33)) 29 | 30 | ## 0.5.1 31 | 32 | * `FIX`: use rule to decide whether to show context pad entry for appending ([#27](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/27)) 33 | 34 | ## 0.5.0 35 | 36 | * `FEAT`: update labels to be sentence case ([#17](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/17)) 37 | * `CHORE`: update dependencies 38 | 39 | ## 0.4.0 40 | 41 | * `FEAT`: move "Call Activity" to "Sub Processes" group in options menu ([#14](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/14)) 42 | 43 | ## 0.3.0 44 | 45 | * `FEAT`: base BPMN element entry removes element template instead of unlinking it ([#11](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/11)) 46 | 47 | ### Breaking Changes 48 | 49 | * `UnlinkTemplatesModule` has been renamed to `RemoveTemplatesModule`. If importing the module directly, update your import accordingly. 50 | 51 | ## 0.2.1 52 | 53 | * `FIX`: apply icon hover colors ([#10](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/10)) 54 | 55 | ## 0.2.0 56 | 57 | * `FEAT`: load palette and context pad icons as html ([#7](https://github.com/bpmn-io/bpmn-js-create-append-anything/pull/7)) 58 | 59 | ## 0.1.0 60 | 61 | _Initial release_ 62 | -------------------------------------------------------------------------------- /test/fixtures/simple.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Flow_0g16wxm 6 | 7 | 8 | Flow_0g16wxm 9 | Flow_0vce4r4 10 | 11 | 12 | 13 | Flow_0vce4r4 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /lib/create-append-anything/create-menu/CreatePaletteProvider.js: -------------------------------------------------------------------------------- 1 | import { createIcon } from '../../icons/Icons'; 2 | 3 | import { query as domQuery } from 'min-dom'; 4 | 5 | const LOWER_PRIORITY = 900; 6 | 7 | /** 8 | * A palette provider for the create elements menu. 9 | */ 10 | export default function CreatePaletteProvider(palette, translate, popupMenu, canvas, mouse) { 11 | 12 | this._palette = palette; 13 | this._translate = translate; 14 | this._popupMenu = popupMenu; 15 | this._canvas = canvas; 16 | this._mouse = mouse; 17 | 18 | this.register(); 19 | } 20 | 21 | CreatePaletteProvider.$inject = [ 22 | 'palette', 23 | 'translate', 24 | 'popupMenu', 25 | 'canvas', 26 | 'mouse' 27 | ]; 28 | 29 | /** 30 | * Register create button provider in the palette 31 | */ 32 | CreatePaletteProvider.prototype.register = function() { 33 | this._palette.registerProvider(LOWER_PRIORITY, this); 34 | }; 35 | 36 | /** 37 | * Gets the palette create entry 38 | * 39 | * @param {djs.model.Base} element 40 | * @returns {Object} 41 | */ 42 | CreatePaletteProvider.prototype.getPaletteEntries = function(element) { 43 | const translate = this._translate, 44 | popupMenu = this._popupMenu, 45 | canvas = this._canvas, 46 | mouse = this._mouse; 47 | 48 | const getPosition = (event) => { 49 | const X_OFFSET = 35; 50 | const Y_OFFSET = 10; 51 | 52 | if (event instanceof KeyboardEvent) { 53 | event = mouse.getLastMoveEvent(); 54 | return { x: event.x, y: event.y }; 55 | } 56 | 57 | const target = event && event.target || domQuery('.djs-palette [data-action="create"]'); 58 | const targetPosition = target.getBoundingClientRect(); 59 | 60 | return target && { 61 | x: targetPosition.left + targetPosition.width / 2 + X_OFFSET, 62 | y: targetPosition.top + targetPosition.height / 2 + Y_OFFSET 63 | }; 64 | }; 65 | 66 | return { 67 | 'create': { 68 | group: 'create', 69 | html: `
${createIcon}
`, 70 | title: translate('Create element'), 71 | action: { 72 | click: function(event) { 73 | const position = getPosition(event); 74 | 75 | const element = canvas.getRootElement(); 76 | 77 | popupMenu.open(element, 'bpmn-create', position, { 78 | title: translate('Create element'), 79 | width: 300, 80 | search: true 81 | }); 82 | } 83 | } 84 | } 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /lib/create-append-anything/keyboard-bindings/KeyboardBindings.js: -------------------------------------------------------------------------------- 1 | import inherits from 'inherits-browser'; 2 | 3 | import KeyboardBindings from 'diagram-js/lib/features/keyboard/KeyboardBindings'; 4 | 5 | 6 | /** 7 | * BPMN 2.0 specific keyboard bindings. 8 | * 9 | * @param {Injector} injector 10 | */ 11 | export default function CreateAppendKeyboardBindings(injector) { 12 | 13 | this._injector = injector; 14 | this._keyboard = this._injector.get('keyboard', false); 15 | this._editorActions = this._injector.get('editorActions', false); 16 | 17 | if (this._keyboard) { 18 | this._injector.invoke(KeyboardBindings, this); 19 | } 20 | } 21 | 22 | inherits(CreateAppendKeyboardBindings, KeyboardBindings); 23 | 24 | CreateAppendKeyboardBindings.$inject = [ 25 | 'injector' 26 | ]; 27 | 28 | 29 | /** 30 | * Register available keyboard bindings. 31 | * 32 | * @param {Keyboard} keyboard 33 | * @param {EditorActions} editorActions 34 | */ 35 | CreateAppendKeyboardBindings.prototype.registerBindings = function() { 36 | 37 | const keyboard = this._keyboard; 38 | const editorActions = this._editorActions; 39 | 40 | // inherit default bindings 41 | KeyboardBindings.prototype.registerBindings.call(this, keyboard, editorActions); 42 | 43 | /** 44 | * Add keyboard binding if respective editor action 45 | * is registered. 46 | * 47 | * @param {string} action name 48 | * @param {Function} fn that implements the key binding 49 | */ 50 | function addListener(action, fn) { 51 | 52 | if (editorActions && editorActions.isRegistered(action)) { 53 | keyboard && keyboard.addListener(fn); 54 | } 55 | } 56 | 57 | // activate append/create element 58 | // A 59 | addListener('appendElement', function(context) { 60 | 61 | const event = context.keyEvent; 62 | 63 | if (keyboard && keyboard.hasModifier(event)) { 64 | return; 65 | } 66 | 67 | if (keyboard && keyboard.isKey([ 'a', 'A' ], event)) { 68 | 69 | editorActions && editorActions.trigger('appendElement', event); 70 | return true; 71 | } 72 | }); 73 | 74 | // N 75 | addListener('createElement', function(context) { 76 | 77 | const event = context.keyEvent; 78 | 79 | if (keyboard && keyboard.hasModifier(event)) { 80 | return; 81 | } 82 | 83 | if (keyboard && keyboard.isKey([ 'n', 'N' ], event)) { 84 | editorActions && editorActions.trigger('createElement', event); 85 | 86 | return true; 87 | } 88 | }); 89 | 90 | }; -------------------------------------------------------------------------------- /lib/create-append-anything/append-menu/AppendContextPadProvider.js: -------------------------------------------------------------------------------- 1 | import { 2 | assign 3 | } from 'min-dash'; 4 | 5 | import { 6 | appendIcon 7 | } from '../../icons/Icons'; 8 | 9 | 10 | /** 11 | * A provider for append context pad button 12 | */ 13 | export default function AppendContextPadProvider(contextPad, popupMenu, translate, canvas, rules) { 14 | 15 | this._contextPad = contextPad; 16 | this._popupMenu = popupMenu; 17 | this._translate = translate; 18 | this._canvas = canvas; 19 | this._rules = rules; 20 | 21 | this.register(); 22 | } 23 | 24 | AppendContextPadProvider.$inject = [ 25 | 'contextPad', 26 | 'popupMenu', 27 | 'translate', 28 | 'canvas', 29 | 'rules' 30 | ]; 31 | 32 | /** 33 | * Register append button provider in the context pad 34 | */ 35 | AppendContextPadProvider.prototype.register = function() { 36 | this._contextPad.registerProvider(this); 37 | }; 38 | 39 | /** 40 | * Gets the append context pad entry 41 | * 42 | * @param {djs.model.Base} element 43 | * @returns {Object} entries 44 | */ 45 | AppendContextPadProvider.prototype.getContextPadEntries = function(element) { 46 | const popupMenu = this._popupMenu; 47 | const translate = this._translate; 48 | const rules = this._rules; 49 | const getAppendMenuPosition = this._getAppendMenuPosition.bind(this); 50 | 51 | if (rules.allowed('shape.append', { element })) { 52 | 53 | // append menu entry 54 | return { 55 | 'append': { 56 | group: 'model', 57 | html: `
${appendIcon}
`, 58 | title: translate('Append element'), 59 | action: { 60 | click: function(event, element) { 61 | 62 | const position = assign(getAppendMenuPosition(element), { 63 | cursor: { x: event.x, y: event.y } 64 | }); 65 | 66 | popupMenu.open(element, 'bpmn-append', position, { 67 | title: translate('Append element'), 68 | width: 300, 69 | search: true 70 | }); 71 | } 72 | } 73 | } 74 | }; 75 | } 76 | }; 77 | 78 | /** 79 | * Calculates the position for the append menu relatively to the element 80 | * 81 | * @param {djs.model.Base} element 82 | * @returns {Object} 83 | */ 84 | AppendContextPadProvider.prototype._getAppendMenuPosition = function(element) { 85 | const X_OFFSET = 5; 86 | 87 | const pad = this._canvas.getContainer().querySelector('.djs-context-pad'); 88 | 89 | const padRect = pad.getBoundingClientRect(); 90 | 91 | const pos = { 92 | x: padRect.right + X_OFFSET, 93 | y: padRect.top 94 | }; 95 | 96 | return pos; 97 | }; -------------------------------------------------------------------------------- /karma.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | const path = require('path'); 4 | 5 | const basePath = '.'; 6 | 7 | // configures browsers to run test against 8 | // any of [ 'ChromeHeadless', 'Chrome', 'Firefox', 'IE', 'PhantomJS' ] 9 | const browsers = (process.env.TEST_BROWSERS || 'ChromeHeadless').split(','); 10 | 11 | const singleStart = process.env.SINGLE_START; 12 | 13 | const coverage = process.env.COVERAGE; 14 | 15 | const absoluteBasePath = path.resolve(path.join(__dirname, basePath)); 16 | 17 | // use puppeteer provided Chrome for testing 18 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 19 | 20 | const suite = coverage ? 'test/coverageBundle.js' : 'test/testBundle.js'; 21 | 22 | module.exports = function(karma) { 23 | 24 | const config = { 25 | 26 | basePath, 27 | 28 | frameworks: [ 29 | 'webpack', 30 | 'mocha', 31 | 'sinon-chai' 32 | ], 33 | 34 | files: [ 35 | suite 36 | ], 37 | 38 | preprocessors: { 39 | [ suite ]: [ 'webpack', 'env' ] 40 | }, 41 | 42 | reporters: [ 'progress' ].concat(coverage ? 'coverage' : []), 43 | 44 | coverageReporter: { 45 | reporters: [ 46 | { type: 'lcov', subdir: '.' }, 47 | ] 48 | }, 49 | 50 | browsers, 51 | 52 | singleRun: true, 53 | autoWatch: false, 54 | 55 | webpack: { 56 | mode: 'development', 57 | module: { 58 | rules: [ 59 | { 60 | test: /\.(css|bpmn)$/, 61 | use: 'raw-loader' 62 | }, 63 | { 64 | test: /\.m?js$/, 65 | exclude: /node_modules/, 66 | use: { 67 | loader: 'babel-loader', 68 | options: { 69 | plugins: coverage ? [ 70 | [ 'istanbul', { 71 | include: [ 72 | 'lib/**' 73 | ] 74 | } ] 75 | ] : [] 76 | } 77 | } 78 | } 79 | ] 80 | }, 81 | resolve: { 82 | mainFields: [ 83 | 'browser', 84 | 'module', 85 | 'main' 86 | ], 87 | modules: [ 88 | 'node_modules', 89 | absoluteBasePath 90 | ] 91 | }, 92 | devtool: 'eval-source-map' 93 | } 94 | }; 95 | 96 | if (singleStart) { 97 | config.browsers = [].concat(config.browsers, 'Debug'); 98 | config.envPreprocessor = [].concat(config.envPreprocessor || [], 'SINGLE_START'); 99 | } 100 | 101 | karma.set(config); 102 | }; 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bpmn-js-create-append-anything", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "all": "run-s lint test build", 8 | "test": "karma start karma.config.js", 9 | "lint": "eslint .", 10 | "dev": "npm test -- --auto-watch --no-single-run", 11 | "start": "npm run start:templates", 12 | "start:bpmn": "cross-env SINGLE_START=BPMN npm run dev", 13 | "start:templates": "cross-env SINGLE_START=templates npm run dev", 14 | "build": "rollup -c --bundleConfigAsCjs", 15 | "build:watch": "rollup -cw", 16 | "prepare": "run-s build" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/bpmn-io/bpmn-js-create-append-anything.git" 21 | }, 22 | "keywords": [ 23 | "bpmn-io" 24 | ], 25 | "author": "bpmn.io", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/smbea/bpmn-js-create-append-anything/issues" 29 | }, 30 | "homepage": "https://github.com/smbea/bpmn-js-create-append-anything#readme", 31 | "publishConfig": { 32 | "access": "public" 33 | }, 34 | "contributors": [ 35 | { 36 | "name": "bpmn.io contributors", 37 | "url": "https://github.com/bpmn-io" 38 | } 39 | ], 40 | "files": [ 41 | "dist" 42 | ], 43 | "module": "dist/index.es.js", 44 | "peerDependencies": { 45 | "diagram-js": ">= 15.3.0" 46 | }, 47 | "devDependencies": { 48 | "@bpmn-io/element-template-chooser": "^2.0.0", 49 | "@rollup/plugin-commonjs": "^29.0.0", 50 | "@rollup/plugin-json": "^6.1.0", 51 | "@rollup/plugin-node-resolve": "^16.0.0", 52 | "@testing-library/preact": "^3.2.3", 53 | "babel-loader": "^10.0.0", 54 | "babel-plugin-istanbul": "^7.0.0", 55 | "bpmn-js": "^18.6.0", 56 | "bpmn-js-element-templates": "^2.5.1", 57 | "bpmn-js-properties-panel": "^5.31.1", 58 | "cross-env": "^10.0.0", 59 | "downloadjs": "^1.4.7", 60 | "eslint": "^9.14.0", 61 | "eslint-plugin-bpmn-io": "^2.0.2", 62 | "file-drops": "^0.5.0", 63 | "karma": "^6.4.4", 64 | "karma-chrome-launcher": "^3.2.0", 65 | "karma-coverage": "^2.2.1", 66 | "karma-debug-launcher": "^0.0.5", 67 | "karma-env-preprocessor": "^0.1.1", 68 | "karma-mocha": "^2.0.1", 69 | "karma-sinon-chai": "^2.0.2", 70 | "karma-webpack": "^5.0.1", 71 | "mocha": "^10.8.2", 72 | "mocha-test-container-support": "^0.2.0", 73 | "npm-run-all2": "^8.0.0", 74 | "puppeteer": "^24.0.0", 75 | "raw-loader": "^4.0.2", 76 | "rollup": "^4.24.4", 77 | "sinon": "^18.0.1", 78 | "sinon-chai": "^3.7.0", 79 | "webpack": "^5.96.1", 80 | "zeebe-bpmn-moddle": "^1.9.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/spec/create-append-anything/CreateAppendAnything.spec.js: -------------------------------------------------------------------------------- 1 | import TestContainer from 'mocha-test-container-support'; 2 | 3 | import { 4 | clearBpmnJS, 5 | setBpmnJS, 6 | insertCoreStyles, 7 | insertBpmnStyles, 8 | enableLogging 9 | } from 'test/TestHelper'; 10 | 11 | import Modeler from 'bpmn-js/lib/Modeler'; 12 | 13 | import { 14 | BpmnPropertiesPanelModule, 15 | BpmnPropertiesProviderModule 16 | } from 'bpmn-js-properties-panel'; 17 | 18 | import { CreateAppendAnythingModule } from 'lib/'; 19 | 20 | const singleStart = window.__env__ && window.__env__.SINGLE_START === 'BPMN'; 21 | 22 | insertCoreStyles(); 23 | insertBpmnStyles(); 24 | 25 | 26 | describe('', function() { 27 | 28 | let modelerContainer; 29 | 30 | let propertiesContainer; 31 | 32 | let container; 33 | 34 | beforeEach(function() { 35 | modelerContainer = document.createElement('div'); 36 | modelerContainer.classList.add('modeler-container'); 37 | 38 | propertiesContainer = document.createElement('div'); 39 | propertiesContainer.classList.add('properties-container'); 40 | 41 | container = TestContainer.get(this); 42 | 43 | container.appendChild(modelerContainer); 44 | container.appendChild(propertiesContainer); 45 | }); 46 | 47 | async function createModeler(xml, options = {}, BpmnJS = Modeler) { 48 | const { 49 | shouldImport = true, 50 | additionalModules = [ 51 | BpmnPropertiesPanelModule, 52 | BpmnPropertiesProviderModule, 53 | CreateAppendAnythingModule 54 | ], 55 | description = {}, 56 | layout = {} 57 | } = options; 58 | 59 | clearBpmnJS(); 60 | 61 | const modeler = new BpmnJS({ 62 | container: modelerContainer, 63 | additionalModules, 64 | propertiesPanel: { 65 | parent: propertiesContainer, 66 | feelTooltipContainer: container, 67 | description, 68 | layout 69 | }, 70 | ...options 71 | }); 72 | 73 | enableLogging && enableLogging(modeler, !!singleStart); 74 | 75 | setBpmnJS(modeler); 76 | 77 | if (!shouldImport) { 78 | return { modeler }; 79 | } 80 | 81 | try { 82 | const result = await modeler.importXML(xml); 83 | 84 | return { error: null, warnings: result.warnings, modeler: modeler }; 85 | } catch (err) { 86 | return { error: err, warnings: err.warnings, modeler: modeler }; 87 | } 88 | } 89 | 90 | 91 | (singleStart ? it.only : it)('should import simple process', async function() { 92 | 93 | // given 94 | const diagramXml = require('test/fixtures/simple.bpmn').default; 95 | 96 | // when 97 | const result = await createModeler(diagramXml); 98 | 99 | // then 100 | expect(result.error).not.to.exist; 101 | }); 102 | 103 | }); 104 | -------------------------------------------------------------------------------- /test/spec/element-templates/replace-menu/ElementTemplatesReplaceProvider.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /lib/create-append-anything/create-menu/CreateMenuProvider.js: -------------------------------------------------------------------------------- 1 | import { CREATE_OPTIONS } from '../../util/CreateOptionsUtil'; 2 | 3 | /** 4 | * This module is a create menu provider for the popup menu. 5 | */ 6 | export default function CreateMenuProvider( 7 | elementFactory, popupMenu, create, 8 | autoPlace, mouse, translate 9 | ) { 10 | this._elementFactory = elementFactory; 11 | this._popupMenu = popupMenu; 12 | this._create = create; 13 | this._autoPlace = autoPlace; 14 | this._mouse = mouse; 15 | this._translate = translate; 16 | 17 | this.register(); 18 | } 19 | 20 | CreateMenuProvider.$inject = [ 21 | 'elementFactory', 22 | 'popupMenu', 23 | 'create', 24 | 'autoPlace', 25 | 'mouse', 26 | 'translate' 27 | ]; 28 | 29 | /** 30 | * Register create menu provider in the popup menu 31 | */ 32 | CreateMenuProvider.prototype.register = function() { 33 | this._popupMenu.registerProvider('bpmn-create', this); 34 | }; 35 | 36 | /** 37 | * Returns the create options as menu entries 38 | * 39 | * @param {djs.model.Base} element 40 | * 41 | * @return {Array} a list of menu entry items 42 | */ 43 | CreateMenuProvider.prototype.getPopupMenuEntries = function() { 44 | 45 | const entries = {}; 46 | 47 | // map options to menu entries 48 | CREATE_OPTIONS.forEach(option => { 49 | const { 50 | actionName, 51 | className, 52 | label, 53 | target, 54 | description, 55 | group, 56 | search, 57 | rank 58 | } = option; 59 | 60 | const targetAction = this._createEntryAction(target); 61 | 62 | entries[`create-${actionName}`] = { 63 | label: label && this._translate(label), 64 | className, 65 | description, 66 | group: group && { 67 | ...group, 68 | name: this._translate(group.name) 69 | }, 70 | search, 71 | rank, 72 | action: { 73 | click: targetAction, 74 | dragstart: targetAction 75 | } 76 | }; 77 | }); 78 | 79 | return entries; 80 | }; 81 | 82 | /** 83 | * Create an action for a given target 84 | * 85 | * @param {Object} target 86 | * @returns {Object} 87 | */ 88 | CreateMenuProvider.prototype._createEntryAction = function(target) { 89 | 90 | const create = this._create; 91 | const mouse = this._mouse; 92 | const popupMenu = this._popupMenu; 93 | const elementFactory = this._elementFactory; 94 | 95 | let newElement; 96 | 97 | return (event) => { 98 | popupMenu.close(); 99 | 100 | // create the new element 101 | if (target.type === 'bpmn:Participant') { 102 | newElement = elementFactory.createParticipantShape(target); 103 | } else { 104 | newElement = elementFactory.create('shape', target); 105 | } 106 | 107 | // use last mouse event if triggered via keyboard 108 | if (event instanceof KeyboardEvent) { 109 | event = mouse.getLastMoveEvent(); 110 | } 111 | 112 | return create.start(event, newElement); 113 | }; 114 | }; -------------------------------------------------------------------------------- /lib/element-templates/create-menu/ElementTemplatesCreateProvider.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'min-dash'; 2 | 3 | /** 4 | * A popup menu provider that allows to create elements with 5 | * element templates. 6 | */ 7 | export default function ElementTemplatesCreateProvider( 8 | popupMenu, translate, 9 | elementTemplates, mouse, create) { 10 | 11 | this._popupMenu = popupMenu; 12 | this._translate = translate; 13 | this._elementTemplates = elementTemplates; 14 | this._mouse = mouse; 15 | this._create = create; 16 | 17 | this.register(); 18 | } 19 | 20 | ElementTemplatesCreateProvider.$inject = [ 21 | 'popupMenu', 22 | 'translate', 23 | 'elementTemplates', 24 | 'mouse', 25 | 'create' 26 | ]; 27 | 28 | /** 29 | * Register create menu provider in the popup menu 30 | */ 31 | ElementTemplatesCreateProvider.prototype.register = function() { 32 | this._popupMenu.registerProvider('bpmn-create', this); 33 | }; 34 | 35 | /** 36 | * Adds the element templates to the create menu. 37 | * @param {djs.model.Base} element 38 | * 39 | * @returns {Object} 40 | */ 41 | ElementTemplatesCreateProvider.prototype.getPopupMenuEntries = function(element) { 42 | return (entries) => { 43 | 44 | // add template entries 45 | assign(entries, this.getTemplateEntries(element)); 46 | 47 | return entries; 48 | }; 49 | }; 50 | 51 | /** 52 | * Get all element templates. 53 | * 54 | * @param {djs.model.Base} element 55 | * 56 | * @return {Array} a list of element templates as menu entries 57 | */ 58 | ElementTemplatesCreateProvider.prototype.getTemplateEntries = function() { 59 | 60 | const templates = this._elementTemplates.getLatest(); 61 | const templateEntries = {}; 62 | 63 | templates.map(template => { 64 | 65 | const { 66 | icon = {}, 67 | category, 68 | keywords = [], 69 | } = template; 70 | 71 | const entryId = `create.template-${template.id}`; 72 | 73 | const defaultGroup = { 74 | id: 'templates', 75 | name: this._translate('Templates') 76 | }; 77 | 78 | templateEntries[entryId] = { 79 | label: template.name, 80 | description: template.description, 81 | documentationRef: template.documentationRef, 82 | imageUrl: icon.contents, 83 | group: category || defaultGroup, 84 | search: keywords, 85 | action: { 86 | click: this._getEntryAction(template), 87 | dragstart: this._getEntryAction(template) 88 | } 89 | }; 90 | }); 91 | 92 | return templateEntries; 93 | }; 94 | 95 | 96 | ElementTemplatesCreateProvider.prototype._getEntryAction = function(template) { 97 | const create = this._create; 98 | const popupMenu = this._popupMenu; 99 | const elementTemplates = this._elementTemplates; 100 | const mouse = this._mouse; 101 | 102 | return (event) => { 103 | 104 | popupMenu.close(); 105 | 106 | // create the new element 107 | let newElement = elementTemplates.createElement(template); 108 | 109 | // use last mouse event if triggered via keyboard 110 | if (event instanceof KeyboardEvent) { 111 | event = mouse.getLastMoveEvent(); 112 | } 113 | 114 | return create.start(event, newElement); 115 | }; 116 | }; 117 | -------------------------------------------------------------------------------- /test/spec/create-append-anything/append-menu/AppendRules.bpmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | SequenceFlow 11 | 12 | 13 | SequenceFlow 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/element-templates/replace-menu/ElementTemplatesReplaceProvider.js: -------------------------------------------------------------------------------- 1 | import { 2 | getBusinessObject, 3 | isAny 4 | } from 'bpmn-js/lib/util/ModelUtil'; 5 | 6 | 7 | /** 8 | * A replace menu provider that allows to replace elements with 9 | * element templates. 10 | */ 11 | export default function ElementTemplatesReplaceProvider(popupMenu, translate, elementTemplates) { 12 | 13 | this._popupMenu = popupMenu; 14 | this._translate = translate; 15 | this._elementTemplates = elementTemplates; 16 | 17 | this.register(); 18 | } 19 | 20 | ElementTemplatesReplaceProvider.$inject = [ 21 | 'popupMenu', 22 | 'translate', 23 | 'elementTemplates' 24 | ]; 25 | 26 | /** 27 | * Register replace menu provider in the popup menu 28 | */ 29 | ElementTemplatesReplaceProvider.prototype.register = function() { 30 | this._popupMenu.registerProvider('bpmn-replace', this); 31 | }; 32 | 33 | /** 34 | * Adds the element templates to the replace menu. 35 | * @param {djs.model.Base} element 36 | * 37 | * @returns {Object} 38 | */ 39 | ElementTemplatesReplaceProvider.prototype.getPopupMenuEntries = function(element) { 40 | 41 | return (entries) => { 42 | 43 | // convert our entries into something sortable 44 | let entrySet = Object.entries(entries); 45 | 46 | // add template entries 47 | entrySet = [ ...entrySet, ...this.getTemplateEntries(element) ]; 48 | 49 | // convert back to object 50 | return entrySet.reduce((entries, [ key, value ]) => { 51 | entries[key] = value; 52 | 53 | return entries; 54 | }, {}); 55 | }; 56 | }; 57 | 58 | /** 59 | * Get all element templates that can be used to replace the given element. 60 | * 61 | * @param {djs.model.Base} element 62 | * 63 | * @return {Array} a list of element templates as menu entries 64 | */ 65 | ElementTemplatesReplaceProvider.prototype.getTemplateEntries = function(element) { 66 | 67 | const templates = this._getMatchingTemplates(element); 68 | return templates.map(template => { 69 | 70 | const { 71 | icon = {}, 72 | category, 73 | keywords = [], 74 | } = template; 75 | 76 | const entryId = `replace.template-${template.id}`; 77 | 78 | const defaultGroup = { 79 | id: 'templates', 80 | name: this._translate('Templates') 81 | }; 82 | 83 | return [ entryId, { 84 | label: template.name, 85 | description: template.description, 86 | documentationRef: template.documentationRef, 87 | imageUrl: icon.contents, 88 | search: keywords, 89 | group: category || defaultGroup, 90 | action: () => { 91 | this._elementTemplates.applyTemplate(element, template); 92 | } 93 | } ]; 94 | }); 95 | }; 96 | 97 | /** 98 | * Returns the templates that can the element can be replaced with. 99 | * 100 | * @param {djs.model.Base} element 101 | * 102 | * @return {Array} 103 | */ 104 | ElementTemplatesReplaceProvider.prototype._getMatchingTemplates = function(element) { 105 | return this._elementTemplates.getLatest().filter(template => { 106 | return isAny(element, template.appliesTo) && !isTemplateApplied(element, template); 107 | }); 108 | }; 109 | 110 | 111 | // helpers //////////// 112 | export function isTemplateApplied(element, template) { 113 | const businessObject = getBusinessObject(element); 114 | 115 | if (businessObject) { 116 | return businessObject.get('zeebe:modelerTemplate') === template.id; 117 | } 118 | 119 | return false; 120 | } 121 | -------------------------------------------------------------------------------- /test/spec/element-templates/CreateAppendElementTemplates.spec.js: -------------------------------------------------------------------------------- 1 | import TestContainer from 'mocha-test-container-support'; 2 | 3 | import { 4 | clearBpmnJS, 5 | setBpmnJS, 6 | insertCoreStyles, 7 | insertBpmnStyles, 8 | enableLogging 9 | } from 'test/TestHelper'; 10 | 11 | import Modeler from 'bpmn-js/lib/Modeler'; 12 | 13 | import { 14 | BpmnPropertiesPanelModule, 15 | BpmnPropertiesProviderModule, 16 | ZeebePropertiesProviderModule, 17 | } from 'bpmn-js-properties-panel'; 18 | 19 | import { 20 | CloudElementTemplatesPropertiesProviderModule as ElementTemplatesProviderModule 21 | } from 'bpmn-js-element-templates'; 22 | 23 | import { CreateAppendElementTemplatesModule, CreateAppendAnythingModule } from 'lib/'; 24 | 25 | import ZeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; 26 | 27 | import ElementTemplateChooserModule from '@bpmn-io/element-template-chooser'; 28 | 29 | import templates from 'test/fixtures/element-templates.json'; 30 | 31 | 32 | const singleStart = window.__env__ && window.__env__.SINGLE_START === 'templates'; 33 | 34 | insertCoreStyles(); 35 | insertBpmnStyles(); 36 | 37 | 38 | describe('', function() { 39 | 40 | let modelerContainer; 41 | 42 | let propertiesContainer; 43 | 44 | let container; 45 | 46 | beforeEach(function() { 47 | modelerContainer = document.createElement('div'); 48 | modelerContainer.classList.add('modeler-container'); 49 | 50 | propertiesContainer = document.createElement('div'); 51 | propertiesContainer.classList.add('properties-container'); 52 | 53 | container = TestContainer.get(this); 54 | 55 | container.appendChild(modelerContainer); 56 | container.appendChild(propertiesContainer); 57 | }); 58 | 59 | async function createModeler(xml, options = {}, BpmnJS = Modeler) { 60 | const { 61 | shouldImport = true, 62 | additionalModules = [ 63 | BpmnPropertiesPanelModule, 64 | BpmnPropertiesProviderModule, 65 | ZeebePropertiesProviderModule, 66 | ElementTemplatesProviderModule, 67 | ElementTemplateChooserModule, 68 | CreateAppendAnythingModule, 69 | CreateAppendElementTemplatesModule 70 | ], 71 | moddleExtensions = { 72 | zeebe: ZeebeModdle 73 | }, 74 | description = {}, 75 | layout = {} 76 | } = options; 77 | 78 | clearBpmnJS(); 79 | 80 | const modeler = new BpmnJS({ 81 | container: modelerContainer, 82 | additionalModules, 83 | propertiesPanel: { 84 | parent: propertiesContainer, 85 | feelTooltipContainer: container, 86 | description, 87 | layout 88 | }, 89 | moddleExtensions, 90 | ...options 91 | }); 92 | 93 | enableLogging && enableLogging(modeler, !!singleStart); 94 | 95 | setBpmnJS(modeler); 96 | 97 | if (!shouldImport) { 98 | return { modeler }; 99 | } 100 | 101 | try { 102 | const result = await modeler.importXML(xml); 103 | 104 | return { error: null, warnings: result.warnings, modeler: modeler }; 105 | } catch (err) { 106 | return { error: err, warnings: err.warnings, modeler: modeler }; 107 | } 108 | } 109 | 110 | 111 | (singleStart ? it.only : it)('should import simple process', async function() { 112 | 113 | // given 114 | const diagramXml = require('test/fixtures/simple.bpmn').default; 115 | 116 | // when 117 | const { error, modeler } = await createModeler(diagramXml); 118 | 119 | 120 | modeler.get('elementTemplates').set(templates); 121 | 122 | // then 123 | expect(error).not.to.exist; 124 | }); 125 | 126 | }); 127 | -------------------------------------------------------------------------------- /test/spec/create-append-anything/append-menu/AppendContextPadProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapModeler, 3 | getBpmnJS, 4 | inject 5 | } from 'test/TestHelper'; 6 | 7 | import { 8 | query as domQuery 9 | } from 'min-dom'; 10 | 11 | import { 12 | is 13 | } from 'bpmn-js/lib/util/ModelUtil'; 14 | 15 | import AppendMenuModule from 'lib/create-append-anything/append-menu'; 16 | import CustomRulesModule from 'bpmn-js/test/util/custom-rules'; 17 | 18 | 19 | describe('features/create-append-anything - append menu provider', function() { 20 | 21 | describe('append', function() { 22 | 23 | const diagramXML = require('../../../fixtures/simple.bpmn').default; 24 | 25 | beforeEach(bootstrapModeler(diagramXML, { 26 | additionalModules: [ 27 | AppendMenuModule, 28 | CustomRulesModule 29 | ] 30 | })); 31 | 32 | 33 | it('should show append menu in the correct position', inject(function(elementRegistry, contextPad) { 34 | 35 | // given 36 | const element = elementRegistry.get('StartEvent_1'), 37 | padding = { y: 1, x: 5 }; 38 | 39 | contextPad.open(element); 40 | 41 | // when 42 | contextPad.trigger('click', padEvent('append')); 43 | 44 | const padMenuRect = getPad().getBoundingClientRect(); 45 | const replaceMenuRect = getPopupMenu().getBoundingClientRect(); 46 | 47 | // then 48 | expect(replaceMenuRect.left).to.be.at.most(padMenuRect.right + padding.x); 49 | expect(replaceMenuRect.top).to.be.at.most(padMenuRect.top + padding.y); 50 | })); 51 | 52 | 53 | it('should hide icon if append is disallowed', inject( 54 | function(elementRegistry, contextPad, customRules) { 55 | 56 | // given 57 | const element = elementRegistry.get('StartEvent_1'); 58 | 59 | // disallow append 60 | customRules.addRule('shape.append', function(context) { 61 | return !is(context.element, 'bpmn:StartEvent'); 62 | }); 63 | 64 | // when 65 | contextPad.open(element); 66 | 67 | const padNode = getPad(); 68 | 69 | // then 70 | expect(padEntry(padNode, 'append')).not.to.exist; 71 | } 72 | )); 73 | 74 | 75 | it('should show icon if append is allowed', inject( 76 | function(elementRegistry, contextPad, customRules) { 77 | 78 | // given 79 | const element = elementRegistry.get('Task_1'); 80 | 81 | // disallow append 82 | customRules.addRule('shape.append', function(context) { 83 | return !is(context.element, 'bpmn:StartEvent'); 84 | }); 85 | 86 | // when 87 | contextPad.open(element); 88 | 89 | const padNode = getPad(); 90 | 91 | // then 92 | expect(padEntry(padNode, 'append')).to.exist; 93 | } 94 | )); 95 | 96 | }); 97 | }); 98 | 99 | 100 | // helper ////////////////////////////////////////////////////////////////////// 101 | function padEntry(element, name) { 102 | return domQuery('[data-action="' + name + '"]', element); 103 | } 104 | 105 | function padEvent(entry) { 106 | const target = padEntry(getPad(), entry); 107 | 108 | return { 109 | target: target, 110 | preventDefault: function() {}, 111 | clientX: 100, 112 | clientY: 100 113 | }; 114 | } 115 | 116 | function getPad() { 117 | return getBpmnJS().invoke(function(canvas) { 118 | return canvas.getContainer().querySelector('.djs-context-pad'); 119 | }); 120 | } 121 | 122 | function getPopupMenu() { 123 | const popup = getBpmnJS().get('popupMenu'); 124 | 125 | return popup._current && domQuery('.djs-popup', popup._current.container); 126 | } -------------------------------------------------------------------------------- /test/spec/create-append-anything/editor-actions/CreateAppendEditorActions.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapModeler, 3 | inject, 4 | getBpmnJS 5 | } from 'test/TestHelper'; 6 | 7 | import { 8 | query as domQuery 9 | } from 'min-dom'; 10 | 11 | import { CreateAppendAnythingModule } from 'lib/'; 12 | 13 | 14 | describe('features/create-append-anything - editor actions', function() { 15 | 16 | const diagramXML = require('test/fixtures/simple.bpmn').default; 17 | 18 | beforeEach( 19 | bootstrapModeler(diagramXML, { 20 | additionalModules: [ 21 | CreateAppendAnythingModule 22 | ] 23 | }) 24 | ); 25 | 26 | describe('#appendElement', function() { 27 | 28 | it('should open append element', inject(function(elementRegistry, selection, editorActions, eventBus) { 29 | 30 | // given 31 | const element = elementRegistry.get('StartEvent_1'); 32 | 33 | selection.select(element); 34 | const changedSpy = sinon.spy(); 35 | 36 | // when 37 | eventBus.once('popupMenu.open', changedSpy); 38 | 39 | editorActions.trigger('appendElement', {}); 40 | 41 | // then 42 | expect(changedSpy).to.have.been.called; 43 | expect(isMenu('bpmn-append')).to.be.true; 44 | })); 45 | 46 | 47 | it('should open create element if multiple elements selected', inject(function(elementRegistry, selection, editorActions, eventBus) { 48 | 49 | // given 50 | const elementIds = [ 'StartEvent_1', 'Task_1' ]; 51 | const elements = elementIds.map(function(id) { 52 | return elementRegistry.get(id); 53 | }); 54 | 55 | selection.select(elements); 56 | const changedSpy = sinon.spy(); 57 | 58 | // when 59 | eventBus.once('popupMenu.open', changedSpy); 60 | 61 | editorActions.trigger('appendElement', {}); 62 | 63 | // then 64 | expect(changedSpy).to.have.been.called; 65 | expect(isMenu('bpmn-create')).to.be.true; 66 | })); 67 | 68 | 69 | it('should open create element if no selection', inject(function(editorActions, eventBus) { 70 | 71 | // given 72 | const changedSpy = sinon.spy(); 73 | 74 | // when 75 | eventBus.once('popupMenu.open', changedSpy); 76 | 77 | editorActions.trigger('appendElement', {}); 78 | 79 | // then 80 | expect(changedSpy).to.have.been.called; 81 | expect(isMenu('bpmn-create')).to.be.true; 82 | })); 83 | 84 | 85 | it('should open create element if append not allowed', inject(function(elementRegistry, selection, editorActions, eventBus) { 86 | 87 | // given 88 | const element = elementRegistry.get('EndEvent_1'); 89 | 90 | selection.select(element); 91 | const changedSpy = sinon.spy(); 92 | 93 | // when 94 | eventBus.once('popupMenu.open', changedSpy); 95 | 96 | editorActions.trigger('appendElement', {}); 97 | 98 | // then 99 | expect(changedSpy).to.have.been.called; 100 | expect(isMenu('bpmn-create')).to.be.true; 101 | })); 102 | 103 | }); 104 | 105 | 106 | describe('#createElement', function() { 107 | 108 | it('should open create element', inject(function(editorActions, eventBus) { 109 | 110 | // given 111 | const changedSpy = sinon.spy(); 112 | eventBus.once('popupMenu.open', changedSpy); 113 | 114 | // when 115 | editorActions.trigger('createElement', {}); 116 | 117 | // then 118 | expect(changedSpy).to.have.been.called; 119 | })); 120 | 121 | }); 122 | 123 | }); 124 | 125 | 126 | // helpers ////////////////////// 127 | function isMenu(menuId) { 128 | const popup = getBpmnJS().get('popupMenu'); 129 | const popupElement = popup._current && domQuery('.djs-popup', popup._current.container); 130 | 131 | return popupElement.classList.contains(menuId); 132 | } -------------------------------------------------------------------------------- /test/spec/create-append-anything/append-menu/AppendRules.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapModeler, 3 | inject 4 | } from 'test/TestHelper'; 5 | 6 | import AppendMenuModule from 'lib/create-append-anything/append-menu'; 7 | 8 | 9 | describe('features/create-append-anything - rules', function() { 10 | 11 | const diagramXML = require('./AppendRules.bpmn').default; 12 | 13 | beforeEach(bootstrapModeler(diagramXML, { 14 | additionalModules: [ 15 | AppendMenuModule 16 | ] 17 | })); 18 | 19 | 20 | describe('element append', function() { 21 | 22 | it('should not allow for given element types', inject(function(elementFactory, rules) { 23 | 24 | // given 25 | const types = [ 26 | 'bpmn:EndEvent', 27 | 'bpmn:Group', 28 | 'bpmn:TextAnnotation', 29 | 'bpmn:Lane', 30 | 'bpmn:Participant', 31 | 'bpmn:DataStoreReference', 32 | 'bpmn:DataObjectReference' 33 | ]; 34 | 35 | // when 36 | const results = types.map(function(type) { 37 | const element = elementFactory.createShape({ type: type }); 38 | return rules.allowed('shape.append', { element }); 39 | }); 40 | 41 | // then 42 | results.forEach(function(result) { 43 | expect(result).to.be.false; 44 | }); 45 | })); 46 | 47 | 48 | it('should not allow for labels', inject(function(elementRegistry, rules) { 49 | 50 | // given 51 | const element = elementRegistry.get('START_EVENT').label; 52 | 53 | // when 54 | const allowed = rules.allowed('shape.append', { element }); 55 | 56 | // then 57 | expect(allowed).to.be.false; 58 | })); 59 | 60 | 61 | it('should not allow for event subprocess', inject(function(elementFactory, rules) { 62 | 63 | // given 64 | const element = elementFactory.createShape({ type: 'bpmn:SubProcess', triggeredByEvent: true }); 65 | 66 | // when 67 | const result = rules.allowed('shape.append', { element }); 68 | 69 | // then 70 | expect(result).to.be.false; 71 | })); 72 | 73 | 74 | it('should not allow for link intermediate throw event', inject(function(elementFactory, rules) { 75 | 76 | // given 77 | const element = elementFactory.createShape({ 78 | type: 'bpmn:IntermediateThrowEvent', 79 | cancelActivity: false, 80 | eventDefinitionType: 'bpmn:LinkEventDefinition' 81 | }); 82 | 83 | // when 84 | const result = rules.allowed('shape.append', { element }); 85 | 86 | // then 87 | expect(result).to.be.false; 88 | })); 89 | 90 | 91 | describe('connections', function() { 92 | 93 | it('should not allow for sequence flows', inject(function(elementRegistry, rules) { 94 | 95 | // given 96 | const element = elementRegistry.get('SequenceFlow'); 97 | 98 | // when 99 | const allowed = rules.allowed('shape.append', { element }); 100 | 101 | // then 102 | expect(allowed).to.be.false; 103 | })); 104 | 105 | 106 | it('should not allow for associations', inject(function(elementRegistry, rules) { 107 | 108 | // given 109 | const element = elementRegistry.get('Association'); 110 | 111 | // when 112 | const allowed = rules.allowed('shape.append', { element }); 113 | 114 | // then 115 | expect(allowed).to.be.false; 116 | })); 117 | 118 | 119 | it('should not allow for message flows', inject(function(elementRegistry, rules) { 120 | 121 | // given 122 | const element = elementRegistry.get('MessageFlow'); 123 | 124 | // when 125 | const allowed = rules.allowed('shape.append', { element }); 126 | 127 | // then 128 | expect(allowed).to.be.false; 129 | })); 130 | 131 | }); 132 | }); 133 | 134 | }); -------------------------------------------------------------------------------- /test/spec/create-append-anything/keyboard-bindings/CreateAppendKeyboardBindings.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | getBpmnJS, 4 | bootstrapModeler 5 | } from 'test/TestHelper'; 6 | 7 | import { forEach } from 'min-dash'; 8 | 9 | import { 10 | createKeyEvent 11 | } from 'bpmn-js/test/util/KeyEvents'; 12 | 13 | import { 14 | query as domQuery 15 | } from 'min-dom'; 16 | 17 | import { CreateAppendAnythingModule } from 'lib/'; 18 | 19 | describe('features/create-append-anything - keyboard bindings', function() { 20 | 21 | const diagramXML = require('test/fixtures/simple.bpmn').default; 22 | 23 | beforeEach( 24 | bootstrapModeler(diagramXML, { 25 | additionalModules: [ 26 | CreateAppendAnythingModule 27 | ] 28 | }) 29 | ); 30 | 31 | 32 | describe('create append keyboard bindings', function() { 33 | 34 | it('should include triggers inside editorActions', inject(function(editorActions) { 35 | 36 | // given 37 | const expectedActions = [ 38 | 'appendElement', 39 | 'createElement' 40 | ]; 41 | const actualActions = editorActions.getActions(); 42 | 43 | // then 44 | expect( 45 | expectedActions.every(action => actualActions.includes(action)) 46 | ).to.be.true; 47 | })); 48 | 49 | 50 | forEach([ 'a', 'A' ], function(key) { 51 | 52 | it(`should trigger append menu for ${key}`, 53 | inject(function(keyboard, popupMenu, elementRegistry, selection) { 54 | 55 | sinon.spy(popupMenu, 'open'); 56 | 57 | // given 58 | const task = elementRegistry.get('Task_1'); 59 | 60 | selection.select(task); 61 | 62 | const e = createKeyEvent(key); 63 | 64 | // when 65 | keyboard._keyHandler(e); 66 | 67 | // then 68 | expect(popupMenu.open).to.have.been.calledOnce; 69 | expect(isMenu('bpmn-append')).to.be.true; 70 | })); 71 | 72 | 73 | it('should trigger create menu', 74 | inject(function(keyboard, popupMenu) { 75 | 76 | sinon.spy(popupMenu, 'open'); 77 | 78 | // given 79 | const e = createKeyEvent(key); 80 | 81 | // when 82 | keyboard._keyHandler(e); 83 | 84 | // then 85 | expect(popupMenu.open).to.have.been.calledOnce; 86 | expect(isMenu('bpmn-create')).to.be.true; 87 | })); 88 | 89 | 90 | it('should not trigger create or append menus', 91 | inject(function(keyboard, popupMenu) { 92 | 93 | sinon.spy(popupMenu, 'open'); 94 | 95 | // given 96 | const e = createKeyEvent(key, { ctrlKey: true }); 97 | 98 | // when 99 | keyboard._keyHandler(e); 100 | 101 | // then 102 | expect(popupMenu.open).to.not.have.been.called; 103 | })); 104 | 105 | }); 106 | 107 | 108 | forEach([ 'n', 'N' ], function(key) { 109 | 110 | it(`should trigger create menu for <${key}>`, 111 | inject(function(keyboard, popupMenu) { 112 | 113 | sinon.spy(popupMenu, 'open'); 114 | 115 | // given 116 | const e = createKeyEvent(key); 117 | 118 | // when 119 | keyboard._keyHandler(e); 120 | 121 | // then 122 | expect(popupMenu.open).to.have.been.calledOnce; 123 | expect(isMenu('bpmn-create')).to.be.true; 124 | })); 125 | 126 | 127 | it('should not trigger create menu', 128 | inject(function(keyboard, popupMenu) { 129 | 130 | sinon.spy(popupMenu, 'open'); 131 | 132 | // given 133 | const e = createKeyEvent(key, { ctrlKey: true }); 134 | 135 | // when 136 | keyboard._keyHandler(e); 137 | 138 | // then 139 | expect(popupMenu.open).to.not.have.been.called; 140 | })); 141 | 142 | }); 143 | 144 | }); 145 | 146 | }); 147 | 148 | 149 | // helpers ////////////////////// 150 | function isMenu(menuId) { 151 | const popup = getBpmnJS().get('popupMenu'); 152 | const popupElement = popup._current && domQuery('.djs-popup', popup._current.container); 153 | 154 | return popupElement.classList.contains(menuId); 155 | } -------------------------------------------------------------------------------- /test/spec/element-templates/replace-menu/RemoveTemplateReplaceProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | getBpmnJS, 4 | bootstrapModeler 5 | } from 'test/TestHelper'; 6 | 7 | import diagramXML from './RemoveTemplateReplaceProvider.bpmn'; 8 | import templates from './RemoveTemplateReplaceProvider.element-templates.json'; 9 | 10 | import { isString } from 'min-dash'; 11 | 12 | import { 13 | BpmnPropertiesPanelModule, 14 | BpmnPropertiesProviderModule, 15 | ZeebePropertiesProviderModule, 16 | } from 'bpmn-js-properties-panel'; 17 | 18 | import { 19 | CloudElementTemplatesPropertiesProviderModule as ElementTemplatesProviderModule 20 | } from 'bpmn-js-element-templates'; 21 | 22 | import { CreateAppendElementTemplatesModule } from 'lib/'; 23 | 24 | import ZeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; 25 | 26 | 27 | describe('', function() { 28 | 29 | beforeEach(bootstrapModeler(diagramXML, { 30 | additionalModules: [ 31 | BpmnPropertiesPanelModule, 32 | BpmnPropertiesProviderModule, 33 | ZeebePropertiesProviderModule, 34 | ElementTemplatesProviderModule, 35 | CreateAppendElementTemplatesModule 36 | ], 37 | moddleExtensions: { 38 | zeebe: ZeebeModdle 39 | } 40 | })); 41 | 42 | beforeEach(inject(function(elementTemplates) { 43 | elementTemplates.set(templates); 44 | })); 45 | 46 | 47 | describe('display', function() { 48 | 49 | it('should not display (task)', inject(function(elementRegistry) { 50 | 51 | // given 52 | const task = elementRegistry.get('Task_1'); 53 | 54 | // when 55 | openPopup(task); 56 | 57 | // then 58 | const entries = getEntries(); 59 | 60 | expect(entries).not.to.have.property('replace-remove-element-template'); 61 | })); 62 | 63 | 64 | it('should display (template service task -> service task)', inject(function() { 65 | 66 | // given 67 | const element = applyTemplate( 68 | 'ServiceTask_1', 69 | 'com.camunda.example.MailTask' 70 | ); 71 | 72 | // when 73 | openPopup(element); 74 | 75 | // then 76 | const entries = getEntries(); 77 | 78 | expect(entries).to.have.property('replace-remove-element-template'); 79 | })); 80 | 81 | 82 | it('should display (template task -> task)', inject(function() { 83 | 84 | // given 85 | const element = applyTemplate( 86 | 'Task_1', 87 | 'example.TaskTemplate' 88 | ); 89 | 90 | // when 91 | openPopup(element); 92 | 93 | // then 94 | const entries = getEntries(); 95 | 96 | expect(entries).to.have.property('replace-remove-element-template'); 97 | })); 98 | 99 | 100 | it('should display (template transaction -> transaction)', inject(function() { 101 | 102 | // given 103 | const element = applyTemplate( 104 | 'SUB_PROCESS', 105 | 'example.TransactionTemplate' 106 | ); 107 | 108 | // when 109 | openPopup(element); 110 | 111 | // then 112 | const entries = getEntries(); 113 | 114 | expect(entries).to.have.property('replace-remove-element-template'); 115 | })); 116 | 117 | }); 118 | 119 | }); 120 | 121 | 122 | // helpers //////////// 123 | 124 | function openPopup(element, offset) { 125 | offset = offset || 100; 126 | 127 | getBpmnJS().invoke(function(popupMenu) { 128 | popupMenu.open(element, 'bpmn-replace', { 129 | x: element.x, y: element.y 130 | }); 131 | 132 | }); 133 | } 134 | 135 | function getEntries() { 136 | const popupMenu = getBpmnJS().get('popupMenu'); 137 | return popupMenu._current.entries; 138 | } 139 | 140 | function applyTemplate(element, template) { 141 | 142 | return getBpmnJS().invoke(function(elementTemplates, elementRegistry) { 143 | 144 | if (isString(element)) { 145 | element = elementRegistry.get(element); 146 | } 147 | 148 | if (isString(template)) { 149 | template = templates.find(t => t.id === template); 150 | } 151 | 152 | expect(element).to.exist; 153 | expect(template).to.exist; 154 | 155 | return elementTemplates.applyTemplate(element, template); 156 | }); 157 | } -------------------------------------------------------------------------------- /lib/create-append-anything/append-menu/AppendMenuProvider.js: -------------------------------------------------------------------------------- 1 | import { isUndefined } from 'min-dash'; 2 | import { CREATE_OPTIONS } from '../../util/CreateOptionsUtil'; 3 | 4 | /** 5 | * This module is an append menu provider for the popup menu. 6 | */ 7 | export default function AppendMenuProvider( 8 | elementFactory, popupMenu, 9 | create, autoPlace, rules, 10 | mouse, translate 11 | ) { 12 | 13 | this._elementFactory = elementFactory; 14 | this._popupMenu = popupMenu; 15 | this._create = create; 16 | this._autoPlace = autoPlace; 17 | this._rules = rules; 18 | this._create = create; 19 | this._mouse = mouse; 20 | this._translate = translate; 21 | 22 | this.register(); 23 | } 24 | 25 | AppendMenuProvider.$inject = [ 26 | 'elementFactory', 27 | 'popupMenu', 28 | 'create', 29 | 'autoPlace', 30 | 'rules', 31 | 'mouse', 32 | 'translate' 33 | ]; 34 | 35 | /** 36 | * Register append menu provider in the popup menu 37 | */ 38 | AppendMenuProvider.prototype.register = function() { 39 | this._popupMenu.registerProvider('bpmn-append', this); 40 | }; 41 | 42 | /** 43 | * Gets the append options for the given element as menu entries 44 | * 45 | * @param {djs.model.Base} element 46 | * 47 | * @return {Array} a list of menu entry items 48 | */ 49 | AppendMenuProvider.prototype.getPopupMenuEntries = function(element) { 50 | const rules = this._rules; 51 | const translate = this._translate; 52 | 53 | const entries = {}; 54 | 55 | if (!rules.allowed('shape.append', { element: element })) { 56 | return []; 57 | } 58 | 59 | // filter out elements with no incoming connections 60 | const appendOptions = this._filterEntries(CREATE_OPTIONS); 61 | 62 | // map options to menu entries 63 | appendOptions.forEach(option => { 64 | const { 65 | actionName, 66 | className, 67 | label, 68 | target, 69 | description, 70 | group, 71 | search, 72 | rank 73 | } = option; 74 | 75 | entries[`append-${actionName}`] = { 76 | label: label && translate(label), 77 | className, 78 | description, 79 | group: group && { 80 | ...group, 81 | name: translate(group.name) 82 | }, 83 | search, 84 | rank, 85 | action: this._createEntryAction(element, target) 86 | }; 87 | }); 88 | 89 | return entries; 90 | }; 91 | 92 | /** 93 | * Filter out entries from the options. 94 | * 95 | * @param {Array} entries 96 | * 97 | * @return {Array} filtered entries 98 | */ 99 | AppendMenuProvider.prototype._filterEntries = function(entries) { 100 | return entries.filter(option => { 101 | 102 | const target = option.target; 103 | const { 104 | type, 105 | eventDefinitionType 106 | } = target; 107 | 108 | if ([ 109 | 'bpmn:StartEvent', 110 | 'bpmn:Participant' 111 | ].includes(type)) { 112 | return false; 113 | } 114 | 115 | if (type === 'bpmn:BoundaryEvent' && isUndefined(eventDefinitionType)) { 116 | return false; 117 | } 118 | 119 | return true; 120 | }); 121 | }; 122 | 123 | /** 124 | * Create an action for a given target. 125 | * 126 | * @param {djs.model.Base} element 127 | * @param {Object} target 128 | * 129 | * @return {Object} 130 | */ 131 | AppendMenuProvider.prototype._createEntryAction = function(element, target) { 132 | const elementFactory = this._elementFactory; 133 | const autoPlace = this._autoPlace; 134 | const create = this._create; 135 | const mouse = this._mouse; 136 | 137 | 138 | const autoPlaceElement = () => { 139 | const newElement = elementFactory.create('shape', target); 140 | autoPlace.append(element, newElement); 141 | }; 142 | 143 | const manualPlaceElement = (event) => { 144 | const newElement = elementFactory.create('shape', target); 145 | 146 | if (event instanceof KeyboardEvent) { 147 | event = mouse.getLastMoveEvent(); 148 | } 149 | 150 | return create.start(event, newElement, { 151 | source: element 152 | }); 153 | }; 154 | 155 | return { 156 | click: this._canAutoPlaceElement(target) ? autoPlaceElement : manualPlaceElement, 157 | dragstart: manualPlaceElement 158 | }; 159 | }; 160 | 161 | /** 162 | * Check if the element should be auto placed. 163 | * 164 | * @param {Object} target 165 | * 166 | * @return {Boolean} 167 | */ 168 | AppendMenuProvider.prototype._canAutoPlaceElement = (target) => { 169 | const { type } = target; 170 | 171 | if (type === 'bpmn:BoundaryEvent') { 172 | return false; 173 | } 174 | 175 | if (type === 'bpmn:SubProcess' && target.triggeredByEvent) { 176 | return false; 177 | } 178 | 179 | if (type === 'bpmn:IntermediateCatchEvent' && target.eventDefinitionType === 'bpmn:LinkEventDefinition') { 180 | return false; 181 | } 182 | 183 | return true; 184 | }; -------------------------------------------------------------------------------- /lib/element-templates/remove-templates/RemoveTemplateReplaceProvider.js: -------------------------------------------------------------------------------- 1 | import { isDifferentType } from 'bpmn-js/lib/features/popup-menu/util/TypeUtil'; 2 | import { getReplaceOptionGroups } from '../../util/ReplaceOptionsUtil'; 3 | 4 | /** 5 | * A replace menu provider that allows to replace elements with 6 | * templates applied with the correspondent plain element. 7 | */ 8 | export default function RemoveTemplateReplaceProvider(popupMenu, translate, elementTemplates) { 9 | 10 | this._popupMenu = popupMenu; 11 | this._translate = translate; 12 | this._elementTemplates = elementTemplates; 13 | 14 | this.register(); 15 | } 16 | 17 | RemoveTemplateReplaceProvider.$inject = [ 18 | 'popupMenu', 19 | 'translate', 20 | 'elementTemplates' 21 | ]; 22 | 23 | /** 24 | * Register replace menu provider in the popup menu 25 | */ 26 | RemoveTemplateReplaceProvider.prototype.register = function() { 27 | this._popupMenu.registerProvider('bpmn-replace', this); 28 | }; 29 | 30 | /** 31 | * Adds the element templates to the replace menu. 32 | * @param {djs.model.Base} element 33 | * 34 | * @returns {Object} 35 | */ 36 | RemoveTemplateReplaceProvider.prototype.getPopupMenuEntries = function(element) { 37 | 38 | return (entries) => { 39 | 40 | // convert our entries into something sortable 41 | let entrySet = Object.entries(entries); 42 | 43 | if (this._elementTemplates && this._elementTemplates.get(element)) { 44 | 45 | // add remove template option 46 | this.addPlainElementEntry(element, entrySet, this._translate, this._elementTemplates); 47 | } 48 | 49 | // convert back to object 50 | return entrySet.reduce((entries, [ key, value ]) => { 51 | entries[key] = value; 52 | 53 | return entries; 54 | }, {}); 55 | }; 56 | }; 57 | 58 | 59 | /** 60 | * Adds the option to replace with plain element (remove template). 61 | * 62 | * @param {djs.model.Base} element 63 | * @param {Array} entries 64 | */ 65 | RemoveTemplateReplaceProvider.prototype.addPlainElementEntry = function(element, entries, translate, elementTemplates) { 66 | 67 | const replaceOption = this.getPlainEntry(element, entries, translate, elementTemplates); 68 | 69 | if (!replaceOption) { 70 | return; 71 | } 72 | 73 | const [ 74 | insertIndex, 75 | entry 76 | ] = replaceOption; 77 | 78 | // insert remove entry 79 | entries.splice(insertIndex, 0, [ entry.id, entry ]); 80 | }; 81 | 82 | /** 83 | * Returns the option to replace with plain element and the index where it should be inserted. 84 | * 85 | * @param {djs.model.Base} element 86 | * @param {Array} entries 87 | * 88 | * @returns {Array} 89 | */ 90 | RemoveTemplateReplaceProvider.prototype.getPlainEntry = function(element, entries, translate, elementTemplates) { 91 | 92 | const { 93 | options, 94 | option, 95 | optionIndex 96 | } = findReplaceOptions(element) || { }; 97 | 98 | if (!options) { 99 | return null; 100 | } 101 | 102 | const entry = { 103 | id: 'replace-remove-element-template', 104 | action: () => { 105 | elementTemplates.removeTemplate(element); 106 | }, 107 | label: translate(option.label), 108 | className: option.className 109 | }; 110 | 111 | // insert after previous option, if it exists 112 | const previousIndex = getOptionIndex(options, optionIndex - 1, entries); 113 | 114 | if (previousIndex) { 115 | return [ 116 | previousIndex + 1, 117 | entry 118 | ]; 119 | } 120 | 121 | // insert before next option, if it exists 122 | const nextIndex = getOptionIndex(options, optionIndex + 1, entries); 123 | 124 | if (nextIndex) { 125 | return [ 126 | nextIndex, 127 | entry 128 | ]; 129 | } 130 | 131 | // fallback to insert at start 132 | return [ 133 | 0, 134 | entry 135 | ]; 136 | }; 137 | 138 | 139 | /** 140 | * @param {ModdleElement} element 141 | * 142 | * @return { { options: Array, option: any, optionIndex: number } | null } 143 | */ 144 | function findReplaceOptions(element) { 145 | 146 | const isSameType = (element, option) => option.target && !isDifferentType(element)(option); 147 | 148 | return getReplaceOptionGroups().reduce((result, options) => { 149 | 150 | if (result) { 151 | return result; 152 | } 153 | 154 | const optionIndex = options.findIndex(option => isSameType(element, option)); 155 | 156 | if (optionIndex === -1) { 157 | return; 158 | } 159 | 160 | return { 161 | options, 162 | option: options[optionIndex], 163 | optionIndex 164 | }; 165 | }, null); 166 | } 167 | 168 | function getOptionIndex(options, index, entries) { 169 | const option = options[index]; 170 | 171 | if (!option) { 172 | return false; 173 | } 174 | 175 | return entries.findIndex( 176 | ([ key ]) => key === option.actionName 177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /test/spec/element-templates/replace-menu/RemoveTemplateReplaceProvider.element-templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$schema": "https://unpkg.com/@camunda/element-templates-json-schema/resources/schema.json", 4 | "name": "Mail Task", 5 | "id": "com.camunda.example.MailTask", 6 | "appliesTo": [ 7 | "bpmn:ServiceTask" 8 | ], 9 | "properties": [ 10 | { 11 | "label": "Implementation Type", 12 | "type": "String", 13 | "value": "com.mycompany.MailTaskImpl", 14 | "editable": false, 15 | "binding": { 16 | "type": "property", 17 | "name": "camunda:class" 18 | } 19 | }, 20 | { 21 | "label": "Sender", 22 | "type": "String", 23 | "binding": { 24 | "type": "camunda:inputParameter", 25 | "name": "sender" 26 | }, 27 | "constraints": { 28 | "notEmpty": true, 29 | "pattern": { 30 | "value": "^[A-z0-9._%+-]+@[A-z0-9.-]+\\.[A-z]{2,}$", 31 | "message": "Must be a valid email." 32 | } 33 | } 34 | }, 35 | { 36 | "label": "Receivers", 37 | "type": "String", 38 | "binding": { 39 | "type": "camunda:inputParameter", 40 | "name": "receivers" 41 | }, 42 | "constraints": { 43 | "notEmpty": true 44 | } 45 | }, 46 | { 47 | "label": "Template", 48 | "description": "By the way, you can use freemarker templates here", 49 | "value": "Hello ${firstName}!", 50 | "type": "Text", 51 | "binding": { 52 | "type": "camunda:inputParameter", 53 | "name": "messageBody", 54 | "scriptFormat": "freemarker" 55 | }, 56 | "constraints": { 57 | "notEmpty": true 58 | } 59 | }, 60 | { 61 | "label": "Result Status", 62 | "description": "The process variable to which to assign the send result to", 63 | "value": "mailSendResult", 64 | "type": "String", 65 | "binding": { 66 | "type": "camunda:outputParameter", 67 | "source": "${ resultStatus }" 68 | } 69 | }, 70 | { 71 | "label": "Send Async?", 72 | "type": "Boolean", 73 | "value": true, 74 | "binding": { 75 | "type": "property", 76 | "name": "camunda:asyncBefore" 77 | } 78 | } 79 | ] 80 | }, 81 | { 82 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 83 | "name": "Task Template", 84 | "id": "example.TaskTemplate", 85 | "appliesTo": [ 86 | "bpmn:Task" 87 | ], 88 | "properties": [ 89 | { 90 | "label": "Are you awesome?", 91 | "type": "Boolean", 92 | "value": true, 93 | "binding": { 94 | "type": "property", 95 | "name": "customProperty" 96 | } 97 | } 98 | ] 99 | }, 100 | { 101 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 102 | "name": "Transaction Template", 103 | "id": "example.TransactionTemplate", 104 | "description": "Tests Transation templating", 105 | "icon": { 106 | "contents": "data:image/svg+xml;utf8,%3Csvg%20width%3D%2218%22%20height%3D%2218%22%20viewBox%3D%220%200%2018%2018%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M17.0335%208.99997C17.0335%2013.4475%2013.4281%2017.0529%208.98065%2017.0529C4.53316%2017.0529%200.927765%2013.4475%200.927765%208.99997C0.927765%204.55248%204.53316%200.947083%208.98065%200.947083C13.4281%200.947083%2017.0335%204.55248%2017.0335%208.99997Z%22%20fill%3D%22%23505562%22%2F%3E%0A%3Cpath%20d%3D%22M4.93126%2014.1571L6.78106%203.71471H10.1375C11.1917%203.71471%2011.9824%203.98323%2012.5095%204.52027C13.0465%205.04736%2013.315%205.73358%2013.315%206.57892C13.315%207.44414%2013.0714%208.15522%2012.5841%208.71215C12.1067%209.25913%2011.4553%209.63705%2010.6298%209.8459L12.0619%2014.1571H10.3315L9.03364%2010.0249H7.24351L6.51254%2014.1571H4.93126ZM7.49711%208.59281H9.24248C9.99832%208.59281%2010.5901%208.42374%2011.0177%208.08561C11.4553%207.73753%2011.6741%207.26513%2011.6741%206.66842C11.6741%206.19106%2011.5249%205.81811%2011.2265%205.54959C10.9282%205.27113%2010.4558%205.1319%209.80936%205.1319H8.10874L7.49711%208.59281Z%22%20fill%3D%22white%22%2F%3E%0A%3C%2Fsvg%3E%0A" 107 | }, 108 | "documentationRef": "https://docs.camunda.io/docs/components/modeler/web-modeler/connectors/available-connectors/rest/", 109 | "appliesTo": [ 110 | "bpmn:SubProcess" 111 | ], 112 | "elementType": { 113 | "value": "bpmn:Transaction" 114 | }, 115 | "properties": [ 116 | { 117 | "label": "Prop", 118 | "type": "String", 119 | "feel": "optional", 120 | "binding": { 121 | "type": "zeebe:property", 122 | "name": "prop" 123 | } 124 | } 125 | ] 126 | } 127 | ] -------------------------------------------------------------------------------- /lib/element-templates/append-menu/ElementTemplatesAppendProvider.js: -------------------------------------------------------------------------------- 1 | import { assign } from 'min-dash'; 2 | 3 | /** 4 | * A popup menu provider that allows to append elements with 5 | * element templates. 6 | */ 7 | export default function ElementTemplatesAppendProvider( 8 | popupMenu, translate, elementTemplates, 9 | autoPlace, create, mouse, rules) { 10 | 11 | this._popupMenu = popupMenu; 12 | this._translate = translate; 13 | this._elementTemplates = elementTemplates; 14 | this._autoPlace = autoPlace; 15 | this._create = create; 16 | this._mouse = mouse; 17 | this._rules = rules; 18 | 19 | this.register(); 20 | } 21 | 22 | ElementTemplatesAppendProvider.$inject = [ 23 | 'popupMenu', 24 | 'translate', 25 | 'elementTemplates', 26 | 'autoPlace', 27 | 'create', 28 | 'move', 29 | 'rules' 30 | ]; 31 | 32 | /** 33 | * Register append menu provider in the popup menu 34 | */ 35 | ElementTemplatesAppendProvider.prototype.register = function() { 36 | this._popupMenu.registerProvider('bpmn-append', this); 37 | }; 38 | 39 | /** 40 | * Adds the element templates to the append menu. 41 | * @param {djs.model.Base} element 42 | * 43 | * @returns {Object} 44 | */ 45 | ElementTemplatesAppendProvider.prototype.getPopupMenuEntries = function(element) { 46 | return (entries) => { 47 | 48 | if (!this._rules.allowed('shape.append', { element: element })) { 49 | return []; 50 | } 51 | 52 | const filteredTemplates = this._filterTemplates(this._elementTemplates.getLatest()); 53 | 54 | // add template entries 55 | assign(entries, this.getTemplateEntries(element, filteredTemplates)); 56 | 57 | return entries; 58 | }; 59 | }; 60 | 61 | /** 62 | * Get all element templates. 63 | * 64 | * @param {djs.model.Base} element 65 | * 66 | * @return {Object} element templates as menu entries 67 | */ 68 | ElementTemplatesAppendProvider.prototype.getTemplateEntries = function(element, templates) { 69 | 70 | const templateEntries = {}; 71 | 72 | templates.map(template => { 73 | 74 | const { 75 | icon = {}, 76 | category, 77 | keywords = [], 78 | } = template; 79 | 80 | const entryId = `append.template-${template.id}`; 81 | 82 | const defaultGroup = { 83 | id: 'templates', 84 | name: this._translate('Templates') 85 | }; 86 | 87 | templateEntries[entryId] = { 88 | label: template.name, 89 | description: template.description, 90 | documentationRef: template.documentationRef, 91 | search: keywords, 92 | imageUrl: icon.contents, 93 | group: category || defaultGroup, 94 | action: this._getEntryAction(element, template) 95 | }; 96 | }); 97 | 98 | return templateEntries; 99 | }; 100 | 101 | /** 102 | * Filter out templates from the options. 103 | * 104 | * @param {Array} templates 105 | * 106 | * @returns {Array} 107 | */ 108 | ElementTemplatesAppendProvider.prototype._filterTemplates = function(templates) { 109 | return templates.filter(template => { 110 | const { 111 | appliesTo, 112 | elementType 113 | } = template; 114 | 115 | const type = (elementType && elementType.value) || appliesTo[0]; 116 | 117 | // elements that can not be appended 118 | if ([ 119 | 'bpmn:StartEvent', 120 | 'bpmn:Participant' 121 | ].includes(type)) { 122 | return false; 123 | } 124 | 125 | // sequence flow templates are supported 126 | // but connections are not appendable 127 | if ('bpmn:SequenceFlow' === type) { 128 | return false; 129 | } 130 | 131 | return true; 132 | }); 133 | }; 134 | 135 | /** 136 | * Create an action for a given template. 137 | * 138 | * @param {djs.model.Base} element 139 | * @param {Object} template 140 | * 141 | * @returns {Object} 142 | */ 143 | ElementTemplatesAppendProvider.prototype._getEntryAction = function(element, template) { 144 | const autoPlaceElement = () => { 145 | const newElement = this._elementTemplates.createElement(template); 146 | 147 | this._autoPlace.append(element, newElement); 148 | }; 149 | 150 | const manualPlaceElement = (event) => { 151 | const newElement = this._elementTemplates.createElement(template); 152 | 153 | if (event instanceof KeyboardEvent) { 154 | event = this._mouse.getLastMoveEvent(); 155 | } 156 | 157 | return this._create.start(event, newElement, { 158 | source: element 159 | }); 160 | }; 161 | 162 | return { 163 | click: canAutoPlaceElement(template) ? autoPlaceElement : manualPlaceElement, 164 | dragstart: manualPlaceElement 165 | }; 166 | }; 167 | 168 | function canAutoPlaceElement(elementTemplate) { 169 | const { 170 | appliesTo = [], 171 | elementType = {} 172 | } = elementTemplate; 173 | 174 | const type = elementType.value || appliesTo[0]; 175 | 176 | if (type === 'bpmn:BoundaryEvent') { 177 | return false; 178 | } 179 | 180 | if (type === 'bpmn:IntermediateCatchEvent' && elementType.eventDefinition === 'bpmn:LinkEventDefinition') { 181 | return false; 182 | } 183 | 184 | return true; 185 | } -------------------------------------------------------------------------------- /test/fixtures/element-templates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 4 | "name": "Task Template", 5 | "id": "example.TaskTemplate", 6 | "appliesTo": [ 7 | "bpmn:Task" 8 | ], 9 | "properties": [ 10 | { 11 | "type": "Boolean", 12 | "binding": { 13 | "type": "property", 14 | "name": "foo" 15 | } 16 | } 17 | ] 18 | }, 19 | { 20 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 21 | "name": "Participant Template", 22 | "id": "example.ParticipantTemplate", 23 | "category": { 24 | "id": "process-templates", 25 | "name": "Process Templates" 26 | }, 27 | "icon": { 28 | "contents": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2049.363%2027.445%22%3E%3Cpath%20d%3D%22M1.818%2012.82h46.363v24.445H1.818z%22%20style%3D%22fill%3A%23fff%3Bstroke%3A%2310ad73%3Bstroke-width%3A3%3Bstroke-linecap%3Around%3Bstroke-linejoin%3Amiter%3Bstroke-dasharray%3Anone%22%20transform%3D%22translate(-.318%20-11.32)%22%2F%3E%3Cpath%20d%3D%22m10.63%2018.247%206.684%206.684-6.684%206.683%22%20style%3D%22fill%3Anone%3Bstroke%3A%2310ad73%3Bstroke-width%3A1.7135%3Bstroke-linecap%3Around%3Bstroke-linejoin%3Around%22%20transform%3D%22translate(4.551%20-11.32)%22%2F%3E%3Cpath%20d%3D%22m16.788%2017.995%206.684%206.684-6.684%206.684%22%20style%3D%22fill%3Anone%3Bstroke%3A%2310ad73%3Bstroke-width%3A1.7135%3Bstroke-linecap%3Around%3Bstroke-linejoin%3Around%22%20transform%3D%22translate(4.551%20-11.32)%22%2F%3E%3Cpath%20d%3D%22m22.947%2017.975%206.683%206.684-6.683%206.683%22%20style%3D%22fill%3Anone%3Bstroke%3A%2310ad73%3Bstroke-width%3A1.7135%3Bstroke-linecap%3Around%3Bstroke-linejoin%3Around%22%20transform%3D%22translate(4.551%20-11.32)%22%2F%3E%3C%2Fsvg%3E" 29 | }, 30 | "appliesTo": [ 31 | "bpmn:Participant" 32 | ], 33 | "properties": [ 34 | { 35 | "binding": { 36 | "type": "property", 37 | "name": "foo" 38 | } 39 | } 40 | ] 41 | }, 42 | { 43 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 44 | "name": "Start Event Template", 45 | "id": "example.StartEventTemplate", 46 | "category": { 47 | "id": "events", 48 | "name": "Events" 49 | }, 50 | "icon": { 51 | "contents": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2049.388%2049.388%22%3E%3Ccircle%20cx%3D%2224.999%22%20cy%3D%2225.066%22%20r%3D%2223.233%22%20style%3D%22fill%3A%23fff%3Bstroke%3A%2310ad73%3Bstroke-width%3A2.9216%3Bstroke-linecap%3Around%3Bstroke-linejoin%3Around%3Bstroke-dasharray%3Anone%22%20transform%3D%22translate(-.306%20-.372)%22%2F%3E%3C%2Fsvg%3E" 52 | }, 53 | "appliesTo": [ 54 | "bpmn:StartEvent" 55 | ], 56 | "properties": [ 57 | { 58 | "binding": { 59 | "type": "property", 60 | "name": "foo" 61 | } 62 | } 63 | ] 64 | }, 65 | { 66 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 67 | "name": "Sequence Flow Template", 68 | "id": "example.SequenceFlowTemplate", 69 | "appliesTo": [ 70 | "bpmn:SequenceFlow" 71 | ], 72 | "properties": [ 73 | { 74 | "type": "String", 75 | "binding": { 76 | "type": "property", 77 | "name": "conditionExpression" 78 | } 79 | } 80 | ] 81 | }, 82 | { 83 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 84 | "name": "Keywords Template", 85 | "id": "example.KeywordsTemplate", 86 | "icon": { 87 | "contents": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2050%2050%22%3E%3Ctext%20xml%3Aspace%3D%22preserve%22%20x%3D%223.615%22%20y%3D%2229.833%22%20style%3D%22font-style%3Anormal%3Bfont-variant%3Anormal%3Bfont-weight%3A400%3Bfont-stretch%3Anormal%3Bfont-size%3A17.6087px%3Bline-height%3A1.25%3Bfont-family%3Amonospace%3Bfill%3A%2310ad73%3Bfill-opacity%3A1%3Bstroke%3Anone%3Bstroke-width%3A.440217%3Bstroke-opacity%3A1%22%3E%3Ctspan%20x%3D%223.615%22%20y%3D%2229.833%22%20style%3D%22font-style%3Anormal%3Bfont-variant%3Anormal%3Bfont-weight%3A400%3Bfont-stretch%3Anormal%3Bfont-family%3Amonospace%3Bfill%3A%2310ad73%3Bfill-opacity%3A1%3Bstroke%3Anone%3Bstroke-width%3A.440217%3Bstroke-opacity%3A1%22%3E%23abc%3C%2Ftspan%3E%3C%2Ftext%3E%3C%2Fsvg%3E" 88 | }, 89 | "category": { 90 | "id": "other-items", 91 | "name": "Other items" 92 | }, 93 | "appliesTo": [ 94 | "bpmn:Task" 95 | ], 96 | "properties": [], 97 | "keywords": [ 98 | "first keyword", 99 | "another keyword" 100 | ] 101 | }, 102 | { 103 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 104 | "name": "Message Boundary Event Template", 105 | "id": "example.MessageBoundaryEventTemplate", 106 | "appliesTo": [ 107 | "bpmn:BoundaryEvent" 108 | ], 109 | "elementType": { 110 | "value": "bpmn:BoundaryEvent", 111 | "eventDefinition": "bpmn:MessageEventDefinition" 112 | }, 113 | "properties": [ 114 | { 115 | "binding": { 116 | "type": "property", 117 | "name": "foo" 118 | } 119 | } 120 | ] 121 | }, 122 | { 123 | "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", 124 | "name": "Link Intermediate Catch Event Template", 125 | "id": "example.LinkIntermediateCatchEventTemplate", 126 | "appliesTo": [ 127 | "bpmn:IntermediateCatchEvent" 128 | ], 129 | "elementType": { 130 | "value": "bpmn:IntermediateCatchEvent", 131 | "eventDefinition": "bpmn:LinkEventDefinition" 132 | }, 133 | "properties": [ 134 | { 135 | "binding": { 136 | "type": "property", 137 | "name": "foo" 138 | } 139 | } 140 | ] 141 | } 142 | ] -------------------------------------------------------------------------------- /test/TestHelper.js: -------------------------------------------------------------------------------- 1 | /* global global */ 2 | 3 | import { 4 | act, 5 | fireEvent 6 | } from '@testing-library/preact'; 7 | 8 | import TestContainer from 'mocha-test-container-support'; 9 | 10 | import { 11 | bootstrapBpmnJS, 12 | getBpmnJS, 13 | inject, 14 | insertCSS 15 | } from 'bpmn-js/test/helper'; 16 | 17 | import semver from 'semver'; 18 | 19 | import fileDrop from 'file-drops'; 20 | 21 | import download from 'downloadjs'; 22 | 23 | import Modeler from 'bpmn-js/lib/Modeler'; 24 | 25 | 26 | let PROPERTIES_PANEL_CONTAINER; 27 | 28 | global.chai.use(function(chai, utils) { 29 | 30 | utils.addMethod(chai.Assertion.prototype, 'jsonEqual', function(comparison) { 31 | 32 | var actual = JSON.stringify(this._obj); 33 | var expected = JSON.stringify(comparison); 34 | 35 | this.assert( 36 | actual == expected, 37 | 'expected #{this} to deep equal #{act}', 38 | 'expected #{this} not to deep equal #{act}', 39 | comparison, // expected 40 | this._obj, // actual 41 | true // show diff 42 | ); 43 | }); 44 | }); 45 | 46 | export * from 'bpmn-js/test/helper'; 47 | 48 | export { 49 | createCanvasEvent, 50 | createEvent 51 | } from 'bpmn-js/test/util/MockEvents'; 52 | 53 | export function bootstrapPropertiesPanel(diagram, options, locals) { 54 | return async function() { 55 | const container = TestContainer.get(this); 56 | 57 | insertBpmnStyles(); 58 | insertCoreStyles(); 59 | 60 | // (1) create modeler + import diagram 61 | const createModeler = bootstrapBpmnJS(Modeler, diagram, options, locals); 62 | await act(() => createModeler.call(this)); 63 | 64 | // (2) clean-up properties panel 65 | clearPropertiesPanelContainer(); 66 | 67 | // (3) attach properties panel 68 | const attachPropertiesPanel = inject(function(propertiesPanel) { 69 | PROPERTIES_PANEL_CONTAINER = document.createElement('div'); 70 | PROPERTIES_PANEL_CONTAINER.classList.add('properties-container'); 71 | 72 | container.appendChild(PROPERTIES_PANEL_CONTAINER); 73 | 74 | return act(() => propertiesPanel.attachTo(PROPERTIES_PANEL_CONTAINER)); 75 | }); 76 | await attachPropertiesPanel(); 77 | }; 78 | } 79 | 80 | export function clearPropertiesPanelContainer() { 81 | if (PROPERTIES_PANEL_CONTAINER) { 82 | PROPERTIES_PANEL_CONTAINER.remove(); 83 | } 84 | } 85 | 86 | export function changeInput(input, value) { 87 | fireEvent.input(input, { target: { value } }); 88 | } 89 | 90 | export function clickInput(input) { 91 | fireEvent.click(input); 92 | } 93 | 94 | export function insertCoreStyles() { 95 | insertCSS( 96 | 'properties-panel.css', 97 | require('@bpmn-io/properties-panel/dist/assets/properties-panel.css').default 98 | ); 99 | 100 | insertCSS( 101 | 'element-templates.css', 102 | require('bpmn-js-element-templates/dist/assets/element-templates.css').default 103 | ); 104 | 105 | insertCSS( 106 | 'test.css', 107 | require('./test.css').default 108 | ); 109 | } 110 | 111 | export function insertBpmnStyles() { 112 | insertCSS( 113 | 'diagram.css', 114 | require('bpmn-js/dist/assets/diagram-js.css').default 115 | ); 116 | 117 | // @barmac: this fails before bpmn-js@9 118 | if (bpmnJsSatisfies('>=9')) { 119 | insertCSS( 120 | 'bpmn-js.css', 121 | require('bpmn-js/dist/assets/bpmn-js.css').default 122 | ); 123 | } 124 | 125 | insertCSS( 126 | 'bpmn-font.css', 127 | require('bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css').default 128 | ); 129 | 130 | insertCSS( 131 | 'element-template-chooser.css', 132 | require('@bpmn-io/element-template-chooser/dist/element-template-chooser.css').default 133 | ); 134 | } 135 | 136 | export function bootstrapModeler(diagram, options, locals) { 137 | return bootstrapBpmnJS(Modeler, diagram, options, locals); 138 | } 139 | 140 | /** 141 | * Execute test only if currently installed bpmn-js is of given version. 142 | * 143 | * @param {string} versionRange 144 | * @param {boolean} only 145 | */ 146 | export function withBpmnJs(versionRange, only = false) { 147 | if (bpmnJsSatisfies(versionRange)) { 148 | return only ? it.only : it; 149 | } else { 150 | return it.skip; 151 | } 152 | } 153 | 154 | function bpmnJsSatisfies(versionRange) { 155 | const bpmnJsVersion = require('bpmn-js/package.json').version; 156 | 157 | return semver.satisfies(bpmnJsVersion, versionRange, { includePrerelease: true }); 158 | } 159 | 160 | /** 161 | * Execute test only if currently installed @bpmn-io/properties-panel is of given version. 162 | * 163 | * @param {string} versionRange 164 | * @param {boolean} only 165 | */ 166 | export function withPropertiesPanel(versionRange, only = false) { 167 | if (propertiesPanelSatisfies(versionRange)) { 168 | return only ? it.only : it; 169 | } else { 170 | return it.skip; 171 | } 172 | } 173 | 174 | function propertiesPanelSatisfies(versionRange) { 175 | const version = require('@bpmn-io/properties-panel/package.json').version; 176 | 177 | return semver.satisfies(version, versionRange, { includePrerelease: true }); 178 | } 179 | 180 | 181 | export async function setEditorValue(editor, value) { 182 | await act(() => { 183 | editor.textContent = value; 184 | }); 185 | 186 | // Requires 2 ticks to propagate the change to bpmn-js 187 | await act(() => {}); 188 | } 189 | 190 | // be able to load files into running bpmn-js test cases 191 | document.documentElement.addEventListener('dragover', fileDrop('Drop a BPMN diagram to open it in the currently active test.', function(files) { 192 | const bpmnJS = getBpmnJS(); 193 | 194 | if (bpmnJS && files.length === 1) { 195 | bpmnJS.importXML(files[0].contents); 196 | } 197 | })); 198 | 199 | insertCSS('file-drops.css', ` 200 | .drop-overlay .box { 201 | background: orange; 202 | border-radius: 3px; 203 | display: inline-block; 204 | font-family: sans-serif; 205 | padding: 4px 10px; 206 | position: fixed; 207 | top: 30px; 208 | left: 50%; 209 | transform: translateX(-50%); 210 | } 211 | `); 212 | 213 | // be able to download diagrams using CTRL/CMD+S 214 | document.addEventListener('keydown', function(event) { 215 | const bpmnJS = getBpmnJS(); 216 | 217 | if (!bpmnJS) { 218 | return; 219 | } 220 | 221 | if (!(event.ctrlKey || event.metaKey) || event.code !== 'KeyS') { 222 | return; 223 | } 224 | 225 | event.preventDefault(); 226 | 227 | bpmnJS.saveXML({ format: true }).then(function(result) { 228 | download(result.xml, 'test.bpmn', 'application/xml'); 229 | }); 230 | }); -------------------------------------------------------------------------------- /test/spec/element-templates/create-menu/ElementTemplatesCreateProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | getBpmnJS, 4 | bootstrapModeler, 5 | createCanvasEvent, 6 | createEvent as globalEvent 7 | } from 'test/TestHelper'; 8 | 9 | import { 10 | query as domQuery 11 | } from 'min-dom'; 12 | 13 | import { 14 | isTemplateApplied 15 | } from 'lib/element-templates/replace-menu/ElementTemplatesReplaceProvider'; 16 | 17 | import diagramXML from './ElementTemplatesCreateProvider.bpmn'; 18 | import templates from 'test/fixtures/element-templates.json'; 19 | 20 | import { 21 | is 22 | } from 'bpmn-js/lib/util/ModelUtil'; 23 | 24 | import { 25 | BpmnPropertiesPanelModule, 26 | BpmnPropertiesProviderModule, 27 | ZeebePropertiesProviderModule 28 | } from 'bpmn-js-properties-panel'; 29 | 30 | import { 31 | CloudElementTemplatesPropertiesProviderModule as ElementTemplatesProviderModule 32 | } from 'bpmn-js-element-templates'; 33 | 34 | import { CreateAppendElementTemplatesModule } from 'lib/'; 35 | 36 | import ZeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; 37 | 38 | 39 | describe('', function() { 40 | 41 | beforeEach(bootstrapModeler(diagramXML, { 42 | additionalModules: [ 43 | BpmnPropertiesPanelModule, 44 | BpmnPropertiesProviderModule, 45 | ZeebePropertiesProviderModule, 46 | ElementTemplatesProviderModule, 47 | CreateAppendElementTemplatesModule 48 | ], 49 | moddleExtensions: { 50 | zeebe: ZeebeModdle 51 | } 52 | })); 53 | 54 | beforeEach(inject(function(elementTemplates) { 55 | elementTemplates.set(templates); 56 | })); 57 | 58 | 59 | describe('display', function() { 60 | 61 | it('should display template options', inject(function(canvas) { 62 | 63 | // given 64 | const rootElement = canvas.getRootElement(); 65 | 66 | // when 67 | openPopup(rootElement); 68 | 69 | // then 70 | const entries = Object.keys(getEntries()); 71 | const templateEntries = entries.filter((entry) => entry.startsWith('create.template-')); 72 | 73 | expect(templateEntries.length).to.eql(templates.length); 74 | })); 75 | 76 | }); 77 | 78 | 79 | describe('create', function() { 80 | 81 | it('should create template', inject(function(elementRegistry, selection) { 82 | 83 | // given 84 | const template = templates[0]; 85 | 86 | // when 87 | triggerEntry(`create.template-${template.id}`); 88 | 89 | // then 90 | expectElementWithTemplate(elementRegistry, 'bpmn:Task', template, true); 91 | expectSelected(selection, 'bpmn:Task'); 92 | })); 93 | 94 | 95 | it('should undo', inject(function(elementRegistry, commandStack, selection) { 96 | 97 | // given 98 | const template = templates[0]; 99 | 100 | // when 101 | triggerEntry(`create.template-${template.id}`); 102 | 103 | // then 104 | expectElementWithTemplate(elementRegistry, 'bpmn:Task', template); 105 | expectSelected(selection, 'bpmn:Task'); 106 | 107 | // when 108 | commandStack.undo(); 109 | 110 | // then 111 | expectElementWithTemplate(elementRegistry, 'bpmn:Task', template, false); 112 | expectSelected(selection, 'bpmn:Task', false); 113 | })); 114 | 115 | 116 | it('should redo', inject(function(elementRegistry, commandStack, selection) { 117 | 118 | // given 119 | const template = templates[0]; 120 | 121 | // when 122 | triggerEntry(`create.template-${template.id}`); 123 | commandStack.undo(); 124 | 125 | // then 126 | expectElementWithTemplate(elementRegistry, 'bpmn:Task', template, false); 127 | expectSelected(selection, 'bpmn:Task', false); 128 | 129 | // when 130 | commandStack.redo(); 131 | 132 | // then 133 | expectElementWithTemplate(elementRegistry, 'bpmn:Task', template); 134 | })); 135 | 136 | }); 137 | 138 | describe('search', function() { 139 | 140 | it('should be searchable by keywords', inject(function(canvas) { 141 | 142 | // given 143 | const rootElement = canvas.getRootElement(); 144 | 145 | // when 146 | openPopup(rootElement); 147 | 148 | const entries = getEntries(); 149 | const entry = entries['create.template-example.KeywordsTemplate']; 150 | 151 | // then 152 | expect(entry?.search).to.be.eql([ 'first keyword', 'another keyword' ]); 153 | })); 154 | 155 | }); 156 | 157 | }); 158 | 159 | 160 | // helpers //////////// 161 | 162 | function openPopup(element, offset) { 163 | offset = offset || 100; 164 | 165 | getBpmnJS().invoke(function(popupMenu) { 166 | popupMenu.open(element, 'bpmn-create', { 167 | x: element.x, y: element.y 168 | }); 169 | 170 | }); 171 | } 172 | 173 | function queryEntry(id) { 174 | var container = getMenuContainer(); 175 | 176 | return domQuery('.djs-popup [data-id="' + id + '"]', container); 177 | } 178 | 179 | function getMenuContainer() { 180 | const popup = getBpmnJS().get('popupMenu'); 181 | return popup._current.container; 182 | } 183 | 184 | function triggerAction(id) { 185 | const entry = queryEntry(id); 186 | 187 | if (!entry) { 188 | throw new Error('entry "' + id + '" not found in append menu'); 189 | } 190 | 191 | const popupMenu = getBpmnJS().get('popupMenu'); 192 | 193 | return popupMenu.trigger(globalEvent(entry, { x: 0, y: 0 })); 194 | } 195 | 196 | function getEntries() { 197 | const popupMenu = getBpmnJS().get('popupMenu'); 198 | return popupMenu._current.entries; 199 | } 200 | 201 | function expectElementWithTemplate(elementRegistry, type, template, result = true) { 202 | const element = elementRegistry.find((element) => is(element, type)); 203 | 204 | if (!result) { 205 | expect(element).to.not.exist; 206 | } else { 207 | expect(element).to.exist; 208 | expect(isTemplateApplied(element, template)).to.be.true; 209 | } 210 | } 211 | 212 | function expectSelected(selection, type, result = true) { 213 | const selected = selection.get(); 214 | 215 | if (!result) { 216 | expect(selected).to.have.length(0); 217 | } else { 218 | expect(selected).to.have.length(1); 219 | expect(is(selected[0], type)).to.be.true; 220 | } 221 | } 222 | 223 | function triggerEntry(id) { 224 | 225 | return getBpmnJS().invoke(function(canvas, dragging) { 226 | 227 | var rootElement = canvas.getRootElement(), 228 | rootGfx = canvas.getGraphics(rootElement); 229 | 230 | openPopup(rootElement); 231 | triggerAction(id); 232 | 233 | dragging.hover({ element: rootElement, gfx: rootGfx }); 234 | dragging.move(createCanvasEvent({ x: 200, y: 300 })); 235 | 236 | // when 237 | dragging.end(); 238 | 239 | }); 240 | } -------------------------------------------------------------------------------- /test/spec/element-templates/append-menu/ElementTemplatesAppendProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | getBpmnJS, 4 | bootstrapModeler, 5 | createEvent 6 | } from 'test/TestHelper'; 7 | 8 | import { 9 | query as domQuery 10 | } from 'min-dom'; 11 | 12 | import { 13 | isTemplateApplied 14 | } from 'lib/element-templates/replace-menu/ElementTemplatesReplaceProvider'; 15 | 16 | import diagramXML from './ElementTemplatesAppendProvider.bpmn'; 17 | import templates from 'test/fixtures/element-templates.json'; 18 | 19 | import { 20 | BpmnPropertiesPanelModule, 21 | BpmnPropertiesProviderModule, 22 | ZeebePropertiesProviderModule 23 | } from 'bpmn-js-properties-panel'; 24 | 25 | import { 26 | CloudElementTemplatesPropertiesProviderModule as ElementTemplatesProviderModule 27 | } from 'bpmn-js-element-templates'; 28 | 29 | import { CreateAppendElementTemplatesModule } from 'lib/'; 30 | 31 | import ZeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; 32 | 33 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 34 | 35 | 36 | describe('', function() { 37 | 38 | beforeEach(bootstrapModeler(diagramXML, { 39 | additionalModules: [ 40 | BpmnPropertiesPanelModule, 41 | BpmnPropertiesProviderModule, 42 | ZeebePropertiesProviderModule, 43 | ElementTemplatesProviderModule, 44 | CreateAppendElementTemplatesModule 45 | ], 46 | moddleExtensions: { 47 | zeebe: ZeebeModdle 48 | } 49 | })); 50 | 51 | beforeEach(inject(function(elementTemplates) { 52 | elementTemplates.set(templates); 53 | })); 54 | 55 | 56 | describe('display', function() { 57 | 58 | it('should display template options', inject(function(elementRegistry) { 59 | 60 | // given 61 | const task = elementRegistry.get('Task_1'); 62 | 63 | // when 64 | openPopup(task); 65 | 66 | // then 67 | const entries = Object.keys(getEntries()); 68 | const templateEntries = entries.filter((entry) => entry.startsWith('append.template-')); 69 | 70 | expect(templateEntries.length).to.be.greaterThan(0); 71 | })); 72 | 73 | 74 | it('should not display template for Start Event', inject(function(elementRegistry) { 75 | 76 | // given 77 | const task = elementRegistry.get('Task_1'); 78 | 79 | // when 80 | openPopup(task); 81 | 82 | // then 83 | const entries = Object.keys(getEntries()); 84 | const startEventTemplateEntry = entries.includes('append.template-example.StartEventTemplate'); 85 | 86 | expect(startEventTemplateEntry).to.not.be.true; 87 | })); 88 | 89 | 90 | it('should not display template for Participant', inject(function(elementRegistry) { 91 | 92 | // given 93 | const task = elementRegistry.get('Task_1'); 94 | 95 | // when 96 | openPopup(task); 97 | 98 | // then 99 | const entries = Object.keys(getEntries()); 100 | const participantTemplateEntry = entries.includes('append.template-example.ParticipantTemplate'); 101 | 102 | expect(participantTemplateEntry).to.not.be.true; 103 | })); 104 | 105 | 106 | it('should not display template for Sequence Flow', inject(function(elementRegistry) { 107 | 108 | // given 109 | const task = elementRegistry.get('Task_1'); 110 | 111 | // when 112 | openPopup(task); 113 | 114 | // then 115 | const entries = Object.keys(getEntries()); 116 | const sequenceFlowTemplateEntry = entries.includes('append.template-example.SequenceFlowTemplate'); 117 | 118 | expect(sequenceFlowTemplateEntry).to.not.be.true; 119 | })); 120 | 121 | }); 122 | 123 | 124 | describe('append', function() { 125 | 126 | it('should append template', inject(function(elementRegistry) { 127 | 128 | // given 129 | const task = elementRegistry.get('Task_1'); 130 | const template = templates[0]; 131 | 132 | openPopup(task); 133 | 134 | // when 135 | triggerAction(`append.template-${template.id}`); 136 | 137 | // then 138 | const outgoingFlows = getBusinessObject(task).outgoing; 139 | const newElement = outgoingFlows[0].targetRef; 140 | 141 | expect(outgoingFlows).to.have.length(1); 142 | expect(isTemplateApplied(newElement, template)).to.be.true; 143 | })); 144 | 145 | 146 | it('should append template via dragstart', inject(function(elementRegistry) { 147 | 148 | // given 149 | const task = elementRegistry.get('Task_1'); 150 | const template = templates[0]; 151 | 152 | openPopup(task); 153 | 154 | // when 155 | placeDragElement(task, `append.template-${template.id}`); 156 | 157 | // then 158 | const outgoingFlows = getBusinessObject(task).outgoing; 159 | const newElement = outgoingFlows[0].targetRef; 160 | 161 | expect(outgoingFlows).to.have.length(1); 162 | expect(isTemplateApplied(newElement, template)).to.be.true; 163 | })); 164 | 165 | 166 | it('should undo', inject(function(elementRegistry, commandStack,) { 167 | 168 | // given 169 | const task = elementRegistry.get('Task_1'); 170 | const template = templates[0]; 171 | 172 | openPopup(task); 173 | 174 | // when 175 | triggerAction(`append.template-${template.id}`); 176 | 177 | // when 178 | commandStack.undo(); 179 | 180 | // then 181 | const outgoingFlows = getBusinessObject(task).outgoing; 182 | 183 | expect(outgoingFlows).to.have.length(0); 184 | })); 185 | 186 | 187 | it('should redo', inject(function(elementRegistry, commandStack) { 188 | 189 | // given 190 | const task = elementRegistry.get('Task_1'); 191 | const template = templates[0]; 192 | 193 | openPopup(task); 194 | 195 | // when 196 | triggerAction(`append.template-${template.id}`); 197 | 198 | // when 199 | commandStack.undo(); 200 | commandStack.redo(); 201 | 202 | // then 203 | const outgoingFlows = getBusinessObject(task).outgoing; 204 | const newElement = outgoingFlows[0].targetRef; 205 | 206 | expect(outgoingFlows).to.have.length(1); 207 | expect(isTemplateApplied(newElement, template)).to.be.true; 208 | })); 209 | 210 | 211 | describe('should trigger create mode', function() { 212 | 213 | it('boundary event', inject(function(elementRegistry, eventBus) { 214 | 215 | // given 216 | const task = elementRegistry.get('Task_1'); 217 | 218 | const spy = sinon.spy(); 219 | 220 | eventBus.on('create.init', spy); 221 | 222 | // when 223 | openPopup(task); 224 | 225 | triggerAction('append.template-example.MessageBoundaryEventTemplate'); 226 | 227 | // then 228 | expect(spy).to.have.been.called; 229 | })); 230 | 231 | 232 | it('link intermediate catch event', inject(function(elementRegistry, eventBus) { 233 | 234 | // given 235 | const task = elementRegistry.get('Task_1'); 236 | 237 | const spy = sinon.spy(); 238 | 239 | eventBus.on('create.init', spy); 240 | 241 | // when 242 | openPopup(task); 243 | 244 | triggerAction('append.template-example.LinkIntermediateCatchEventTemplate'); 245 | 246 | // then 247 | expect(spy).to.have.been.called; 248 | })); 249 | 250 | }); 251 | 252 | }); 253 | 254 | 255 | describe('search', function() { 256 | 257 | it('should be searchable by keywords', inject(function(elementRegistry) { 258 | 259 | // given 260 | const task = elementRegistry.get('Task_1'); 261 | 262 | openPopup(task); 263 | 264 | // when 265 | const entries = getEntries(); 266 | const entry = entries['append.template-example.KeywordsTemplate']; 267 | 268 | // then 269 | expect(entry?.search).to.be.eql([ 'first keyword', 'another keyword' ]); 270 | })); 271 | 272 | }); 273 | 274 | }); 275 | 276 | 277 | // helpers //////////// 278 | 279 | function openPopup(element, offset) { 280 | offset = offset || 100; 281 | 282 | getBpmnJS().invoke(function(popupMenu) { 283 | popupMenu.open(element, 'bpmn-append', { 284 | x: element.x, y: element.y 285 | }); 286 | 287 | }); 288 | } 289 | 290 | function queryEntry(id) { 291 | var container = getMenuContainer(); 292 | 293 | return domQuery('.djs-popup [data-id="' + id + '"]', container); 294 | } 295 | 296 | function getMenuContainer() { 297 | const popup = getBpmnJS().get('popupMenu'); 298 | return popup._current.container; 299 | } 300 | 301 | function triggerAction(id, action = 'click') { 302 | const entry = queryEntry(id); 303 | 304 | if (!entry) { 305 | throw new Error('entry "' + id + '" not found in append menu'); 306 | } 307 | 308 | const popupMenu = getBpmnJS().get('popupMenu'); 309 | return popupMenu.trigger(createEvent(entry, { x: 400, y: 400 }), null, action); 310 | } 311 | 312 | function getEntries() { 313 | const popupMenu = getBpmnJS().get('popupMenu'); 314 | return popupMenu._current.entries; 315 | } 316 | 317 | function placeDragElement(element, action) { 318 | var dragging = getBpmnJS().get('dragging'); 319 | var elementRegistry = getBpmnJS().get('elementRegistry'); 320 | 321 | let processElement = elementRegistry.get('Process_1uc9zgy'); 322 | 323 | triggerAction(action, 'dragstart'); 324 | 325 | dragging.hover({ element: processElement }); 326 | dragging.end(); 327 | } 328 | -------------------------------------------------------------------------------- /test/spec/element-templates/replace-menu/ElementTemplatesReplaceProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | getBpmnJS, 4 | bootstrapModeler 5 | } from 'test/TestHelper'; 6 | 7 | import { 8 | query as domQuery 9 | } from 'min-dom'; 10 | 11 | import { 12 | isTemplateApplied 13 | } from 'lib/element-templates/replace-menu/ElementTemplatesReplaceProvider'; 14 | 15 | import diagramXML from './ElementTemplatesReplaceProvider.bpmn'; 16 | import templates from './ElementTemplatesReplaceProvider.element-templates.json'; 17 | 18 | import { isString } from 'min-dash'; 19 | 20 | import { 21 | BpmnPropertiesPanelModule, 22 | BpmnPropertiesProviderModule, 23 | ZeebePropertiesProviderModule 24 | } from 'bpmn-js-properties-panel'; 25 | 26 | import { 27 | CloudElementTemplatesPropertiesProviderModule as ElementTemplatesProviderModule 28 | } from 'bpmn-js-element-templates'; 29 | 30 | import { CreateAppendElementTemplatesModule } from 'lib/'; 31 | 32 | import ZeebeModdle from 'zeebe-bpmn-moddle/resources/zeebe'; 33 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 34 | 35 | 36 | describe('', function() { 37 | 38 | beforeEach(bootstrapModeler(diagramXML, { 39 | additionalModules: [ 40 | BpmnPropertiesPanelModule, 41 | BpmnPropertiesProviderModule, 42 | ZeebePropertiesProviderModule, 43 | ElementTemplatesProviderModule, 44 | CreateAppendElementTemplatesModule 45 | ], 46 | moddleExtensions: { 47 | zeebe: ZeebeModdle 48 | } 49 | })); 50 | 51 | beforeEach(inject(function(elementTemplates) { 52 | elementTemplates.set(templates); 53 | })); 54 | 55 | 56 | describe('display', function() { 57 | 58 | it('should display matching element templates', inject(function(elementRegistry) { 59 | 60 | // given 61 | const task = elementRegistry.get('ServiceTask_1'); 62 | 63 | // when 64 | openPopup(task); 65 | 66 | // then 67 | const entries = getTemplateEntries(); 68 | expect(entries).to.have.length(7); 69 | })); 70 | 71 | 72 | it('should not display element templates that do not apply', inject(function(elementRegistry) { 73 | 74 | // given 75 | const task = elementRegistry.get('Task_1'); 76 | 77 | // when 78 | openPopup(task); 79 | 80 | // then 81 | const entries = getTemplateEntries(); 82 | expect(entries).to.have.length(6); 83 | })); 84 | 85 | 86 | it('should not display applied element template', inject(function(elementRegistry, elementTemplates) { 87 | 88 | // given 89 | const task = applyTemplate( 90 | 'ServiceTask_1', 91 | 'io.camunda.connectors.HttpJson.v1.noAuth' 92 | ); 93 | 94 | // when 95 | openPopup(task); 96 | 97 | // then 98 | const entries = getTemplateEntries(); 99 | expect(entries).to.have.length(6); 100 | })); 101 | 102 | 103 | describe('should handle non-existing template', function() { 104 | 105 | it('bpmn:Group', inject(function(elementRegistry, selection) { 106 | 107 | // given 108 | const group = elementRegistry.get('GROUP'); 109 | 110 | // when 111 | selection.select(group); 112 | 113 | // then 114 | // no error 115 | })); 116 | 117 | }); 118 | 119 | }); 120 | 121 | 122 | describe('options', function() { 123 | 124 | beforeEach(inject(function(elementRegistry) { 125 | 126 | // given 127 | const task = elementRegistry.get('ServiceTask_1'); 128 | 129 | // when 130 | openPopup(task); 131 | })); 132 | 133 | 134 | it('should have title', inject(function() { 135 | 136 | // given 137 | const template = templates[0]; 138 | const entry = getEntry(`replace.template-${template.id}`); 139 | 140 | // then 141 | expect(entry.label).to.eql(template.name); 142 | })); 143 | 144 | 145 | it('should have icon', inject(function() { 146 | 147 | // given 148 | const template = templates[0]; 149 | const entry = getEntry(`replace.template-${template.id}`); 150 | 151 | // then 152 | expect(entry.imageUrl).to.eql(template.icon.contents); 153 | })); 154 | 155 | 156 | it('should have description', inject(function() { 157 | 158 | // given 159 | const template = templates[0]; 160 | const entry = getEntry(`replace.template-${template.id}`); 161 | 162 | // then 163 | expect(entry.description).to.eql(template.description); 164 | })); 165 | 166 | 167 | it('should have documentation link', inject(function() { 168 | 169 | // given 170 | const template = templates[0]; 171 | const entry = getEntry(`replace.template-${template.id}`); 172 | 173 | // then 174 | expect(entry.documentationRef).to.eql(template.documentationRef); 175 | })); 176 | 177 | 178 | it('should have group - default', inject(function() { 179 | 180 | // given 181 | const templateEntryId = getTemplateEntries()[0]; 182 | const entry = getEntry(templateEntryId); 183 | 184 | // then 185 | expect(entry.group.id).to.eql('templates'); 186 | expect(entry.group.name).to.eql('Templates'); 187 | })); 188 | 189 | 190 | it('should have group - category', inject(function(popupMenu) { 191 | 192 | // given 193 | const templateEntryId = getTemplateEntries()[1]; 194 | const entry = getEntry(templateEntryId); 195 | 196 | // then 197 | expect(entry.group.id).to.eql('connectors'); 198 | expect(entry.group.name).to.eql('Connectors'); 199 | })); 200 | 201 | }); 202 | 203 | 204 | describe('replace', function() { 205 | 206 | it('should apply template', inject(function(elementRegistry) { 207 | 208 | // given 209 | const task = elementRegistry.get('ServiceTask_1'); 210 | const template = templates[0]; 211 | 212 | // when 213 | openPopup(task); 214 | 215 | triggerAction(`replace.template-${template.id}`); 216 | 217 | // then 218 | expect(isTemplateApplied(task, template)).to.be.true; 219 | 220 | })); 221 | 222 | 223 | it('should remove template', inject(function(elementRegistry) { 224 | 225 | // given 226 | let task = elementRegistry.get('ServiceTask_1_template'); 227 | 228 | // then 229 | expect(hasExtensionElements(task)).to.be.true; 230 | 231 | // when 232 | openPopup(task); 233 | triggerAction('replace-remove-element-template'); 234 | 235 | // then 236 | task = elementRegistry.get('ServiceTask_1_template'); 237 | expect(isTemplateApplied(task, templates[0])).to.be.false; 238 | expect(hasExtensionElements(task)).to.be.false; 239 | })); 240 | 241 | 242 | it('should undo', inject(function(elementRegistry, commandStack) { 243 | 244 | // given 245 | const task = elementRegistry.get('ServiceTask_1'); 246 | const template = templates[0]; 247 | 248 | openPopup(task); 249 | triggerAction(`replace.template-${template.id}`); 250 | 251 | // when 252 | commandStack.undo(); 253 | 254 | // then 255 | expect(isTemplateApplied(task, template)).to.be.false; 256 | })); 257 | 258 | 259 | it('should redo', inject(function(elementRegistry, commandStack) { 260 | 261 | // given 262 | const task = elementRegistry.get('ServiceTask_1'); 263 | const template = templates[0]; 264 | 265 | openPopup(task); 266 | triggerAction(`replace.template-${template.id}`); 267 | 268 | // when 269 | commandStack.undo(); 270 | commandStack.redo(); 271 | 272 | // then 273 | expect(isTemplateApplied(task, template)).to.be.true; 274 | })); 275 | 276 | }); 277 | 278 | 279 | describe('search', function() { 280 | 281 | it('should be searchable by keywords', inject(function(elementRegistry) { 282 | 283 | // given 284 | const task = elementRegistry.get('Task_1'); 285 | 286 | // when 287 | openPopup(task); 288 | 289 | const entries = getEntries(); 290 | const entry = entries['replace.template-example.KeywordsTemplate']; 291 | 292 | // then 293 | expect(entry?.search).to.be.eql([ 'first keyword', 'another keyword' ]); 294 | })); 295 | 296 | }); 297 | 298 | }); 299 | 300 | 301 | // helpers //////////// 302 | 303 | function openPopup(element, offset) { 304 | offset = offset || 100; 305 | 306 | getBpmnJS().invoke(function(popupMenu) { 307 | popupMenu.open(element, 'bpmn-replace', { 308 | x: element.x, y: element.y 309 | }); 310 | 311 | }); 312 | } 313 | 314 | function queryEntry(id) { 315 | var container = getMenuContainer(); 316 | 317 | return domQuery('.djs-popup [data-id="' + id + '"]', container); 318 | } 319 | 320 | function getMenuContainer() { 321 | const popup = getBpmnJS().get('popupMenu'); 322 | return popup._current.container; 323 | } 324 | 325 | function triggerAction(id) { 326 | const entry = queryEntry(id); 327 | 328 | if (!entry) { 329 | throw new Error('entry "' + id + '" not found in replace menu'); 330 | } 331 | 332 | const popupMenu = getBpmnJS().get('popupMenu'); 333 | const eventBus = getBpmnJS().get('eventBus'); 334 | 335 | return popupMenu.trigger( 336 | eventBus.createEvent({ 337 | target: entry, 338 | x: 0, 339 | y: 0, 340 | }) 341 | ); 342 | } 343 | 344 | function getEntries() { 345 | const popupMenu = getBpmnJS().get('popupMenu'); 346 | return popupMenu._current.entries; 347 | } 348 | 349 | function getEntry(id) { 350 | const popupMenu = getBpmnJS().get('popupMenu'); 351 | 352 | return popupMenu._getEntry(id); 353 | } 354 | 355 | function getTemplateEntries() { 356 | const entries = getEntries(); 357 | const entryIds = Object.keys(entries); 358 | 359 | return entryIds.filter(entry => entry.startsWith('replace.template')); 360 | } 361 | 362 | 363 | function applyTemplate(element, template) { 364 | 365 | return getBpmnJS().invoke(function(elementTemplates, elementRegistry) { 366 | 367 | if (isString(element)) { 368 | element = elementRegistry.get(element); 369 | } 370 | 371 | if (isString(template)) { 372 | template = templates.find(t => t.id === template); 373 | } 374 | 375 | expect(element).to.exist; 376 | expect(template).to.exist; 377 | 378 | return elementTemplates.applyTemplate(element, template); 379 | }); 380 | } 381 | 382 | function hasExtensionElements(element) { 383 | const businessObject = getBusinessObject(element); 384 | const extensionElements = businessObject.get('extensionElements'); 385 | 386 | if (!extensionElements) { 387 | return false; 388 | } else { 389 | return true; 390 | } 391 | } -------------------------------------------------------------------------------- /test/spec/create-append-anything/create-menu/CreateMenuProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapModeler, 3 | getBpmnJS, 4 | inject, 5 | createCanvasEvent, 6 | createEvent as globalEvent 7 | } from 'test/TestHelper'; 8 | 9 | import { 10 | query as domQuery 11 | } from 'min-dom'; 12 | 13 | import { is } from 'bpmn-js/lib/util/ModelUtil'; 14 | 15 | import CreateMenuModule from 'lib/create-append-anything/create-menu'; 16 | 17 | 18 | describe('features/popup-menu - create menu provider', function() { 19 | 20 | const diagramXML = require('./CreateMenuProvider.bpmn').default; 21 | 22 | beforeEach( 23 | bootstrapModeler(diagramXML, { 24 | additionalModules: [ CreateMenuModule ] 25 | }) 26 | ); 27 | 28 | describe('create', function() { 29 | 30 | describe('task', function() { 31 | 32 | it('should create', inject(function(canvas, dragging, selection, elementRegistry) { 33 | 34 | // when 35 | 36 | triggerEntry('create-task', canvas, dragging); 37 | 38 | // then 39 | expectElement(elementRegistry, 'bpmn:Task'); 40 | expectSelected(selection, 'bpmn:Task'); 41 | })); 42 | 43 | 44 | it('should undo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 45 | 46 | // when 47 | triggerEntry('create-task', canvas, dragging); 48 | 49 | // then 50 | expectElement(elementRegistry, 'bpmn:Task'); 51 | expectSelected(selection, 'bpmn:Task'); 52 | 53 | // when 54 | commandStack.undo(); 55 | 56 | // then 57 | 58 | expectElement(elementRegistry, 'bpmn:Task', false); 59 | expectSelected(selection, 'bpmn:Task', false); 60 | })); 61 | 62 | 63 | it('should redo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 64 | 65 | // when 66 | triggerEntry('create-task', canvas, dragging); 67 | 68 | commandStack.undo(); 69 | 70 | // then 71 | expectElement(elementRegistry, 'bpmn:Task', false); 72 | expectSelected(selection, 'bpmn:Task', false); 73 | 74 | // when; 75 | commandStack.redo(); 76 | 77 | // then 78 | expectElement(elementRegistry, 'bpmn:Task'); 79 | })); 80 | 81 | }); 82 | 83 | 84 | describe('sub process', function() { 85 | 86 | it('should create', inject(function(canvas, dragging, selection, elementRegistry) { 87 | 88 | // when 89 | triggerEntry('create-expanded-subprocess', canvas, dragging); 90 | 91 | // then 92 | expectElement(elementRegistry, 'bpmn:SubProcess'); 93 | expectSelected(selection, 'bpmn:SubProcess'); 94 | })); 95 | 96 | 97 | it('should undo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 98 | 99 | // when 100 | triggerEntry('create-expanded-subprocess', canvas, dragging); 101 | 102 | // then 103 | expectElement(elementRegistry, 'bpmn:SubProcess'); 104 | expectSelected(selection, 'bpmn:SubProcess'); 105 | 106 | // when 107 | commandStack.undo(); 108 | 109 | // then 110 | expectElement(elementRegistry, 'bpmn:SubProcess', false); 111 | expectSelected(selection, 'bpmn:SubProcess', false); 112 | })); 113 | 114 | 115 | it('should redo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 116 | 117 | // when 118 | triggerEntry('create-expanded-subprocess', canvas, dragging); 119 | 120 | commandStack.undo(); 121 | 122 | // then 123 | expectElement(elementRegistry, 'bpmn:SubProcess', false); 124 | expectSelected(selection, 'bpmn:SubProcess', false); 125 | 126 | // when; 127 | commandStack.redo(); 128 | 129 | // then 130 | expectElement(elementRegistry, 'bpmn:SubProcess'); 131 | })); 132 | 133 | }); 134 | 135 | 136 | describe('event', function() { 137 | 138 | it('should create', inject(function(canvas, dragging, selection, elementRegistry) { 139 | 140 | // when 141 | triggerEntry('create-none-start-event', canvas, dragging); 142 | 143 | // then 144 | expectElement(elementRegistry, 'bpmn:StartEvent'); 145 | expectSelected(selection, 'bpmn:StartEvent'); 146 | })); 147 | 148 | 149 | it('should undo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 150 | 151 | // when 152 | triggerEntry('create-none-start-event', canvas, dragging); 153 | 154 | // then 155 | expectElement(elementRegistry, 'bpmn:StartEvent'); 156 | expectSelected(selection, 'bpmn:StartEvent'); 157 | 158 | // when 159 | commandStack.undo(); 160 | 161 | // then 162 | expectElement(elementRegistry, 'bpmn:StartEvent', false); 163 | expectSelected(selection, 'bpmn:StartEvent', false); 164 | })); 165 | 166 | 167 | it('should redo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 168 | 169 | // when 170 | triggerEntry('create-none-start-event', canvas, dragging); 171 | 172 | commandStack.undo(); 173 | 174 | // then 175 | expectElement(elementRegistry, 'bpmn:StartEvent', false); 176 | expectSelected(selection, 'bpmn:StartEvent', false); 177 | 178 | // when; 179 | commandStack.redo(); 180 | 181 | // then 182 | expectElement(elementRegistry, 'bpmn:StartEvent'); 183 | })); 184 | 185 | }); 186 | 187 | 188 | describe('gateway', function() { 189 | 190 | it('should create', inject(function(canvas, dragging, selection, elementRegistry) { 191 | 192 | // when 193 | triggerEntry('create-exclusive-gateway', canvas, dragging); 194 | 195 | // then 196 | expectElement(elementRegistry, 'bpmn:ExclusiveGateway'); 197 | expectSelected(selection, 'bpmn:ExclusiveGateway'); 198 | })); 199 | 200 | 201 | it('should undo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 202 | 203 | // when 204 | triggerEntry('create-exclusive-gateway', canvas, dragging); 205 | 206 | // then 207 | expectElement(elementRegistry, 'bpmn:ExclusiveGateway'); 208 | expectSelected(selection, 'bpmn:ExclusiveGateway'); 209 | 210 | // when 211 | commandStack.undo(); 212 | 213 | // then 214 | expectElement(elementRegistry, 'bpmn:ExclusiveGateway', false); 215 | expectSelected(selection, 'bpmn:ExclusiveGateway', false); 216 | })); 217 | 218 | 219 | it('should redo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 220 | 221 | // when 222 | triggerEntry('create-exclusive-gateway', canvas, dragging); 223 | 224 | commandStack.undo(); 225 | 226 | // then 227 | expectElement(elementRegistry, 'bpmn:ExclusiveGateway', false); 228 | expectSelected(selection, 'bpmn:ExclusiveGateway', false); 229 | 230 | // when; 231 | commandStack.redo(); 232 | 233 | // then 234 | expectElement(elementRegistry, 'bpmn:ExclusiveGateway'); 235 | })); 236 | 237 | }); 238 | 239 | 240 | describe('data reference', function() { 241 | 242 | it('should create', inject(function(canvas, dragging, selection, elementRegistry) { 243 | 244 | // when 245 | triggerEntry('create-data-store-reference', canvas, dragging); 246 | 247 | // then 248 | expectElement(elementRegistry, 'bpmn:DataStoreReference'); 249 | expectSelected(selection, 'bpmn:DataStoreReference'); 250 | })); 251 | 252 | 253 | it('should undo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 254 | 255 | // when 256 | triggerEntry('create-data-store-reference', canvas, dragging); 257 | 258 | // then 259 | expectElement(elementRegistry, 'bpmn:DataStoreReference'); 260 | expectSelected(selection, 'bpmn:DataStoreReference'); 261 | 262 | // when 263 | commandStack.undo(); 264 | 265 | // then 266 | expectElement(elementRegistry, 'bpmn:DataStoreReference', false); 267 | expectSelected(selection, 'bpmn:DataStoreReference', false); 268 | })); 269 | 270 | 271 | it('should redo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 272 | 273 | // when 274 | triggerEntry('create-data-store-reference', canvas, dragging); 275 | 276 | commandStack.undo(); 277 | 278 | // then 279 | expectElement(elementRegistry, 'bpmn:DataStoreReference', false); 280 | expectSelected(selection, 'bpmn:DataStoreReference', false); 281 | 282 | // when; 283 | commandStack.redo(); 284 | 285 | // then 286 | expectElement(elementRegistry, 'bpmn:DataStoreReference'); 287 | })); 288 | 289 | }); 290 | 291 | 292 | describe('participant', function() { 293 | 294 | it('should create', inject(function(canvas, dragging, selection, elementRegistry) { 295 | 296 | // when 297 | triggerEntry('create-expanded-pool', canvas, dragging); 298 | 299 | // then 300 | expectElement(elementRegistry, 'bpmn:Participant'); 301 | expectSelected(selection, 'bpmn:Participant'); 302 | })); 303 | 304 | 305 | it('should undo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 306 | 307 | // when 308 | triggerEntry('create-expanded-pool', canvas, dragging); 309 | 310 | // then 311 | expectElement(elementRegistry, 'bpmn:Participant'); 312 | expectSelected(selection, 'bpmn:Participant'); 313 | 314 | // when 315 | commandStack.undo(); 316 | 317 | // then 318 | expectElement(elementRegistry, 'bpmn:Participant', false); 319 | expectSelected(selection, 'bpmn:Participant', false); 320 | })); 321 | 322 | 323 | it('should redo', inject(function(canvas, dragging, selection, commandStack, elementRegistry) { 324 | 325 | // when 326 | triggerEntry('create-expanded-pool', canvas, dragging); 327 | 328 | commandStack.undo(); 329 | 330 | // then 331 | expectElement(elementRegistry, 'bpmn:Participant', false); 332 | expectSelected(selection, 'bpmn:Participant', false); 333 | 334 | // when; 335 | commandStack.redo(); 336 | 337 | // then 338 | expectElement(elementRegistry, 'bpmn:Participant'); 339 | })); 340 | 341 | }); 342 | 343 | }); 344 | 345 | }); 346 | 347 | 348 | // // helpers 349 | function openPopup(element, offset) { 350 | offset = offset || 100; 351 | 352 | getBpmnJS().invoke(function(popupMenu) { 353 | 354 | popupMenu.open(element, 'bpmn-create', { 355 | x: element.x + offset, y: element.y + offset 356 | }); 357 | 358 | }); 359 | } 360 | 361 | function queryEntry(id) { 362 | const container = getMenuContainer(); 363 | 364 | return domQuery('.djs-popup [data-id="' + id + '"]', container); 365 | } 366 | 367 | function getMenuContainer() { 368 | const popup = getBpmnJS().get('popupMenu'); 369 | return popup._current.container; 370 | } 371 | 372 | function triggerAction(id) { 373 | const entry = queryEntry(id); 374 | 375 | if (!entry) { 376 | throw new Error('entry "' + id + '" not found in append menu'); 377 | } 378 | 379 | const popupMenu = getBpmnJS().get('popupMenu'); 380 | 381 | return popupMenu.trigger(globalEvent(entry, { x: 0, y: 0 })); 382 | } 383 | 384 | function triggerEntry(id, canvas, dragging) { 385 | const rootElement = canvas.getRootElement(), 386 | rootGfx = canvas.getGraphics(rootElement); 387 | 388 | openPopup(rootElement); 389 | triggerAction(id); 390 | 391 | dragging.hover({ element: rootElement, gfx: rootGfx }); 392 | dragging.move(createCanvasEvent({ x: 100, y: 100 })); 393 | 394 | // when 395 | dragging.end(); 396 | } 397 | 398 | function expectElement(elementRegistry, type, result = true) { 399 | const element = elementRegistry.find((element) => is(element, type)); 400 | 401 | if (!result) { 402 | expect(element).to.not.exist; 403 | } else { 404 | expect(element).to.exist; 405 | } 406 | } 407 | 408 | function expectSelected(selection, type, result = true) { 409 | const selected = selection.get(); 410 | 411 | if (!result) { 412 | expect(selected).to.have.length(0); 413 | } else { 414 | expect(selected).to.have.length(1); 415 | expect(is(selected[0], type)).to.be.true; 416 | } 417 | } -------------------------------------------------------------------------------- /test/spec/create-append-anything/append-menu/AppendMenuProvider.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | bootstrapModeler, 3 | getBpmnJS, 4 | inject, 5 | createEvent as globalEvent 6 | } from 'test/TestHelper'; 7 | 8 | 9 | import AppendMenuModule from 'lib/create-append-anything/append-menu'; 10 | import CustomRulesModule from 'bpmn-js/test/util/custom-rules'; 11 | 12 | import { 13 | query as domQuery, 14 | queryAll as domQueryAll 15 | } from 'min-dom'; 16 | 17 | import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; 18 | 19 | 20 | describe('features/create-append-anything - append menu provider', function() { 21 | 22 | const diagramXML = require('./AppendMenuProvider.bpmn').default; 23 | 24 | beforeEach(bootstrapModeler(diagramXML, { 25 | additionalModules: [ 26 | AppendMenuModule, 27 | CustomRulesModule 28 | ] 29 | })); 30 | 31 | 32 | describe('rules', function() { 33 | 34 | it('should get entries by default', inject(function(elementRegistry) { 35 | 36 | // given 37 | const startEvent = elementRegistry.get('StartEvent'); 38 | 39 | // when 40 | openPopup(startEvent); 41 | 42 | // then 43 | expect(queryEntries()).to.have.length.above(0); 44 | })); 45 | 46 | 47 | it('should get entries when custom rule returns true', 48 | inject(function(elementRegistry, customRules) { 49 | 50 | // given 51 | const startEvent = elementRegistry.get('StartEvent'); 52 | 53 | customRules.addRule('shape.append', function() { 54 | return true; 55 | }); 56 | 57 | // when 58 | openPopup(startEvent); 59 | 60 | // then 61 | expect(queryEntries()).to.have.length.above(0); 62 | }) 63 | ); 64 | 65 | 66 | it('should get no entries when custom rule returns false', 67 | inject(function(elementRegistry, customRules) { 68 | 69 | // given 70 | const startEvent = elementRegistry.get('StartEvent'); 71 | 72 | customRules.addRule('shape.append', function() { 73 | return false; 74 | }); 75 | 76 | // when 77 | openPopup(startEvent); 78 | 79 | // then 80 | expect(queryEntries()).to.have.length(0); 81 | }) 82 | ); 83 | 84 | }); 85 | 86 | 87 | describe('menu', function() { 88 | 89 | describe('should not appear as append option', function() { 90 | 91 | it('Start Event', inject(function(elementRegistry) { 92 | 93 | // given 94 | const task = elementRegistry.get('Task'); 95 | 96 | // when 97 | openPopup(task); 98 | 99 | // then 100 | expect(queryEntry('append-none-start')).to.not.exist; 101 | })); 102 | 103 | 104 | it('Participant', inject(function(elementRegistry) { 105 | 106 | // given 107 | const task = elementRegistry.get('Task'); 108 | 109 | // when 110 | openPopup(task); 111 | 112 | // then 113 | expect(queryEntry('append-expanded-pool')).to.not.exist; 114 | })); 115 | 116 | 117 | it('None Boundary Event', inject(function(elementRegistry) { 118 | 119 | // given 120 | const task = elementRegistry.get('Task'); 121 | 122 | // when 123 | openPopup(task); 124 | 125 | // then 126 | expect(queryEntry('append-boundary-event')).to.not.exist; 127 | })); 128 | 129 | }); 130 | 131 | }); 132 | 133 | 134 | describe('append', function() { 135 | 136 | describe('task', function() { 137 | 138 | it('should append', inject(function(elementRegistry) { 139 | 140 | // given 141 | const startEvent = elementRegistry.get('StartEvent'); 142 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 143 | 144 | expect(outgoingFlows).to.have.length(1); 145 | 146 | // when 147 | openPopup(startEvent); 148 | triggerAction('append-task'); 149 | 150 | // then 151 | expect(outgoingFlows).to.have.length(2); 152 | })); 153 | 154 | 155 | it('should append via dragstart', inject(function(elementRegistry, eventBus, dragging) { 156 | 157 | // given 158 | const startEvent = elementRegistry.get('StartEvent'); 159 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 160 | const createSpy = sinon.spy(); 161 | const endSpy = sinon.spy(); 162 | 163 | eventBus.on('create.start', createSpy); 164 | eventBus.on('create.end', endSpy); 165 | 166 | // when 167 | openPopup(startEvent); 168 | placeDragElement(startEvent, 'append-task'); 169 | 170 | // then 171 | expect(createSpy).to.have.been.called; 172 | expect(endSpy).to.have.been.called; 173 | expect(outgoingFlows).to.have.length(2); 174 | })); 175 | 176 | 177 | it('should undo', inject(function(elementRegistry, commandStack) { 178 | 179 | // given 180 | const startEvent = elementRegistry.get('StartEvent'); 181 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 182 | 183 | // when 184 | openPopup(startEvent); 185 | triggerAction('append-task'); 186 | 187 | // then 188 | expect(outgoingFlows).to.have.length(2); 189 | 190 | // when 191 | commandStack.undo(); 192 | 193 | // then 194 | expect(outgoingFlows).to.have.length(1); 195 | })); 196 | 197 | 198 | it('should redo', inject(function(elementRegistry, commandStack) { 199 | 200 | // given 201 | const startEvent = elementRegistry.get('StartEvent'); 202 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 203 | 204 | // when 205 | openPopup(startEvent); 206 | triggerAction('append-task'); 207 | commandStack.undo(); 208 | 209 | // then 210 | expect(outgoingFlows).to.have.length(1); 211 | 212 | // when 213 | commandStack.redo(); 214 | 215 | // then 216 | expect(outgoingFlows).to.have.length(2); 217 | })); 218 | 219 | }); 220 | 221 | 222 | describe('sub process', function() { 223 | 224 | it('should append', inject(function(elementRegistry) { 225 | 226 | // given 227 | const startEvent = elementRegistry.get('StartEvent'); 228 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 229 | 230 | expect(outgoingFlows).to.have.length(1); 231 | 232 | // when 233 | openPopup(startEvent); 234 | triggerAction('append-expanded-subprocess'); 235 | 236 | // then 237 | expect(outgoingFlows).to.have.length(2); 238 | })); 239 | 240 | 241 | it('should append via dragstart', inject(function(elementRegistry, eventBus, dragging) { 242 | 243 | // given 244 | const startEvent = elementRegistry.get('StartEvent'); 245 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 246 | const createSpy = sinon.spy(); 247 | const endSpy = sinon.spy(); 248 | 249 | eventBus.on('create.start', createSpy); 250 | eventBus.on('create.end', endSpy); 251 | 252 | // when 253 | openPopup(startEvent); 254 | placeDragElement(startEvent, 'append-expanded-subprocess'); 255 | 256 | // then 257 | expect(createSpy).to.have.been.called; 258 | expect(endSpy).to.have.been.called; 259 | expect(outgoingFlows).to.have.length(2); 260 | })); 261 | 262 | 263 | it('should undo', inject(function(elementRegistry, commandStack) { 264 | 265 | // given 266 | const startEvent = elementRegistry.get('StartEvent'); 267 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 268 | 269 | // when 270 | openPopup(startEvent); 271 | triggerAction('append-expanded-subprocess'); 272 | 273 | // then 274 | expect(outgoingFlows).to.have.length(2); 275 | 276 | // when 277 | commandStack.undo(); 278 | 279 | // then 280 | expect(outgoingFlows).to.have.length(1); 281 | })); 282 | 283 | 284 | it('should redo', inject(function(elementRegistry, commandStack) { 285 | 286 | // given 287 | const startEvent = elementRegistry.get('StartEvent'); 288 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 289 | 290 | openPopup(startEvent); 291 | triggerAction('append-expanded-subprocess'); 292 | commandStack.undo(); 293 | 294 | // then 295 | expect(outgoingFlows).to.have.length(1); 296 | 297 | // when 298 | commandStack.redo(); 299 | 300 | // then 301 | expect(outgoingFlows).to.have.length(2); 302 | })); 303 | 304 | 305 | describe('should trigger create mode', function() { 306 | 307 | it('event subprocess', inject(function(elementRegistry, eventBus) { 308 | 309 | // given 310 | const task = elementRegistry.get('Task'); 311 | 312 | const spy = sinon.spy(); 313 | 314 | eventBus.on('create.init', spy); 315 | 316 | // when 317 | openPopup(task); 318 | 319 | triggerAction('append-event-subprocess'); 320 | 321 | // then 322 | expect(spy).to.have.been.called; 323 | 324 | })); 325 | 326 | }); 327 | 328 | 329 | }); 330 | 331 | 332 | describe('event', function() { 333 | 334 | it('should append', inject(function(elementRegistry) { 335 | 336 | // given 337 | const task = elementRegistry.get('Task'); 338 | const outgoingFlows = getBusinessObject(task).outgoing; 339 | 340 | expect(outgoingFlows).to.have.length(1); 341 | 342 | // when 343 | openPopup(task); 344 | triggerAction('append-none-end-event'); 345 | 346 | // then 347 | expect(outgoingFlows).to.have.length(2); 348 | })); 349 | 350 | 351 | it('should append via dragstart', inject(function(elementRegistry, eventBus, dragging) { 352 | 353 | // given 354 | const startEvent = elementRegistry.get('StartEvent'); 355 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 356 | const createSpy = sinon.spy(); 357 | const endSpy = sinon.spy(); 358 | 359 | eventBus.on('create.start', createSpy); 360 | eventBus.on('create.end', endSpy); 361 | 362 | // when 363 | openPopup(startEvent); 364 | placeDragElement(startEvent, 'append-none-end-event'); 365 | 366 | // then 367 | expect(createSpy).to.have.been.called; 368 | expect(endSpy).to.have.been.called; 369 | expect(outgoingFlows).to.have.length(2); 370 | })); 371 | 372 | 373 | it('should undo', inject(function(elementRegistry, commandStack) { 374 | 375 | // given 376 | const task = elementRegistry.get('Task'); 377 | const outgoingFlows = getBusinessObject(task).outgoing; 378 | 379 | // when 380 | openPopup(task); 381 | triggerAction('append-none-end-event'); 382 | 383 | // then 384 | expect(outgoingFlows).to.have.length(2); 385 | 386 | // when 387 | commandStack.undo(); 388 | 389 | // then 390 | expect(outgoingFlows).to.have.length(1); 391 | })); 392 | 393 | 394 | it('should redo', inject(function(elementRegistry, commandStack) { 395 | 396 | // given 397 | const task = elementRegistry.get('Task'); 398 | const outgoingFlows = getBusinessObject(task).outgoing; 399 | 400 | openPopup(task); 401 | triggerAction('append-none-end-event'); 402 | commandStack.undo(); 403 | 404 | // then 405 | expect(outgoingFlows).to.have.length(1); 406 | 407 | // when 408 | commandStack.redo(); 409 | 410 | // then 411 | expect(outgoingFlows).to.have.length(2); 412 | })); 413 | 414 | 415 | describe('should trigger create mode', function() { 416 | 417 | it('boundary event', inject(function(elementRegistry, eventBus) { 418 | 419 | // given 420 | const task = elementRegistry.get('Task'); 421 | 422 | const spy = sinon.spy(); 423 | 424 | eventBus.on('create.init', spy); 425 | 426 | // when 427 | openPopup(task); 428 | 429 | triggerAction('append-non-interrupting-message-boundary'); 430 | 431 | // then 432 | expect(spy).to.have.been.called; 433 | })); 434 | 435 | 436 | it('link intermediate catch event', inject(function(elementRegistry, eventBus) { 437 | 438 | // given 439 | const task = elementRegistry.get('Task'); 440 | 441 | const spy = sinon.spy(); 442 | 443 | eventBus.on('create.init', spy); 444 | 445 | // when 446 | openPopup(task); 447 | 448 | triggerAction('append-link-intermediate-catch'); 449 | 450 | // then 451 | expect(spy).to.have.been.called; 452 | })); 453 | 454 | }); 455 | 456 | }); 457 | 458 | 459 | describe('gateway', function() { 460 | 461 | it('should append', inject(function(elementRegistry) { 462 | 463 | // given 464 | const startEvent = elementRegistry.get('StartEvent'); 465 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 466 | 467 | expect(outgoingFlows).to.have.length(1); 468 | 469 | // when 470 | openPopup(startEvent); 471 | triggerAction('append-exclusive-gateway'); 472 | 473 | // then 474 | expect(outgoingFlows).to.have.length(2); 475 | })); 476 | 477 | 478 | it('should append via dragstart', inject(function(elementRegistry, eventBus, dragging) { 479 | 480 | // given 481 | const startEvent = elementRegistry.get('StartEvent'); 482 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 483 | const createSpy = sinon.spy(); 484 | const endSpy = sinon.spy(); 485 | 486 | eventBus.on('create.start', createSpy); 487 | eventBus.on('create.end', endSpy); 488 | 489 | // when 490 | openPopup(startEvent); 491 | placeDragElement(startEvent, 'append-exclusive-gateway'); 492 | 493 | // then 494 | expect(createSpy).to.have.been.called; 495 | expect(endSpy).to.have.been.called; 496 | expect(outgoingFlows).to.have.length(2); 497 | })); 498 | 499 | 500 | it('should undo', inject(function(elementRegistry, commandStack) { 501 | 502 | // given 503 | const startEvent = elementRegistry.get('StartEvent'); 504 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 505 | 506 | // when 507 | openPopup(startEvent); 508 | triggerAction('append-exclusive-gateway'); 509 | 510 | // then 511 | expect(outgoingFlows).to.have.length(2); 512 | 513 | // when 514 | commandStack.undo(); 515 | 516 | // then 517 | expect(outgoingFlows).to.have.length(1); 518 | })); 519 | 520 | 521 | it('should redo', inject(function(elementRegistry, commandStack) { 522 | 523 | // given 524 | const startEvent = elementRegistry.get('StartEvent'); 525 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 526 | 527 | openPopup(startEvent); 528 | triggerAction('append-exclusive-gateway'); 529 | commandStack.undo(); 530 | 531 | // then 532 | expect(outgoingFlows).to.have.length(1); 533 | 534 | // when 535 | commandStack.redo(); 536 | 537 | // then 538 | expect(outgoingFlows).to.have.length(2); 539 | })); 540 | 541 | }); 542 | 543 | 544 | describe('data reference', function() { 545 | 546 | it('should append', inject(function(elementRegistry) { 547 | 548 | // given 549 | const task = elementRegistry.get('Task'); 550 | const dataOutputAssociations = getBusinessObject(task).get('dataOutputAssociations'); 551 | 552 | expect(dataOutputAssociations).to.have.length(0); 553 | 554 | // when 555 | openPopup(task); 556 | triggerAction('append-data-store-reference'); 557 | 558 | // then 559 | expect(dataOutputAssociations).to.have.length(1); 560 | })); 561 | 562 | 563 | it('should append via dragstart', inject(function(elementRegistry, eventBus, dragging) { 564 | 565 | // given 566 | const startEvent = elementRegistry.get('StartEvent'); 567 | const outgoingFlows = getBusinessObject(startEvent).outgoing; 568 | const createSpy = sinon.spy(); 569 | const endSpy = sinon.spy(); 570 | 571 | eventBus.on('create.start', createSpy); 572 | eventBus.on('create.end', endSpy); 573 | 574 | // when 575 | openPopup(startEvent); 576 | placeDragElement(startEvent, 'append-data-store-reference'); 577 | 578 | // then 579 | expect(createSpy).to.have.been.called; 580 | expect(endSpy).to.have.been.called; 581 | expect(outgoingFlows).to.have.length(1); 582 | })); 583 | 584 | 585 | it('should undo', inject(function(elementRegistry, commandStack) { 586 | 587 | // given 588 | const task = elementRegistry.get('Task'); 589 | const dataOutputAssociations = getBusinessObject(task).get('dataOutputAssociations'); 590 | 591 | // when 592 | openPopup(task); 593 | triggerAction('append-data-store-reference'); 594 | 595 | // then 596 | expect(dataOutputAssociations).to.have.length(1); 597 | 598 | // when 599 | commandStack.undo(); 600 | 601 | // then 602 | expect(dataOutputAssociations).to.have.length(0); 603 | })); 604 | 605 | 606 | it('should redo', inject(function(elementRegistry, commandStack) { 607 | 608 | // given 609 | const task = elementRegistry.get('Task'); 610 | const dataOutputAssociations = getBusinessObject(task).get('dataOutputAssociations'); 611 | 612 | openPopup(task); 613 | triggerAction('append-data-store-reference'); 614 | commandStack.undo(); 615 | 616 | // then 617 | expect(dataOutputAssociations).to.have.length(0); 618 | 619 | // when 620 | commandStack.redo(); 621 | 622 | // then 623 | expect(dataOutputAssociations).to.have.length(1); 624 | })); 625 | 626 | }); 627 | 628 | }); 629 | 630 | }); 631 | 632 | 633 | // // helpers 634 | function openPopup(element, offset) { 635 | offset = offset || 100; 636 | 637 | getBpmnJS().invoke(function(popupMenu) { 638 | 639 | popupMenu.open(element, 'bpmn-append', { 640 | x: element.x + offset, y: element.y + offset 641 | }); 642 | 643 | }); 644 | } 645 | 646 | function queryEntry(id) { 647 | const container = getMenuContainer(); 648 | 649 | return domQuery('.djs-popup [data-id="' + id + '"]', container); 650 | } 651 | 652 | function queryEntries() { 653 | const container = getMenuContainer(); 654 | 655 | return domQueryAll('.djs-popup .entry', container); 656 | } 657 | 658 | function getMenuContainer() { 659 | const popup = getBpmnJS().get('popupMenu'); 660 | return popup._current.container; 661 | } 662 | 663 | function triggerAction(id, action = 'click') { 664 | const entry = queryEntry(id); 665 | 666 | if (!entry) { 667 | throw new Error('entry "' + id + '" not found in append menu'); 668 | } 669 | 670 | const popupMenu = getBpmnJS().get('popupMenu'); 671 | return popupMenu.trigger(globalEvent(entry, { x: 0, y: 0 }), null, action); 672 | } 673 | 674 | function placeDragElement(element, action) { 675 | const dragging = getBpmnJS().get('dragging'); 676 | const elementRegistry = getBpmnJS().get('elementRegistry'); 677 | 678 | let processElement = elementRegistry.get('Process_07k5b99'); 679 | 680 | triggerAction(action, 'dragstart'); 681 | 682 | dragging.hover({ element: processElement }); 683 | dragging.end(); 684 | } -------------------------------------------------------------------------------- /lib/util/CreateOptionsUtil.js: -------------------------------------------------------------------------------- 1 | export const EVENT_GROUP = { 2 | id: 'events', 3 | name: 'Events' 4 | }; 5 | 6 | export const TASK_GROUP = { 7 | id: 'tasks', 8 | name: 'Tasks' 9 | }; 10 | 11 | export const DATA_GROUP = { 12 | id: 'data', 13 | name: 'Data' 14 | }; 15 | 16 | export const PARTICIPANT_GROUP = { 17 | id: 'participants', 18 | name: 'Participants' 19 | }; 20 | 21 | export const SUBPROCESS_GROUP = { 22 | id: 'subprocess', 23 | name: 'Sub-processes' 24 | }; 25 | 26 | export const GATEWAY_GROUP = { 27 | id: 'gateways', 28 | name: 'Gateways' 29 | }; 30 | 31 | export const NONE_EVENTS = [ 32 | { 33 | label: 'Start event', 34 | actionName: 'none-start-event', 35 | className: 'bpmn-icon-start-event-none', 36 | target: { 37 | type: 'bpmn:StartEvent' 38 | } 39 | }, 40 | { 41 | label: 'Intermediate throw event', 42 | actionName: 'none-intermediate-throwing', 43 | className: 'bpmn-icon-intermediate-event-none', 44 | target: { 45 | type: 'bpmn:IntermediateThrowEvent' 46 | } 47 | }, 48 | { 49 | label: 'Boundary event', 50 | actionName: 'none-boundary-event', 51 | className: 'bpmn-icon-intermediate-event-none', 52 | target: { 53 | type: 'bpmn:BoundaryEvent' 54 | } 55 | }, 56 | { 57 | label: 'End event', 58 | actionName: 'none-end-event', 59 | className: 'bpmn-icon-end-event-none', 60 | target: { 61 | type: 'bpmn:EndEvent' 62 | } 63 | } 64 | ].map(option => ({ ...option, group: EVENT_GROUP })); 65 | 66 | export const TYPED_START_EVENTS = [ 67 | { 68 | label: 'Message start event', 69 | actionName: 'message-start', 70 | className: 'bpmn-icon-start-event-message', 71 | target: { 72 | type: 'bpmn:StartEvent', 73 | eventDefinitionType: 'bpmn:MessageEventDefinition' 74 | } 75 | }, 76 | { 77 | label: 'Timer start event', 78 | actionName: 'timer-start', 79 | className: 'bpmn-icon-start-event-timer', 80 | target: { 81 | type: 'bpmn:StartEvent', 82 | eventDefinitionType: 'bpmn:TimerEventDefinition' 83 | } 84 | }, 85 | { 86 | label: 'Conditional start event', 87 | actionName: 'conditional-start', 88 | className: 'bpmn-icon-start-event-condition', 89 | target: { 90 | type: 'bpmn:StartEvent', 91 | eventDefinitionType: 'bpmn:ConditionalEventDefinition' 92 | } 93 | }, 94 | { 95 | label: 'Signal start event', 96 | actionName: 'signal-start', 97 | className: 'bpmn-icon-start-event-signal', 98 | target: { 99 | type: 'bpmn:StartEvent', 100 | eventDefinitionType: 'bpmn:SignalEventDefinition' 101 | } 102 | } 103 | ].map(option => ({ ...option, group: EVENT_GROUP })); 104 | 105 | export const TYPED_NON_INTERRUPTING_START_EVENTS = [ 106 | { 107 | label: 'Message start event (non-interrupting)', 108 | actionName: 'replace-with-non-interrupting-message-start', 109 | className: 'bpmn-icon-start-event-non-interrupting-message', 110 | target: { 111 | type: 'bpmn:StartEvent', 112 | eventDefinitionType: 'bpmn:MessageEventDefinition', 113 | isInterrupting: false 114 | } 115 | }, 116 | { 117 | label: 'Timer start event (non-interrupting)', 118 | actionName: 'replace-with-non-interrupting-timer-start', 119 | className: 'bpmn-icon-start-event-non-interrupting-timer', 120 | target: { 121 | type: 'bpmn:StartEvent', 122 | eventDefinitionType: 'bpmn:TimerEventDefinition', 123 | isInterrupting: false 124 | } 125 | }, 126 | { 127 | label: 'Conditional start event (non-interrupting)', 128 | actionName: 'replace-with-non-interrupting-conditional-start', 129 | className: 'bpmn-icon-start-event-non-interrupting-condition', 130 | target: { 131 | type: 'bpmn:StartEvent', 132 | eventDefinitionType: 'bpmn:ConditionalEventDefinition', 133 | isInterrupting: false 134 | } 135 | }, 136 | { 137 | label: 'Signal start event (non-interrupting)', 138 | actionName: 'replace-with-non-interrupting-signal-start', 139 | className: 'bpmn-icon-start-event-non-interrupting-signal', 140 | target: { 141 | type: 'bpmn:StartEvent', 142 | eventDefinitionType: 'bpmn:SignalEventDefinition', 143 | isInterrupting: false 144 | } 145 | }, 146 | { 147 | label: 'Escalation start event (non-interrupting)', 148 | actionName: 'replace-with-non-interrupting-escalation-start', 149 | className: 'bpmn-icon-start-event-non-interrupting-escalation', 150 | target: { 151 | type: 'bpmn:StartEvent', 152 | eventDefinitionType: 'bpmn:EscalationEventDefinition', 153 | isInterrupting: false 154 | } 155 | } 156 | ].map(option => ({ ...option, group: EVENT_GROUP, rank: -1 })); 157 | 158 | export const TYPED_INTERMEDIATE_EVENT = [ 159 | { 160 | label: 'Message intermediate catch event', 161 | actionName: 'message-intermediate-catch', 162 | className: 'bpmn-icon-intermediate-event-catch-message', 163 | target: { 164 | type: 'bpmn:IntermediateCatchEvent', 165 | eventDefinitionType: 'bpmn:MessageEventDefinition' 166 | } 167 | }, 168 | { 169 | label: 'Message intermediate throw event', 170 | actionName: 'message-intermediate-throw', 171 | className: 'bpmn-icon-intermediate-event-throw-message', 172 | target: { 173 | type: 'bpmn:IntermediateThrowEvent', 174 | eventDefinitionType: 'bpmn:MessageEventDefinition' 175 | } 176 | }, 177 | { 178 | label: 'Timer intermediate catch event', 179 | actionName: 'timer-intermediate-catch', 180 | className: 'bpmn-icon-intermediate-event-catch-timer', 181 | target: { 182 | type: 'bpmn:IntermediateCatchEvent', 183 | eventDefinitionType: 'bpmn:TimerEventDefinition' 184 | } 185 | }, 186 | { 187 | label: 'Escalation intermediate throw event', 188 | actionName: 'escalation-intermediate-throw', 189 | className: 'bpmn-icon-intermediate-event-throw-escalation', 190 | target: { 191 | type: 'bpmn:IntermediateThrowEvent', 192 | eventDefinitionType: 'bpmn:EscalationEventDefinition' 193 | } 194 | }, 195 | { 196 | label: 'Conditional intermediate catch event', 197 | actionName: 'conditional-intermediate-catch', 198 | className: 'bpmn-icon-intermediate-event-catch-condition', 199 | target: { 200 | type: 'bpmn:IntermediateCatchEvent', 201 | eventDefinitionType: 'bpmn:ConditionalEventDefinition' 202 | } 203 | }, 204 | { 205 | label: 'Link intermediate catch event', 206 | actionName: 'link-intermediate-catch', 207 | className: 'bpmn-icon-intermediate-event-catch-link', 208 | target: { 209 | type: 'bpmn:IntermediateCatchEvent', 210 | eventDefinitionType: 'bpmn:LinkEventDefinition', 211 | eventDefinitionAttrs: { 212 | name: '' 213 | } 214 | } 215 | }, 216 | { 217 | label: 'Link intermediate throw event', 218 | actionName: 'link-intermediate-throw', 219 | className: 'bpmn-icon-intermediate-event-throw-link', 220 | target: { 221 | type: 'bpmn:IntermediateThrowEvent', 222 | eventDefinitionType: 'bpmn:LinkEventDefinition', 223 | eventDefinitionAttrs: { 224 | name: '' 225 | } 226 | } 227 | }, 228 | { 229 | label: 'Compensation intermediate throw event', 230 | actionName: 'compensation-intermediate-throw', 231 | className: 'bpmn-icon-intermediate-event-throw-compensation', 232 | target: { 233 | type: 'bpmn:IntermediateThrowEvent', 234 | eventDefinitionType: 'bpmn:CompensateEventDefinition' 235 | } 236 | }, 237 | { 238 | label: 'Signal intermediate catch event', 239 | actionName: 'signal-intermediate-catch', 240 | className: 'bpmn-icon-intermediate-event-catch-signal', 241 | target: { 242 | type: 'bpmn:IntermediateCatchEvent', 243 | eventDefinitionType: 'bpmn:SignalEventDefinition' 244 | } 245 | }, 246 | { 247 | label: 'Signal intermediate throw event', 248 | actionName: 'signal-intermediate-throw', 249 | className: 'bpmn-icon-intermediate-event-throw-signal', 250 | target: { 251 | type: 'bpmn:IntermediateThrowEvent', 252 | eventDefinitionType: 'bpmn:SignalEventDefinition' 253 | } 254 | } 255 | ].map(option => ({ ...option, group: EVENT_GROUP })); 256 | 257 | export const TYPED_BOUNDARY_EVENT = [ 258 | { 259 | label: 'Message boundary event', 260 | actionName: 'message-boundary', 261 | className: 'bpmn-icon-intermediate-event-catch-message', 262 | target: { 263 | type: 'bpmn:BoundaryEvent', 264 | eventDefinitionType: 'bpmn:MessageEventDefinition' 265 | } 266 | }, 267 | { 268 | label: 'Timer boundary event', 269 | actionName: 'timer-boundary', 270 | className: 'bpmn-icon-intermediate-event-catch-timer', 271 | target: { 272 | type: 'bpmn:BoundaryEvent', 273 | eventDefinitionType: 'bpmn:TimerEventDefinition' 274 | } 275 | }, 276 | { 277 | label: 'Escalation boundary event', 278 | actionName: 'escalation-boundary', 279 | className: 'bpmn-icon-intermediate-event-catch-escalation', 280 | target: { 281 | type: 'bpmn:BoundaryEvent', 282 | eventDefinitionType: 'bpmn:EscalationEventDefinition' 283 | } 284 | }, 285 | { 286 | label: 'Conditional boundary event', 287 | actionName: 'conditional-boundary', 288 | className: 'bpmn-icon-intermediate-event-catch-condition', 289 | target: { 290 | type: 'bpmn:BoundaryEvent', 291 | eventDefinitionType: 'bpmn:ConditionalEventDefinition' 292 | } 293 | }, 294 | { 295 | label: 'Error boundary event', 296 | actionName: 'error-boundary', 297 | className: 'bpmn-icon-intermediate-event-catch-error', 298 | target: { 299 | type: 'bpmn:BoundaryEvent', 300 | eventDefinitionType: 'bpmn:ErrorEventDefinition' 301 | } 302 | }, 303 | { 304 | label: 'Cancel boundary event', 305 | actionName: 'cancel-boundary', 306 | className: 'bpmn-icon-intermediate-event-catch-cancel', 307 | target: { 308 | type: 'bpmn:BoundaryEvent', 309 | eventDefinitionType: 'bpmn:CancelEventDefinition' 310 | } 311 | }, 312 | { 313 | label: 'Signal boundary event', 314 | actionName: 'signal-boundary', 315 | className: 'bpmn-icon-intermediate-event-catch-signal', 316 | target: { 317 | type: 'bpmn:BoundaryEvent', 318 | eventDefinitionType: 'bpmn:SignalEventDefinition' 319 | } 320 | }, 321 | { 322 | label: 'Compensation boundary event', 323 | actionName: 'compensation-boundary', 324 | className: 'bpmn-icon-intermediate-event-catch-compensation', 325 | target: { 326 | type: 'bpmn:BoundaryEvent', 327 | eventDefinitionType: 'bpmn:CompensateEventDefinition' 328 | } 329 | }, 330 | { 331 | label: 'Message boundary event (non-interrupting)', 332 | actionName: 'non-interrupting-message-boundary', 333 | className: 'bpmn-icon-intermediate-event-catch-non-interrupting-message', 334 | target: { 335 | type: 'bpmn:BoundaryEvent', 336 | eventDefinitionType: 'bpmn:MessageEventDefinition', 337 | cancelActivity: false 338 | } 339 | }, 340 | { 341 | label: 'Timer boundary event (non-interrupting)', 342 | actionName: 'non-interrupting-timer-boundary', 343 | className: 'bpmn-icon-intermediate-event-catch-non-interrupting-timer', 344 | target: { 345 | type: 'bpmn:BoundaryEvent', 346 | eventDefinitionType: 'bpmn:TimerEventDefinition', 347 | cancelActivity: false 348 | } 349 | }, 350 | { 351 | label: 'Escalation boundary event (non-interrupting)', 352 | actionName: 'non-interrupting-escalation-boundary', 353 | className: 'bpmn-icon-intermediate-event-catch-non-interrupting-escalation', 354 | target: { 355 | type: 'bpmn:BoundaryEvent', 356 | eventDefinitionType: 'bpmn:EscalationEventDefinition', 357 | cancelActivity: false 358 | } 359 | }, 360 | { 361 | label: 'Conditional boundary event (non-interrupting)', 362 | actionName: 'non-interrupting-conditional-boundary', 363 | className: 'bpmn-icon-intermediate-event-catch-non-interrupting-condition', 364 | target: { 365 | type: 'bpmn:BoundaryEvent', 366 | eventDefinitionType: 'bpmn:ConditionalEventDefinition', 367 | cancelActivity: false 368 | } 369 | }, 370 | { 371 | label: 'Signal boundary event (non-interrupting)', 372 | actionName: 'non-interrupting-signal-boundary', 373 | className: 'bpmn-icon-intermediate-event-catch-non-interrupting-signal', 374 | target: { 375 | type: 'bpmn:BoundaryEvent', 376 | eventDefinitionType: 'bpmn:SignalEventDefinition', 377 | cancelActivity: false 378 | } 379 | } 380 | ].map(option => ({ ...option, group: EVENT_GROUP })); 381 | 382 | export const TYPED_END_EVENT = [ 383 | { 384 | label: 'Message end event', 385 | actionName: 'message-end', 386 | className: 'bpmn-icon-end-event-message', 387 | target: { 388 | type: 'bpmn:EndEvent', 389 | eventDefinitionType: 'bpmn:MessageEventDefinition' 390 | } 391 | }, 392 | { 393 | label: 'Escalation end event', 394 | actionName: 'escalation-end', 395 | className: 'bpmn-icon-end-event-escalation', 396 | target: { 397 | type: 'bpmn:EndEvent', 398 | eventDefinitionType: 'bpmn:EscalationEventDefinition' 399 | } 400 | }, 401 | { 402 | label: 'Error end event', 403 | actionName: 'error-end', 404 | className: 'bpmn-icon-end-event-error', 405 | target: { 406 | type: 'bpmn:EndEvent', 407 | eventDefinitionType: 'bpmn:ErrorEventDefinition' 408 | } 409 | }, 410 | { 411 | label: 'Cancel end event', 412 | actionName: 'cancel-end', 413 | className: 'bpmn-icon-end-event-cancel', 414 | target: { 415 | type: 'bpmn:EndEvent', 416 | eventDefinitionType: 'bpmn:CancelEventDefinition' 417 | } 418 | }, 419 | { 420 | label: 'Compensation end event', 421 | actionName: 'compensation-end', 422 | className: 'bpmn-icon-end-event-compensation', 423 | target: { 424 | type: 'bpmn:EndEvent', 425 | eventDefinitionType: 'bpmn:CompensateEventDefinition' 426 | } 427 | }, 428 | { 429 | label: 'Signal end event', 430 | actionName: 'signal-end', 431 | className: 'bpmn-icon-end-event-signal', 432 | target: { 433 | type: 'bpmn:EndEvent', 434 | eventDefinitionType: 'bpmn:SignalEventDefinition' 435 | } 436 | }, 437 | { 438 | label: 'Terminate end event', 439 | actionName: 'terminate-end', 440 | className: 'bpmn-icon-end-event-terminate', 441 | target: { 442 | type: 'bpmn:EndEvent', 443 | eventDefinitionType: 'bpmn:TerminateEventDefinition' 444 | } 445 | } 446 | ].map(option => ({ ...option, group: EVENT_GROUP })); 447 | 448 | export const GATEWAY = [ 449 | { 450 | label: 'Exclusive gateway', 451 | actionName: 'exclusive-gateway', 452 | className: 'bpmn-icon-gateway-xor', 453 | target: { 454 | type: 'bpmn:ExclusiveGateway' 455 | } 456 | }, 457 | { 458 | label: 'Parallel gateway', 459 | actionName: 'parallel-gateway', 460 | className: 'bpmn-icon-gateway-parallel', 461 | target: { 462 | type: 'bpmn:ParallelGateway' 463 | } 464 | }, 465 | { 466 | label: 'Inclusive gateway', 467 | search: 'or', 468 | actionName: 'inclusive-gateway', 469 | className: 'bpmn-icon-gateway-or', 470 | target: { 471 | type: 'bpmn:InclusiveGateway' 472 | }, 473 | rank: -1 474 | }, 475 | { 476 | label: 'Complex gateway', 477 | actionName: 'complex-gateway', 478 | className: 'bpmn-icon-gateway-complex', 479 | target: { 480 | type: 'bpmn:ComplexGateway' 481 | }, 482 | rank: -1 483 | }, 484 | { 485 | label: 'Event-based gateway', 486 | actionName: 'event-based-gateway', 487 | className: 'bpmn-icon-gateway-eventbased', 488 | target: { 489 | type: 'bpmn:EventBasedGateway', 490 | instantiate: false, 491 | eventGatewayType: 'Exclusive' 492 | } 493 | } 494 | ].map(option => ({ ...option, group: GATEWAY_GROUP })); 495 | 496 | export const SUBPROCESS = [ 497 | { 498 | label: 'Call activity', 499 | actionName: 'call-activity', 500 | className: 'bpmn-icon-call-activity', 501 | target: { 502 | type: 'bpmn:CallActivity' 503 | } 504 | }, 505 | { 506 | label: 'Transaction', 507 | actionName: 'transaction', 508 | className: 'bpmn-icon-transaction', 509 | target: { 510 | type: 'bpmn:Transaction', 511 | isExpanded: true 512 | } 513 | }, 514 | { 515 | label: 'Event sub-process', 516 | search: 'subprocess', 517 | actionName: 'event-subprocess', 518 | className: 'bpmn-icon-event-subprocess-expanded', 519 | target: { 520 | type: 'bpmn:SubProcess', 521 | triggeredByEvent: true, 522 | isExpanded: true 523 | } 524 | }, 525 | { 526 | label: 'Sub-process (collapsed)', 527 | search: 'subprocess', 528 | actionName: 'collapsed-subprocess', 529 | className: 'bpmn-icon-subprocess-collapsed', 530 | target: { 531 | type: 'bpmn:SubProcess', 532 | isExpanded: false 533 | } 534 | }, 535 | { 536 | label: 'Sub-process (expanded)', 537 | search: 'subprocess', 538 | actionName: 'expanded-subprocess', 539 | className: 'bpmn-icon-subprocess-expanded', 540 | target: { 541 | type: 'bpmn:SubProcess', 542 | isExpanded: true 543 | } 544 | }, 545 | { 546 | label: 'Ad-hoc sub-process (collapsed)', 547 | search: 'adhoc subprocess', 548 | actionName: 'collapsed-ad-hoc-subprocess', 549 | className: 'bpmn-icon-subprocess-collapsed', 550 | target: { 551 | type: 'bpmn:AdHocSubProcess', 552 | isExpanded: false 553 | } 554 | }, 555 | { 556 | label: 'Ad-hoc sub-process (expanded)', 557 | search: 'adhoc subprocess', 558 | actionName: 'expanded-ad-hoc-subprocess', 559 | className: 'bpmn-icon-subprocess-expanded', 560 | target: { 561 | type: 'bpmn:AdHocSubProcess', 562 | isExpanded: true 563 | } 564 | } 565 | ].map(option => ({ ...option, group: SUBPROCESS_GROUP })); 566 | 567 | export const TASK = [ 568 | { 569 | label: 'Task', 570 | actionName: 'task', 571 | className: 'bpmn-icon-task', 572 | target: { 573 | type: 'bpmn:Task' 574 | } 575 | }, 576 | { 577 | label: 'User task', 578 | actionName: 'user-task', 579 | className: 'bpmn-icon-user', 580 | target: { 581 | type: 'bpmn:UserTask' 582 | } 583 | }, 584 | { 585 | label: 'Service task', 586 | actionName: 'service-task', 587 | className: 'bpmn-icon-service', 588 | target: { 589 | type: 'bpmn:ServiceTask' 590 | } 591 | }, 592 | { 593 | label: 'Send task', 594 | actionName: 'send-task', 595 | className: 'bpmn-icon-send', 596 | target: { 597 | type: 'bpmn:SendTask' 598 | }, 599 | rank: -1 600 | }, 601 | { 602 | label: 'Receive task', 603 | actionName: 'receive-task', 604 | className: 'bpmn-icon-receive', 605 | target: { 606 | type: 'bpmn:ReceiveTask' 607 | }, 608 | rank: -1 609 | }, 610 | { 611 | label: 'Manual task', 612 | actionName: 'manual-task', 613 | className: 'bpmn-icon-manual', 614 | target: { 615 | type: 'bpmn:ManualTask' 616 | }, 617 | rank: -1 618 | }, 619 | { 620 | label: 'Business rule task', 621 | actionName: 'rule-task', 622 | className: 'bpmn-icon-business-rule', 623 | target: { 624 | type: 'bpmn:BusinessRuleTask' 625 | } 626 | }, 627 | { 628 | label: 'Script task', 629 | actionName: 'script-task', 630 | className: 'bpmn-icon-script', 631 | target: { 632 | type: 'bpmn:ScriptTask' 633 | } 634 | } 635 | ].map(option => ({ ...option, group: TASK_GROUP })); 636 | 637 | export const DATA_OBJECTS = [ 638 | { 639 | label: 'Data store reference', 640 | actionName: 'data-store-reference', 641 | className: 'bpmn-icon-data-store', 642 | target: { 643 | type: 'bpmn:DataStoreReference' 644 | } 645 | }, 646 | { 647 | label: 'Data object reference', 648 | actionName: 'data-object-reference', 649 | className: 'bpmn-icon-data-object', 650 | target: { 651 | type: 'bpmn:DataObjectReference' 652 | } 653 | } 654 | ].map(option => ({ ...option, group: DATA_GROUP })); 655 | 656 | export const PARTICIPANT = [ 657 | { 658 | label: 'Expanded pool/participant', 659 | search: 'Non-empty pool/participant', 660 | actionName: 'expanded-pool', 661 | className: 'bpmn-icon-participant', 662 | target: { 663 | type: 'bpmn:Participant', 664 | isExpanded: true 665 | } 666 | }, 667 | { 668 | label: 'Empty pool/participant', 669 | search: 'Collapsed pool/participant', 670 | actionName: 'collapsed-pool', 671 | className: 'bpmn-icon-lane', 672 | target: { 673 | type: 'bpmn:Participant', 674 | isExpanded: false 675 | } 676 | } 677 | ].map(option => ({ ...option, group: PARTICIPANT_GROUP })); 678 | 679 | export const CREATE_OPTIONS = [ 680 | ...GATEWAY, 681 | ...TASK, 682 | ...SUBPROCESS, 683 | ...NONE_EVENTS, 684 | ...TYPED_START_EVENTS, 685 | ...TYPED_NON_INTERRUPTING_START_EVENTS, 686 | ...TYPED_INTERMEDIATE_EVENT, 687 | ...TYPED_END_EVENT, 688 | ...TYPED_BOUNDARY_EVENT, 689 | ...DATA_OBJECTS, 690 | ...PARTICIPANT 691 | ]; 692 | --------------------------------------------------------------------------------