Skip to content

Commit

Permalink
feat(cloud): templates as replace menu entries
Browse files Browse the repository at this point in the history
Closes #204
  • Loading branch information
smbea committed Nov 29, 2022
1 parent 8c07bfd commit 193e9cb
Show file tree
Hide file tree
Showing 5 changed files with 1,113 additions and 1 deletion.
97 changes: 97 additions & 0 deletions lib/camunda-cloud/ElementTemplatesReplaceProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
getBusinessObject,
isAny
} from 'bpmn-js/lib/util/ModelUtil';

/**
* A replace menu provider that allows to replace elements with
* element templates.
*/
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);
};

ElementTemplatesReplaceProvider.prototype._getMatchingTemplates = function(element) {
return this._elementTemplates.getLatest().filter(template => {
return isAny(element, template.appliesTo) && !isTemplateApplied(element, template);
});
};

/**
* Get all element templates that can be used to replace the given element.
*
* @param {djs.model.Base} element
*
* @return {Array<Object>} a list of element templates as menu entries
*/
ElementTemplatesReplaceProvider.prototype.getEntries = function(element) {

const templates = this._getMatchingTemplates(element);
return templates.map(template => {

const {
id,
icon = {},
} = template;

const group = {
id: 'templates',
name: this._translate('Templates')
};

return {
...template,
group,
id: `replace-with-template-${id}`,
imageUrl: sanitizeImageUrl(icon.contents),
action: () => {
this._elementTemplates.applyTemplate(element, template);
}
};
});
};


export default {
__init__: [ 'elementTemplatesProvider' ],
elementTemplatesProvider: [ 'type', ElementTemplatesReplaceProvider ]
};


// helpers ////////////
function sanitizeImageUrl(url) {
if (url && /^(https?|data):/.test(url)) {
return url;
}

return null;
}

export function isTemplateApplied(element, template) {
const businessObject = getBusinessObject(element);

if (businessObject) {
return businessObject.get('zeebe:modelerTemplate') === template.id;
}

return false;
}
4 changes: 3 additions & 1 deletion lib/camunda-cloud/Modeler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ZeebeDescriptionProvider
} from 'bpmn-js-properties-panel';

import ElementTemplatesReplaceProvider from './ElementTemplatesReplaceProvider';

import { commonModdleExtensions, commonModules } from './util/commonModules';

Expand Down Expand Up @@ -44,7 +45,8 @@ Modeler.prototype._camundaCloudModules = [
behaviorsModule,
rulesModule,
zeebePropertiesProviderModule,
cloudElementTemplatesPropertiesProvider
cloudElementTemplatesPropertiesProvider,
ElementTemplatesReplaceProvider
];

Modeler.prototype._modules = [].concat(
Expand Down
259 changes: 259 additions & 0 deletions test/camunda-cloud/ElementTemplatesReplaceProviderSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
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/ElementTemplatesReplaceProvider';

import simpleXml from 'test/fixtures/replaceElementTemplates.bpmn';
import templates from './replace-element-templates.json';


describe('<ElementTemplatesReplaceProvider>', 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');

// when
openPopup(task);

triggerAction(`replace-with-template-${templates[0].id}`);

// then
expect(isTemplateApplied(task, templates[0])).to.be.true;
}));


it('should undo', inject(function(elementRegistry, commandStack) {

// given
const task = elementRegistry.get('ServiceTask_1');
openPopup(task);
triggerAction(`replace-with-template-${templates[0].id}`);

// when
commandStack.undo();

// then
expect(isTemplateApplied(task, templates[0])).to.be.false;
}));


it('should redo', inject(function(elementRegistry, commandStack) {

// given
const task = elementRegistry.get('ServiceTask_1');
openPopup(task);
triggerAction(`replace-with-template-${templates[0].id}`);

// when
commandStack.undo();
commandStack.redo();

// then
expect(isTemplateApplied(task, templates[0])).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,
})
);
}
Loading

0 comments on commit 193e9cb

Please sign in to comment.