diff --git a/lib/camunda-cloud/Modeler.js b/lib/camunda-cloud/Modeler.js index 5d7bd9ea..3922b90d 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 replaceModule from './features/replace'; import { commonModdleExtensions, commonModules } from './util/commonModules'; @@ -44,7 +45,8 @@ Modeler.prototype._camundaCloudModules = [ behaviorsModule, rulesModule, zeebePropertiesProviderModule, - cloudElementTemplatesPropertiesProvider + cloudElementTemplatesPropertiesProvider, + replaceModule ]; Modeler.prototype._modules = [].concat( diff --git a/lib/camunda-cloud/features/replace/ElementTemplatesReplaceProvider.js b/lib/camunda-cloud/features/replace/ElementTemplatesReplaceProvider.js new file mode 100644 index 00000000..75b6149b --- /dev/null +++ b/lib/camunda-cloud/features/replace/ElementTemplatesReplaceProvider.js @@ -0,0 +1,215 @@ +import { + isDifferentType +} from 'bpmn-js/lib/features/popup-menu/util/TypeUtil'; + +import { + getBusinessObject, + isAny +} from 'bpmn-js/lib/util/ModelUtil'; + +import { + getReplaceOptions +} from './ReplaceOptionsUtil'; + +/** + * A replace menu provider that allows to replace elements with + * element templates. + */ +export default function ElementTemplatesReplaceProvider(popupMenu, translate, elementTemplates) { + + this._popupMenu = popupMenu; + this._translate = translate; + this._elementTemplates = elementTemplates; + + this.register(); +} + +ElementTemplatesReplaceProvider.$inject = [ + 'popupMenu', + 'translate', + 'elementTemplates' +]; + +/** + * Register replace menu provider in the popup menu + */ +ElementTemplatesReplaceProvider.prototype.register = function() { + this._popupMenu.registerProvider('bpmn-replace', this); +}; + +/** + * Adds the element templates to the replace menu. + * @param {djs.model.Base} element + * + * @returns {Object} + */ +ElementTemplatesReplaceProvider.prototype.getPopupMenuEntries = function(element) { + + return (entries) => { + + // convert our entries into something sortable + let entrySet = Object.entries(entries); + + // add unlink template option + this._addPlainElementEntry(element, entrySet); + + // add template entries + entrySet = [ ...entrySet, ...this.getTemplateEntries(element) ]; + + // convert back to object + return entrySet.reduce((entries, [ key, value ]) => { + entries[key] = value; + + return entries; + }, {}); + }; +}; + +/** + * Adds the option to replace with plain element (unlink template). + * + * @param {djs.model.Base} element + * @param {Array} entries + */ +ElementTemplatesReplaceProvider.prototype._addPlainElementEntry = function(element, entries) { + + const replaceOption = this._getPlainEntry(element, entries); + + const [ + insertIndex, + entry + ] = replaceOption; + + // insert unlink entry + entries.splice(insertIndex, 0, [ entry.id, entry ]); +}; + +/** + * Returns the option to replace with plain element and the index where it should be inserted. + * + * @param {djs.model.Base} element + * @param {Array} entries + * + * @returns {Array} + */ +ElementTemplatesReplaceProvider.prototype._getPlainEntry = function(element, entries) { + + const options = getReplaceOptions(); + + const isSameType = (element, option) => option.target && !isDifferentType(element)(option); + + const optionIndex = options.findIndex(option => isSameType(element, option)); + + if (optionIndex === -1) { + return; + } + + const option = options[optionIndex]; + + const entry = { + id: option.actionName, + action: () => { + this._elementTemplates.applyTemplate(element, null); + }, + label: this._translate(option.label), + className: option.className + }; + + // insert after previous option, if it exists + const previousIndex = getOptionIndex(options, optionIndex - 1, entries); + + if (previousIndex) { + return [ + previousIndex + 1, + entry + ]; + } + + // insert before next option, if it exists + const nextIndex = getOptionIndex(options, optionIndex + 1, entries); + + if (nextIndex) { + return [ + nextIndex, + entry + ]; + } + + // fallback to insert at start + return [ + 0, + entry + ]; +}; + +/** + * Get all element templates that can be used to replace the given element. + * + * @param {djs.model.Base} element + * + * @return {Array} a list of element templates as menu entries + */ +ElementTemplatesReplaceProvider.prototype.getTemplateEntries = function(element) { + + const templates = this._getMatchingTemplates(element); + return templates.map(template => { + + const { + icon = {}, + group = { + id: 'templates', + name: this._translate('Templates') + } + } = template; + + const entryId = `replace-with-template-${template.id}`; + + return [ entryId, { + name: template.name, + description: template.description, + documentationRef: template.documentationRef, + imageUrl: icon.contents, + group, + action: () => { + this._elementTemplates.applyTemplate(element, template); + } + } ]; + }); +}; + +/** + * Returns the templates that can the element can be replaced with. + * + * @param {djs.model.Base} element + * + * @return {Array} + */ +ElementTemplatesReplaceProvider.prototype._getMatchingTemplates = function(element) { + return this._elementTemplates.getLatest().filter(template => { + return isAny(element, template.appliesTo) && !isTemplateApplied(element, template); + }); +}; + + +// helpers //////////// +export function isTemplateApplied(element, template) { + const businessObject = getBusinessObject(element); + + if (businessObject) { + return businessObject.get('zeebe:modelerTemplate') === template.id; + } + + return false; +} + +function getOptionIndex(options, index, entries) { + const option = options[index]; + + if (!option) { + return false; + } + + return entries.findIndex( + ([ key ]) => key === option.actionName + ); +} \ No newline at end of file diff --git a/lib/camunda-cloud/features/replace/ReplaceOptionsUtil.js b/lib/camunda-cloud/features/replace/ReplaceOptionsUtil.js new file mode 100644 index 00000000..9c9e2674 --- /dev/null +++ b/lib/camunda-cloud/features/replace/ReplaceOptionsUtil.js @@ -0,0 +1,7 @@ +import * as replaceOptions from 'bpmn-js/lib/features/replace/ReplaceOptions'; + +const ALL_OPTIONS = Object.values(replaceOptions).flat(); + +export function getReplaceOptions() { + return ALL_OPTIONS; +} \ No newline at end of file diff --git a/lib/camunda-cloud/features/replace/index.js b/lib/camunda-cloud/features/replace/index.js new file mode 100644 index 00000000..eefbf0e2 --- /dev/null +++ b/lib/camunda-cloud/features/replace/index.js @@ -0,0 +1,6 @@ +import ElementTemplatesReplaceProvider from './ElementTemplatesReplaceProvider'; + +export default { + __init__: [ 'elementTemplatesProvider' ], + elementTemplatesProvider: [ 'type', ElementTemplatesReplaceProvider ] +}; \ No newline at end of file diff --git a/test/camunda-cloud/ElementTemplatesReplaceProviderSpec.js b/test/camunda-cloud/ElementTemplatesReplaceProviderSpec.js new file mode 100644 index 00000000..99265cf5 --- /dev/null +++ b/test/camunda-cloud/ElementTemplatesReplaceProviderSpec.js @@ -0,0 +1,285 @@ +import { + inject, + getBpmnJS, + bootstrapCamundaCloudModeler +} from 'test/TestHelper'; + +import { + attr as domAttr, + queryAll as domQueryAll, + query as domQuery +} from 'min-dom'; + +import { + isTemplateApplied +} from 'lib/camunda-cloud/features/replace/ElementTemplatesReplaceProvider'; + +import simpleXml from '../fixtures/replaceElementTemplates.bpmn'; +import templates from './replace-element-templates.json'; + + +describe('', function() { + + beforeEach(bootstrapCamundaCloudModeler( + simpleXml + )); + + beforeEach(inject(function(elementTemplates) { + elementTemplates.set(templates); + })); + + + describe('display', function() { + + it('should display matching element templates', inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('ServiceTask_1'); + + // when + openPopup(task); + + // then + expect(queryTemplateEntries()).to.have.length(5); + + })); + + + it('should not display element templates that do not apply', inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('Task_1'); + + // when + openPopup(task); + + // then + expect(queryTemplateEntries()).to.have.length(4); + + })); + + + it('should not display applied element template', inject(function(elementRegistry, elementTemplates) { + + // given + const task = elementRegistry.get('ServiceTask_1'); + elementTemplates.applyTemplate(task, templates[0]); + + // when + openPopup(task); + + // then + expect(queryTemplateEntries()).to.have.length(4); + + })); + + }); + + + describe('options', function() { + + beforeEach(inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('ServiceTask_1'); + + // when + openPopup(task); + + })); + + it('should display title', function() { + + // given + const template = templates[0]; + const entry = queryEntry(`replace-with-template-${template.id}`); + + // then + expect(domQuery('.djs-popup-entry-name', entry).textContent).to.eql(template.name); + + }); + + + it('should display icon', function() { + + // given + const template = templates[0]; + const entry = queryEntry(`replace-with-template-${template.id}`); + const icon = domQuery('.djs-popup-entry-icon', entry); + + // then + expect(icon.getAttribute('src')).to.eql(template.icon.contents); + + }); + + + it('should display description', function() { + + // given + const template = templates[0]; + const entry = queryEntry(`replace-with-template-${template.id}`); + const description = domQuery('.djs-popup-entry-description', entry); + + // then + expect(description.textContent).to.eql(template.description); + + }); + + + it('should display documentation link', function() { + + // given + const template = templates[0]; + const entry = queryEntry(`replace-with-template-${template.id}`); + const link = domQuery('.djs-popup-entry-docs a', entry); + + // then + expect(link.getAttribute('href')).to.eql(template.documentationRef); + + }); + + + it('should display group', function() { + + // given + const group = domQuery('.djs-popup-group[data-group="templates"'); + + // then + expect(group).to.exist; + expect(group.children).to.have.lengthOf(5); + + }); + + }); + + + describe('replace', function() { + + it('should apply template', inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('ServiceTask_1'); + const template = templates[0]; + + // when + openPopup(task); + + triggerAction(`replace-with-template-${template.id}`); + + // then + expect(isTemplateApplied(task, template)).to.be.true; + + })); + + + it('should unbind template', inject(function(elementRegistry) { + + // given + const task = elementRegistry.get('ServiceTask_1'); + const template = templates[0]; + + openPopup(task); + triggerAction(`replace-with-template-${template.id}`); + + // when + openPopup(task); + triggerAction('replace-with-service-task'); + + // then + expect(isTemplateApplied(task, template)).to.be.false; + })); + + + it('should undo', inject(function(elementRegistry, commandStack) { + + // given + const task = elementRegistry.get('ServiceTask_1'); + const template = templates[0]; + + openPopup(task); + triggerAction(`replace-with-template-${template.id}`); + + // when + commandStack.undo(); + + // then + expect(isTemplateApplied(task, template)).to.be.false; + + })); + + + it('should redo', inject(function(elementRegistry, commandStack) { + + // given + const task = elementRegistry.get('ServiceTask_1'); + const template = templates[0]; + + openPopup(task); + triggerAction(`replace-with-template-${template.id}`); + + // when + commandStack.undo(); + commandStack.redo(); + + // then + expect(isTemplateApplied(task, template)).to.be.true; + + })); + + }); + +}); + + +// helpers //////////// + +function openPopup(element, offset) { + offset = offset || 100; + + getBpmnJS().invoke(function(popupMenu) { + popupMenu.open(element, 'bpmn-replace', { + x: element.x, y: element.y + }); + + }); +} + +function queryEntry(id) { + var container = getMenuContainer(); + + return domQuery('.djs-popup [data-id="' + id + '"]', container); +} + +function queryTemplateEntries() { + var container = getMenuContainer(); + + const entries = Array.from(domQueryAll('.djs-popup-body .entry', container)); + return entries.filter(function(entry) { + return domAttr(entry, 'data-id').startsWith('replace-with-template'); + }); +} + +function getMenuContainer() { + const popup = getBpmnJS().get('popupMenu'); + return popup._current.container; +} + + +function triggerAction(id) { + var entry = queryEntry(id); + + if (!entry) { + throw new Error('entry "' + id + '" not found in replace menu'); + } + + var popupMenu = getBpmnJS().get('popupMenu'); + var eventBus = getBpmnJS().get('eventBus'); + + return popupMenu.trigger( + eventBus.createEvent({ + target: entry, + x: 0, + y: 0, + }) + ); +} diff --git a/test/camunda-cloud/replace-element-templates.json b/test/camunda-cloud/replace-element-templates.json new file mode 100644 index 00000000..1ad557d8 --- /dev/null +++ b/test/camunda-cloud/replace-element-templates.json @@ -0,0 +1,737 @@ +[ + { + "$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/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "REST Connector (Basic Auth)", + "id": "io.camunda.connectors.HttpJson.v1.basicAuth", + "description": "Invoke REST API and retrieve the result secured by Basic Authentication", + "documentationRef": "https://docs.camunda.io/docs/components/modeler/web-modeler/connectors/available-connectors/rest/", + "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" + }, + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:ServiceTask" + }, + "groups": [ + { + "id": "endpoint", + "label": "HTTP Endpoint" + }, + { + "id": "input", + "label": "Payload" + }, + { + "id": "authentication", + "label": "Authentication" + }, + { + "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 + }, + { + "type": "Hidden", + "value": "basic", + "binding": { + "type": "zeebe:input", + "name": "authentication.type" + } + }, + { + "label": "Username", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "authentication.username" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Password", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "authentication.password" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Request Body", + "description": "JSON payload to send with the request", + "group": "input", + "type": "Text", + "feel": "required", + "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/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "REST Connector (Bearer Token Auth)", + "id": "io.camunda.connectors.HttpJson.v1.bearerToken", + "description": "Invoke REST API and retrieve the result secured by Bearer Token Authentication", + "documentationRef": "https://docs.camunda.io/docs/components/modeler/web-modeler/connectors/available-connectors/rest/", + "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" + }, + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:ServiceTask" + }, + "groups": [ + { + "id": "endpoint", + "label": "HTTP Endpoint" + }, + { + "id": "input", + "label": "Payload" + }, + { + "id": "authentication", + "label": "Authentication" + }, + { + "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 + }, + { + "type": "Hidden", + "value": "bearer", + "binding": { + "type": "zeebe:input", + "name": "authentication.type" + } + }, + { + "label": "Bearer Token", + "group": "authentication", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "authentication.token" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Request Body", + "description": "JSON payload to send with the request", + "group": "input", + "type": "Text", + "feel": "required", + "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/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "SendGrid Email Template Connector", + "id": "io.camunda.connectors.SendGrid.v1.template", + "description": "Send an Email via SendGrid Dynamic Template", + "documentationRef": "https://docs.camunda.io/docs/components/modeler/web-modeler/connectors/available-connectors/sendgrid/", + "icon": { + "contents": "data:image/svg+xml;utf8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M0.285706%205.40847H5.43837V10.5611H0.285706V5.40847Z%22%20fill%3D%22white%22%2F%3E%0A%3Cpath%20d%3D%22M0.285706%205.40847H5.43837V10.5611H0.285706V5.40847Z%22%20fill%3D%22%2399E1F4%22%2F%3E%0A%3Cpath%20d%3D%22M5.43837%2010.5611L10.5611%2010.5616V15.6844H5.43837V10.5611Z%22%20fill%3D%22white%22%2F%3E%0A%3Cpath%20d%3D%22M5.43837%2010.5611L10.5611%2010.5616V15.6844H5.43837V10.5611Z%22%20fill%3D%22%2399E1F4%22%2F%3E%0A%3Cpath%20d%3D%22M0.285706%2015.6846L5.43837%2015.6844V15.7143H0.285706V15.6846ZM0.285706%2010.5619H5.43837V15.6844L0.285706%2015.6846V10.5619Z%22%20fill%3D%22%231A82E2%22%2F%3E%0A%3Cpath%20d%3D%22M5.43837%200.285706H10.5611V5.40847H5.43837V0.285706ZM10.5616%205.43837H15.7143V10.5611H10.5616V5.43837Z%22%20fill%3D%22%2300B3E3%22%2F%3E%0A%3Cpath%20d%3D%22M5.43837%2010.5611L10.5611%2010.5616V5.40847H5.43837V10.5611Z%22%20fill%3D%22%23009DD9%22%2F%3E%0A%3Cpath%20d%3D%22M10.5611%200.285706H15.7143V5.40847H10.5611V0.285706Z%22%20fill%3D%22%231A82E2%22%2F%3E%0A%3Cpath%20d%3D%22M10.5611%205.40847H15.7143V5.43837H10.5616L10.5611%205.40847Z%22%20fill%3D%22%231A82E2%22%2F%3E%0A%3C%2Fsvg%3E" + }, + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:ServiceTask" + }, + "groups": [ + { + "id": "sendgrid", + "label": "SendGrid API" + }, + { + "id": "sender", + "label": "Sender" + }, + { + "id": "receiver", + "label": "Receiver" + }, + { + "id": "template", + "label": "Dynamic Email Template" + } + ], + "properties": [ + { + "type": "Hidden", + "value": "io.camunda:sendgrid:1", + "binding": { + "type": "zeebe:taskDefinition:type" + } + }, + { + "label": "SendGrid API Key", + "group": "sendgrid", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "apiKey" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Name", + "group": "sender", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "from.name" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Email Address", + "group": "sender", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "from.email" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Name", + "group": "receiver", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "to.name" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Email Address", + "group": "receiver", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "to.email" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Template ID", + "group": "template", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "template.id" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Template Data", + "group": "template", + "type": "Text", + "feel": "required", + "binding": { + "type": "zeebe:input", + "name": "template.data" + }, + "constraints": { + "notEmpty": true + } + } + ] + }, + { + "$schema": "https://unpkg.com/@camunda/zeebe-element-templates-json-schema/resources/schema.json", + "name": "SendGrid Email Connector", + "id": "io.camunda.connectors.SendGrid.v1.content", + "description": "Send an Email via SendGrid", + "documentationRef": "https://docs.camunda.io/docs/components/modeler/web-modeler/connectors/available-connectors/sendgrid/", + "icon": { + "contents": "data:image/svg+xml;utf8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20d%3D%22M0.285706%205.40847H5.43837V10.5611H0.285706V5.40847Z%22%20fill%3D%22white%22%2F%3E%0A%3Cpath%20d%3D%22M0.285706%205.40847H5.43837V10.5611H0.285706V5.40847Z%22%20fill%3D%22%2399E1F4%22%2F%3E%0A%3Cpath%20d%3D%22M5.43837%2010.5611L10.5611%2010.5616V15.6844H5.43837V10.5611Z%22%20fill%3D%22white%22%2F%3E%0A%3Cpath%20d%3D%22M5.43837%2010.5611L10.5611%2010.5616V15.6844H5.43837V10.5611Z%22%20fill%3D%22%2399E1F4%22%2F%3E%0A%3Cpath%20d%3D%22M0.285706%2015.6846L5.43837%2015.6844V15.7143H0.285706V15.6846ZM0.285706%2010.5619H5.43837V15.6844L0.285706%2015.6846V10.5619Z%22%20fill%3D%22%231A82E2%22%2F%3E%0A%3Cpath%20d%3D%22M5.43837%200.285706H10.5611V5.40847H5.43837V0.285706ZM10.5616%205.43837H15.7143V10.5611H10.5616V5.43837Z%22%20fill%3D%22%2300B3E3%22%2F%3E%0A%3Cpath%20d%3D%22M5.43837%2010.5611L10.5611%2010.5616V5.40847H5.43837V10.5611Z%22%20fill%3D%22%23009DD9%22%2F%3E%0A%3Cpath%20d%3D%22M10.5611%200.285706H15.7143V5.40847H10.5611V0.285706Z%22%20fill%3D%22%231A82E2%22%2F%3E%0A%3Cpath%20d%3D%22M10.5611%205.40847H15.7143V5.43837H10.5616L10.5611%205.40847Z%22%20fill%3D%22%231A82E2%22%2F%3E%0A%3C%2Fsvg%3E" + }, + "appliesTo": [ + "bpmn:Task" + ], + "elementType": { + "value": "bpmn:ServiceTask" + }, + "groups": [ + { + "id": "sendgrid", + "label": "SendGrid API" + }, + { + "id": "sender", + "label": "Sender" + }, + { + "id": "receiver", + "label": "Receiver" + }, + { + "id": "content", + "label": "Email Content" + } + ], + "properties": [ + { + "type": "Hidden", + "value": "io.camunda:sendgrid:1", + "binding": { + "type": "zeebe:taskDefinition:type" + } + }, + { + "label": "SendGrid API Key", + "group": "sendgrid", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "apiKey" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Name", + "group": "sender", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "from.name" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Email Address", + "group": "sender", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "from.email" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Name", + "group": "receiver", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "to.name" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Email Address", + "group": "receiver", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "to.email" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Subject", + "group": "content", + "type": "String", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "content.subject" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Content Type", + "group": "content", + "type": "String", + "feel": "optional", + "value": "text/plain", + "binding": { + "type": "zeebe:input", + "name": "content.type" + }, + "constraints": { + "notEmpty": true + } + }, + { + "label": "Body", + "group": "content", + "type": "Text", + "feel": "optional", + "binding": { + "type": "zeebe:input", + "name": "content.value" + }, + "constraints": { + "notEmpty": true + } + } + ] + } +] \ No newline at end of file diff --git a/test/fixtures/replaceElementTemplates.bpmn b/test/fixtures/replaceElementTemplates.bpmn new file mode 100644 index 00000000..95877e51 --- /dev/null +++ b/test/fixtures/replaceElementTemplates.bpmn @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + +