diff --git a/lib/camunda-cloud/Modeler.js b/lib/camunda-cloud/Modeler.js index fcb465ae..d096e9bc 100644 --- a/lib/camunda-cloud/Modeler.js +++ b/lib/camunda-cloud/Modeler.js @@ -16,6 +16,7 @@ import replaceModule from './features/replace'; import sharedReplaceModule from '../shared/features/replace'; import colorPickerModule from 'bpmn-js-color-picker'; import createAppendAnythingModule from 'bpmn-js/lib/features/create-append-anything'; +import createAppendElementTemplatesModule from './features/create-append-anything'; import { commonModdleExtensions, commonModules } from './util/commonModules'; import { without } from 'min-dash'; @@ -60,6 +61,7 @@ Modeler.prototype._camundaCloudModules = [ rulesModule, zeebePropertiesProviderModule, cloudElementTemplatesPropertiesProvider, + createAppendElementTemplatesModule, replaceModule, sharedReplaceModule, colorPickerModule diff --git a/lib/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.js b/lib/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.js new file mode 100644 index 00000000..d32da964 --- /dev/null +++ b/lib/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.js @@ -0,0 +1,160 @@ +import { assign } from 'min-dash'; + + +/** + * A popup menu provider that allows to append elements with + * element templates. + */ +export default function ElementTemplatesAppendProvider( + popupMenu, translate, elementTemplates, + autoPlace, create, mouse, rules) { + + this._popupMenu = popupMenu; + this._translate = translate; + this._elementTemplates = elementTemplates; + this._autoPlace = autoPlace; + this._create = create; + this._mouse = mouse; + this._rules = rules; + + this.register(); +} + +ElementTemplatesAppendProvider.$inject = [ + 'popupMenu', + 'translate', + 'elementTemplates', + 'autoPlace', + 'create', + 'move', + 'rules' +]; + +/** + * Register append menu provider in the popup menu + */ +ElementTemplatesAppendProvider.prototype.register = function() { + this._popupMenu.registerProvider('bpmn-append', this); +}; + +/** + * Adds the element templates to the append menu. + * @param {djs.model.Base} element + * + * @returns {Object} + */ +ElementTemplatesAppendProvider.prototype.getPopupMenuEntries = function(element) { + return (entries) => { + + if (!this._rules.allowed('shape.append', { element: element })) { + return []; + } + + const filteredTemplates = this._filterTemplates(this._elementTemplates.getLatest()); + + // add template entries + assign(entries, this.getTemplateEntries(element, filteredTemplates)); + + return entries; + }; +}; + +/** + * Get all element templates. + * + * @param {djs.model.Base} element + * + * @return {Object} element templates as menu entries + */ +ElementTemplatesAppendProvider.prototype.getTemplateEntries = function(element, templates) { + + const templateEntries = {}; + + templates.map(template => { + + const { + icon = {}, + category, + } = template; + + const entryId = `append.template-${template.id}`; + + const defaultGroup = { + id: 'templates', + name: this._translate('Templates') + }; + + templateEntries[entryId] = { + label: template.name, + description: template.description, + documentationRef: template.documentationRef, + imageUrl: icon.contents, + group: category || defaultGroup, + action: this._getEntryAction(element, template) + }; + }); + + return templateEntries; +}; + +/** + * Filter out templates from the options. + * + * @param {Array} templates + * + * @returns {Array} + */ +ElementTemplatesAppendProvider.prototype._filterTemplates = function(templates) { + return templates.filter(template => { + const { + appliesTo, + elementType + } = template; + + const type = (elementType && elementType.value) || appliesTo[0]; + + // elements that can not be appended + if ([ + 'bpmn:StartEvent', + 'bpmn:Participant' + ].includes(type)) { + return false; + } + + // sequence flow templates are supported + // but connections are not appendable + if ('bpmn:SequenceFlow' === type) { + return false; + } + + return true; + }); +}; + +/** + * Create an action for a given template. + * + * @param {djs.model.Base} element + * @param {Object} template + * + * @returns {Object} + */ +ElementTemplatesAppendProvider.prototype._getEntryAction = function(element, template) { + return { + + click: () => { + const newElement = this._elementTemplates.createElement(template); + this._autoPlace.append(element, newElement); + }, + + dragstart: (event) => { + const newElement = this._elementTemplates.createElement(template); + + if (event instanceof KeyboardEvent) { + event = this._mouse.getLastMoveEvent(); + } + + this._create.start(event, newElement); + } + }; +}; diff --git a/lib/camunda-cloud/features/create-append-anything/index.js b/lib/camunda-cloud/features/create-append-anything/index.js new file mode 100644 index 00000000..cc23dd73 --- /dev/null +++ b/lib/camunda-cloud/features/create-append-anything/index.js @@ -0,0 +1,6 @@ +import ElementTemplatesAppendProvider from './ElementTemplatesAppendProvider'; + +export default { + __init__: [ 'elementTemplatesAppendProvider' ], + elementTemplatesAppendProvider: [ 'type', ElementTemplatesAppendProvider ] +}; \ No newline at end of file diff --git a/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.bpmn b/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.bpmn new file mode 100644 index 00000000..e455a5d8 --- /dev/null +++ b/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.bpmn @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.json b/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.json new file mode 100644 index 00000000..8964b564 --- /dev/null +++ b/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.json @@ -0,0 +1,64 @@ +[ + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "Task Template", + "id": "example.TaskTemplate", + "appliesTo": [ + "bpmn:Task" + ], + "properties": [ + { + "type": "Boolean", + "binding": { + "type": "property", + "name": "customProperty" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "Participant Template", + "id": "example.ParticipantTemplate", + "appliesTo": ["bpmn:Participant"], + "properties": [ + { + "binding": { + "type": "property", + "name": "someProp" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "Start Event Template", + "id": "example.StartEventTemplate", + "appliesTo": ["bpmn:StartEvent"], + "properties": [ + { + "binding": { + "type": "property", + "name": "someProp" + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "Sequence Flow Template", + "id": "example.SequenceFlowTemplate", + "appliesTo": [ + "bpmn:SequenceFlow" + ], + "properties": [ + { + "type": "String", + "binding": { + "type": "property", + "name": "conditionExpression" + } + } + ] + } +] \ No newline at end of file diff --git a/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProviderSpec.js b/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProviderSpec.js new file mode 100644 index 00000000..7a6b3793 --- /dev/null +++ b/test/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProviderSpec.js @@ -0,0 +1,221 @@ +import { + inject, + getBpmnJS, + bootstrapCamundaCloudModeler, +} from 'test/TestHelper'; + +import { + query as domQuery +} from 'min-dom'; + +import diagramXML from './ElementTemplatesAppendProvider.bpmn'; +import templates from './ElementTemplatesAppendProvider.json'; + +import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil'; + + +describe('', function() { + + beforeEach(bootstrapCamundaCloudModeler(diagramXML)); + + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + + describe('display', function() { + + it('should display template options', inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + openPopup(task); + + // then + const entries = Object.keys(getEntries()); + const templateEntries = entries.filter((entry) => entry.startsWith('append.template-')); + + expect(templateEntries.length).to.be.greaterThan(0); + })); + + + it('should not display template for Start Event', inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + openPopup(task); + + // then + const entries = Object.keys(getEntries()); + const startEventTemplateEntry = entries.includes('append.template-example.StartEventTemplate'); + + expect(startEventTemplateEntry).to.not.be.true; + })); + + + it('should not display template for Participant', inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + openPopup(task); + + // then + const entries = Object.keys(getEntries()); + const participantTemplateEntry = entries.includes('append.template-example.ParticipantTemplate'); + + expect(participantTemplateEntry).to.not.be.true; + })); + + + it('should not display template for Sequence Flow', inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + openPopup(task); + + // then + const entries = Object.keys(getEntries()); + const sequenceFlowTemplateEntry = entries.includes('append.template-example.SequenceFlowTemplate'); + + expect(sequenceFlowTemplateEntry).to.not.be.true; + })); + + }); + + + describe('append', function() { + + it('should append template', inject(function(elementRegistry, selection) { + + // given + const task = elementRegistry.get('Task_1'); + const template = templates[0]; + + openPopup(task); + + // when + triggerAction(`append.template-${template.id}`); + + // then + const outgoingFlows = getBusinessObject(task).outgoing; + const newElement = outgoingFlows[0].targetRef; + + expect(outgoingFlows).to.have.length(1); + expect(isTemplateApplied(newElement, template)).to.be.true; + })); + + + it('should undo', inject(function(elementRegistry, commandStack, elementTemplates) { + + // given + const task = elementRegistry.get('Task_1'); + const template = templates[0]; + + openPopup(task); + + // when + triggerAction(`append.template-${template.id}`); + + // when + commandStack.undo(); + + // then + const outgoingFlows = getBusinessObject(task).outgoing; + + expect(outgoingFlows).to.have.length(0); + })); + + + it('should redo', inject(function(elementRegistry, commandStack, elementTemplates) { + + // given + const task = elementRegistry.get('Task_1'); + const template = templates[0]; + + openPopup(task); + + // when + triggerAction(`append.template-${template.id}`); + + // when + commandStack.undo(); + commandStack.redo(); + + // then + const outgoingFlows = getBusinessObject(task).outgoing; + const newElement = outgoingFlows[0].targetRef; + + expect(outgoingFlows).to.have.length(1); + expect(isTemplateApplied(newElement, template)).to.be.true; + })); + + }); + +}); + + +// helpers //////////// + +function openPopup(element, offset) { + offset = offset || 100; + + getBpmnJS().invoke(function(popupMenu) { + popupMenu.open(element, 'bpmn-append', { + x: element.x, y: element.y + }); + + }); +} + +function queryEntry(id) { + var container = getMenuContainer(); + + return domQuery('.djs-popup [data-id="' + id + '"]', container); +} + +function getMenuContainer() { + const popup = getBpmnJS().get('popupMenu'); + return popup._current.container; +} + +function triggerAction(id) { + const entry = queryEntry(id); + + if (!entry) { + throw new Error('entry "' + id + '" not found in append menu'); + } + + const popupMenu = getBpmnJS().get('popupMenu'); + const eventBus = getBpmnJS().get('eventBus'); + + return popupMenu.trigger( + eventBus.createEvent({ + target: entry, + x: 0, + y: 0, + }) + ); +} + +function getEntries() { + const popupMenu = getBpmnJS().get('popupMenu'); + return popupMenu._current.entries; +} + +function isTemplateApplied(element, template) { + const businessObject = getBusinessObject(element); + + if (businessObject) { + return businessObject.get('modelerTemplate') === template.id; + } + + return false; +} \ No newline at end of file