From ad0ea18341f28b1e8a3180483839691dc9433d7d Mon Sep 17 00:00:00 2001 From: Beatriz Mendes Date: Fri, 3 Feb 2023 11:05:11 +0100 Subject: [PATCH] feat(create-append-anything): append with templates Closes #235 --- lib/camunda-cloud/Modeler.js | 2 + .../ElementTemplatesAppendProvider.js | 149 +++++++++++++ .../features/create-append-anything/index.js | 6 + .../ElementTemplatesAppendProvider.bpmn | 13 ++ .../ElementTemplatesAppendProvider.json | 160 ++++++++++++++ .../ElementTemplatesAppendProviderSpec.js | 205 ++++++++++++++++++ 6 files changed, 535 insertions(+) create mode 100644 lib/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.js create mode 100644 lib/camunda-cloud/features/create-append-anything/index.js create mode 100644 test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProvider.bpmn create mode 100644 test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProvider.json create mode 100644 test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProviderSpec.js diff --git a/lib/camunda-cloud/Modeler.js b/lib/camunda-cloud/Modeler.js index 9ef36626..00309cf6 100644 --- a/lib/camunda-cloud/Modeler.js +++ b/lib/camunda-cloud/Modeler.js @@ -12,6 +12,7 @@ import { ZeebeDescriptionProvider } from 'bpmn-js-properties-panel'; +import createAppendAnything from './features/create-append-anything'; import replaceModule from './features/replace'; import sharedReplaceModule from '../shared/features/replace'; import colorPickerModule from 'bpmn-js-color-picker'; @@ -48,6 +49,7 @@ Modeler.prototype._camundaCloudModules = [ rulesModule, zeebePropertiesProviderModule, cloudElementTemplatesPropertiesProvider, + createAppendAnything, 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..f0e5ef9d --- /dev/null +++ b/lib/camunda-cloud/features/create-append-anything/ElementTemplatesAppendProvider.js @@ -0,0 +1,149 @@ +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, move, rules) { + + this._popupMenu = popupMenu; + this._translate = translate; + this._elementTemplates = elementTemplates; + this._autoPlace = autoPlace; + this._create = create; + this._move = move; + 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]; + + return (![ + 'bpmn:StartEvent', + 'bpmn:Participant' + ].includes(type)); + }); +}; + +/** + * 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/rules/create-append-anything/ElementTemplatesAppendProvider.bpmn b/test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProvider.bpmn new file mode 100644 index 00000000..e455a5d8 --- /dev/null +++ b/test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProvider.bpmn @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProvider.json b/test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProvider.json new file mode 100644 index 00000000..b3bf0232 --- /dev/null +++ b/test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProvider.json @@ -0,0 +1,160 @@ +[ + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "REST Connector (No Auth)", + "id": "io.camunda.connectors.HttpJson.v1.noAuth", + "description": "Invoke REST API and retrieve the result", + "icon": { + "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" + }, + "documentationRef": "https://docs.camunda.io/docs/components/modeler/web-modeler/connectors/available-connectors/rest/", + "appliesTo": [ + "bpmn:ServiceTask" + ], + "elementType": { + "value": "bpmn:ServiceTask" + }, + "groups": [ + { + "id": "endpoint", + "label": "HTTP Endpoint" + }, + { + "id": "input", + "label": "Payload" + }, + { + "id": "output", + "label": "Response Mapping" + } + ], + "properties": [ + { + "type": "Hidden", + "value": "io.camunda:http-json:1", + "binding": { + "type": "zeebe:taskDefinition:type" + } + }, + { + "label": "Method", + "group": "endpoint", + "type": "Dropdown", + "value": "get", + "choices": [ + { "name": "GET", "value": "get" }, + { "name": "POST", "value": "post" }, + { "name": "PATCH", "value": "patch" }, + { "name": "PUT", "value": "put" }, + { "name": "DELETE", "value": "delete" } + ], + "binding": { + "type": "zeebe:input", + "name": "method" + } + }, + { + "label": "URL", + "group": "endpoint", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "url" + }, + "constraints": { + "notEmpty": true, + "pattern": { + "value": "^https?://.*", + "message": "Must be a http(s) URL." + } + } + }, + { + "label": "Query Parameters", + "description": "Map of query parameters to add to the request URL", + "group": "endpoint", + "type": "Text", + "feel": "required", + "binding": { + "type": "zeebe:input", + "name": "queryParameters" + }, + "optional": true + }, + { + "label": "HTTP Headers", + "description": "Map of HTTP headers to add to the request", + "group": "endpoint", + "type": "Text", + "feel": "required", + "binding": { + "type": "zeebe:input", + "name": "headers" + }, + "optional": true + }, + { + "label": "Request Body", + "description": "JSON payload to send with the request", + "group": "input", + "type": "Text", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "body" + }, + "optional": true + }, + { + "label": "Result Variable", + "description": "Name of variable to store the response in", + "group": "output", + "type": "String", + "binding": { + "type": "zeebe:taskHeader", + "key": "resultVariable" + } + }, + { + "label": "Result Expression", + "description": "Expression to map the response into process variables", + "group": "output", + "type": "Text", + "feel": "required", + "binding": { + "type": "zeebe:taskHeader", + "key": "resultExpression" + } + } + ] + }, + { + "$schema": "https://unpkg.com/browse/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "participant template", + "id": "participant-template", + "appliesTo": ["bpmn:Participant"], + "properties": [ + { + "binding": { + "type": "property", + "name": "someProp" + } + } + ] + }, + { + "$schema": "https://unpkg.com/browse/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "start event template", + "id": "start-event-template", + "appliesTo": ["bpmn:StartEvent"], + "properties": [ + { + "binding": { + "type": "property", + "name": "someProp" + } + } + ] + } +] \ No newline at end of file diff --git a/test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProviderSpec.js b/test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProviderSpec.js new file mode 100644 index 00000000..71f15a6a --- /dev/null +++ b/test/camunda-cloud/features/rules/create-append-anything/ElementTemplatesAppendProviderSpec.js @@ -0,0 +1,205 @@ +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 startEventTemplate = entries.includes('append.template-start-event-template'); + + expect(startEventTemplate).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 startEventTemplate = entries.includes('append.template-participant-template'); + + expect(startEventTemplate).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