const { isExpanded } = require('bpmn-js/lib/util/DiUtil');
const ConstraintParser = require('@proceed/constraint-parser-xml-json');
const UpdateDefinitionsCommandHandler = require('./command-handlers/update-definitions-command-handler.js');
const UpdateCalledProcessHandler = require('./command-handlers/update-called-process.js');
const AddScriptHandler = require('./command-handlers/add-script-handler.js');
const UpdateMetaDataHandler = require('./command-handlers/update-meta-data.js');
const UpdateDocumentationHandler = require('./command-handlers/update-documentation.js');
const UpdateEventDefinitionHandler = require('./command-handlers/update-event-definition.js');
const constraintParser = new ConstraintParser();
const {
toBpmnObject,
getElementsByTagName,
getTargetDefinitionsAndProcessIdForCallActivityByObject,
getUserTaskImplementationString,
generateUserTaskFileName,
getMetaDataFromElement,
getMilestonesFromElement,
getResourcesFromElement,
getLocationsFromElement,
} = require('@proceed/bpmn-helper/');
let elementRegistry;
let elementFactory;
let bpmnFactory;
let cli;
let modeling;
let moddle;
let canvas;
let commandStack;
let bpmnModeler;
/**
* Registers the modeler the events are supposed to be applied in with this module
*
* @param {Object} modeler an instance of a bpmn-js modeler
*/
export function registerModeler(modeler) {
elementRegistry = modeler.get('elementRegistry');
elementFactory = modeler.get('elementFactory');
bpmnFactory = modeler.get('bpmnFactory');
cli = modeler.get('cli');
modeling = modeler.get('modeling');
moddle = modeler.get('moddle');
canvas = modeler.get('canvas');
commandStack = modeler.get('commandStack');
bpmnModeler = modeler;
commandStack.registerHandler('definitions.updateProperties', UpdateDefinitionsCommandHandler);
commandStack.registerHandler('element.updateCalledProcess', UpdateCalledProcessHandler);
commandStack.registerHandler('element.updateScript', AddScriptHandler);
commandStack.registerHandler('element.updateMetaData', UpdateMetaDataHandler);
commandStack.registerHandler('element.updateDocumentation', UpdateDocumentationHandler);
commandStack.registerHandler('element.updateEventDefinition', UpdateEventDefinitionHandler);
const eventBus = modeler.get('eventBus');
// cleanup before removing an element
eventBus.on('commandStack.shape.delete.preExecute', 10000, ({ context }) => {
let { shape } = context;
if (shape.type === 'bpmn:CallActivity') {
removeCallActivityReference(shape.id, true);
}
});
// create startEvent inside of collapsed subprocess
eventBus.on('commandStack.shape.replace.postExecute', 10000, ({ context }) => {
let { newShape } = context;
if (newShape.type === 'bpmn:SubProcess' && !isExpanded(newShape)) {
modeling.createShape(
{ type: 'bpmn:StartEvent', hidden: true },
{ x: newShape.x + newShape.width / 6, y: newShape.y + newShape.height / 2 },
newShape,
{ autoResize: false }
);
}
});
}
// stores which modeler events were triggered by changes made in another client
export const externalEvents = [];
/**
* Adds the given constraints to the extensionElements of the given modeler element
*
* @param {Object} element the modeler element we want to add the constraints to
* @param {Object} cons the constraints we want to add
*/
export async function addConstraintsToElement(element, cons, dontPropagate = false) {
let extensionElements;
// get the already existing extensionElements or create a new one
if (element.businessObject.extensionElements) {
({ extensionElements } = element.businessObject);
} else {
extensionElements = moddle.create('bpmn:ExtensionElements');
extensionElements.values = [];
}
// remove old constraints
extensionElements.values = extensionElements.values.filter(
(el) => el.$type !== 'proceed:ProcessConstraints'
);
if (cons) {
const { hardConstraints, softConstraints } = cons;
const constraints = { processConstraints: { hardConstraints, softConstraints } };
// parse constraints into xml to be able to use bpmn-moddle to create expected object from xml
let constraintXML = constraintParser.fromJsToXml(constraints);
constraintXML = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:extensionElements xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:proceed="https://docs.proceed-labs.org/BPMN">
${constraintXML}
</bpmn2:extensionElements>`;
const constraintObj = await toBpmnObject(constraintXML, 'bpmn:ExtensionElements');
// if there are constraints add them to the extensionsElement, (one entry is the type)
if (Object.keys(constraintObj.values[0]).length > 1) {
extensionElements.values.push(constraintObj.values[0]);
}
}
// if the extensionElements aren't empty => add them to the element
if (extensionElements.values.length === 0) {
extensionElements = undefined;
}
commandStack.execute('element.updateProperties', {
element,
properties: { extensionElements },
additionalInfo: { constraints: cons },
isExternalEvent: true,
dontPropagate,
});
}
export function getMilestones({ businessObject }) {
if (!businessObject) {
return;
}
return getMilestonesFromElement(businessObject);
}
/**
* Adds the given milestones to the extensionElements of the given modeler element
*
* @param {Object} element the modeler element we want to add the milestones to
* @param {Object} milestones the milestones we want to add
*/
export async function addMilestonesToElement(element, milestones, dontPropagate = false) {
let extensionElements;
// get the already existing extensionElements or create a new one
if (element.businessObject.extensionElements) {
({ extensionElements } = element.businessObject);
} else {
extensionElements = moddle.create('bpmn:ExtensionElements');
extensionElements.values = [];
}
// remove old constraints
extensionElements.values = extensionElements.values.filter(
(el) => el.$type !== 'proceed:Milestones'
);
if (milestones) {
const milestonesElement = moddle.create('proceed:Milestones');
milestonesElement.milestone = [];
milestones.forEach((milestone) => {
const milestoneElement = moddle.create('proceed:Milestone');
milestoneElement.id = milestone.id;
milestoneElement.name = milestone.name;
milestoneElement.description = milestone.description;
milestonesElement.milestone.push(milestoneElement);
});
extensionElements.values.push(milestonesElement);
}
// if the extensionElements aren't empty => add them to the element
if (extensionElements.values.length === 0) {
extensionElements = undefined;
}
commandStack.execute('element.updateProperties', {
element,
properties: { extensionElements },
additionalInfo: { milestones },
isExternalEvent: true,
dontPropagate,
});
}
export function getLocations({ businessObject }) {
if (!businessObject) {
return;
}
return getLocationsFromElement(businessObject);
}
/**
* Adds the given locations to the extensionElements of the given modeler element
*
* @param {Object} element the modeler element we want to add the locations to
* @param {Object} locations the locations we want to add
*/
export async function addLocationsToElement(element, locations, dontPropagate = false) {
let extensionElements;
// get the already existing extensionElements or create a new one
if (element.businessObject.extensionElements) {
({ extensionElements } = element.businessObject);
} else {
extensionElements = moddle.create('bpmn:ExtensionElements');
extensionElements.values = [];
}
// remove old constraints
extensionElements.values = extensionElements.values.filter(
(el) => el.$type !== 'proceed:Locations'
);
if (locations) {
const locationsElement = moddle.create('proceed:Locations');
Object.entries(locations).forEach(([locationType, locationTypeEntries]) => {
locationsElement[locationType] = [];
locationTypeEntries.forEach((location) => {
const locationTypeElement = moddle.create(`proceed:${locationType}`);
locationTypeElement.id = location.id;
locationTypeElement.shortName = location.shortName;
locationTypeElement.longName = location.longName;
locationTypeElement.description = location.description;
locationTypeElement.areaRef = location.areaRef;
locationTypeElement.buildingRef = location.buildingRef;
locationTypeElement.factoryRef = location.factoryRef;
locationTypeElement.companyRef = location.companyRef;
locationsElement[locationType].push(locationTypeElement);
});
});
extensionElements.values.push(locationsElement);
}
// if the extensionElements aren't empty => add them to the element
if (extensionElements.values.length === 0) {
extensionElements = undefined;
}
commandStack.execute('element.updateProperties', {
element,
properties: { extensionElements },
additionalInfo: { locations },
isExternalEvent: true,
dontPropagate,
});
}
export function getResources({ businessObject }) {
if (!businessObject) {
return;
}
return getResourcesFromElement(businessObject);
}
/**
* Adds the given resources to the extensionElements of the given modeler element
*
* @param {Object} element the modeler element we want to add the resources to
* @param {Object} resources the resources we want to add
*/
export async function addResourcesToElement(element, resources, dontPropagate = false) {
let extensionElements;
// get the already existing extensionElements or create a new one
if (element.businessObject.extensionElements) {
({ extensionElements } = element.businessObject);
} else {
extensionElements = moddle.create('bpmn:ExtensionElements');
extensionElements.values = [];
}
// remove old constraints
extensionElements.values = extensionElements.values.filter(
(el) => el.$type !== 'proceed:Resources'
);
if (resources) {
const resourcesElement = moddle.create('proceed:Resources');
Object.entries(resources).forEach(([resourceType, resourceTypeEntries]) => {
resourcesElement[resourceType] = [];
resourceTypeEntries.forEach((resource) => {
const resourceTypeElement = moddle.create(`proceed:${resourceType}`);
resourceTypeElement.id = resource.id;
resourceTypeElement.shortName = resource.shortName;
resourceTypeElement.longName = resource.longName;
resourceTypeElement.manufacturer = resource.manufacturer;
resourceTypeElement.manufacturerSerialNumber = resource.manufacturerSerialNumber;
resourceTypeElement.unit = resource.unit;
resourceTypeElement.quantity = resource.quantity;
resourceTypeElement.description = resource.description;
resourcesElement[resourceType].push(resourceTypeElement);
});
});
extensionElements.values.push(resourcesElement);
}
// if the extensionElements aren't empty => add them to the element
if (extensionElements.values.length === 0) {
extensionElements = undefined;
}
commandStack.execute('element.updateProperties', {
element,
properties: { extensionElements },
additionalInfo: { resources },
isExternalEvent: true,
dontPropagate,
});
}
/**
* Returns a constraint object containing all the constraints of the given object
*
* @param {Object} element the modeler element we want to know the constraints of
* @returns {Object} - contains all constraints of the given element
*/
export async function getElementConstraints(element) {
let constraints = {
hardConstraints: [],
softConstraints: [],
};
const { businessObject } = element;
if (businessObject.extensionElements) {
const constraintElements = businessObject.extensionElements.values.filter(
(el) => el.$type === 'proceed:ProcessConstraints'
);
// for now we have to parse the xml using the constraintParser
// maybe rewrite the constraintParser to use bpmn-moddle instead and then use the intermediate step from ModdleObject -> Object here
if (constraintElements) {
const { xml } = await bpmnModeler.saveXML({ format: true });
const elementId = element.type === 'bpmn:Process' ? undefined : element.id;
const parsedConstraints = constraintParser.getConstraints(xml, elementId);
// if there were constraints for the element replace default constraints object and make sure that there is an entry for both types
if (parsedConstraints) {
constraints = parsedConstraints.processConstraints;
constraints.hardConstraints = constraints.hardConstraints || [];
constraints.softConstraints = constraints.softConstraints || [];
}
}
}
return constraints;
}
/**
* Adds process and task constraints as extension elements to the process after checking for inconsistencies
* @param processConstraints
* @param taskConstraintMapping
*/
export async function addConstraints(processConstraints, taskConstraintMapping) {
const promises = [];
if (processConstraints) {
const process = canvas.getRootElement();
promises.push(addConstraintsToElement(process, processConstraints));
}
if (taskConstraintMapping) {
const taskIds = Object.keys(taskConstraintMapping);
promises.concat(
taskIds.map(async (id) => {
const task = elementRegistry.get(id);
if (!task) {
return;
}
await addConstraintsToElement(task, taskConstraintMapping[id]);
})
);
}
await Promise.all(promises);
}
/**
* Add meta information of the called bpmn process to the modeler bpmn where it's getting called from. This includes a custom namespace in the definitions part,
* an import element as first child of definitions and the calledElement attribute of the call activity bpmn element
*
* @param {String} callActivityId The ID of the call activity bpmn element inside the rootBpmn
* @param {String} calledBpmn The bpmn file of the called process
* @param {String} calledProcessLocation The definitionId of the calledBpmn.
*/
export async function addCallActivityReference(callActivityId, calledBpmn, calledProcessLocation) {
// Retrieving all necessary informations from the called bpmn
const calledBpmnObject = await toBpmnObject(calledBpmn);
const [calledBpmnDefinitions] = getElementsByTagName(calledBpmnObject, 'bpmn:Definitions');
const [calledProcess] = getElementsByTagName(calledBpmnObject, 'bpmn:Process');
const calledProcessTargetNamespace = calledBpmnDefinitions.targetNamespace;
commandStack.execute('element.updateCalledProcess', {
elementId: callActivityId,
calledProcessId: calledProcess.id,
calledProcessName: calledBpmnDefinitions.name,
calledProcessTargetNamespace,
calledProcessLocation,
});
}
/**
* Remove the reference to the called process added in {@link addCallActivityReference} but remains the actual bpmn element
*
* @param {String} callActivityId The ID of the bpmn element for which the meta information should be removed
* @param {Boolean} noDistribution if this event should not be distributed to other machines
*/
export function removeCallActivityReference(callActivityId, noDistribution) {
// remove calledElement from callActivity
commandStack.execute('element.updateCalledProcess', {
elementId: callActivityId,
isExternalEvent: noDistribution,
});
}
/**
* Gets the id of the process definition of the process called in a callActivity
*
* @param {String} callActivityId
* @returns {String|undefined} - the id of the process definition of the called process
*/
export function getDefinitionsIdForCallActivity(callActivityId) {
const definitions = bpmnModeler.getDefinitions();
try {
const { definitionId } = getTargetDefinitionsAndProcessIdForCallActivityByObject(
definitions,
callActivityId
);
return definitionId;
} catch (err) {
return undefined;
}
}
export function addJSToElement(elementId, script) {
commandStack.execute('element.updateScript', {
elementId,
script,
});
}
export function setUserTaskFileName(taskId, fileName) {
const userTask = elementRegistry.get(taskId);
if (!userTask) {
return;
}
commandStack.execute('element.updateProperties', {
element: userTask,
properties: { fileName, implementation: getUserTaskImplementationString() },
});
}
export function setName(newName) {
commandStack.execute('definitions.updateProperties', {
properties: { name: newName },
});
}
export function getMetaData({ businessObject }) {
if (!businessObject) {
return;
}
return getMetaDataFromElement(businessObject);
}
export function updateMetaData(elementId, metaData) {
commandStack.execute('element.updateMetaData', {
elementId,
metaData,
});
}
export function getDocumentation({ businessObject }) {
if (!businessObject) {
return;
}
if (businessObject.documentation) {
return businessObject.documentation[0].text;
} else {
return '';
}
}
export function updateDocumentation(elementId, documentation) {
commandStack.execute('element.updateDocumentation', {
elementId,
documentation,
});
}
export function updateTimer(elementId, formalExpression) {
commandStack.execute('element.updateEventDefinition', {
elementId,
formalExpression,
});
}
export function updateErrorOrEscalation(elementId, refId, label) {
commandStack.execute('element.updateEventDefinition', {
elementId,
refName: label,
refId,
});
}
export function changeUserTasksImplementation(use5thIndustry) {
// set every user task implementation to the given implementation
const userTasks = elementRegistry.filter((element) => element.type === 'bpmn:UserTask');
userTasks.forEach((userTask) => {
const { businessObject } = userTask;
if (use5thIndustry) {
// retain old idOrder if there is one
setUserTaskImplementation(userTask.id, '5thIndustry');
} else {
// retain old fileName if there is one or generate new one if there isn't
setUserTaskFileName(userTask.id, businessObject.fileName || generateUserTaskFileName());
}
});
}
export function setUserTaskImplementation(elementId, implementation) {
const userTask = elementRegistry.get(elementId);
if (!userTask) {
return;
}
commandStack.execute('element.updateProperties', {
element: userTask,
// make sure the property change is transmitted
properties: { implementation },
});
}
/**
* Sets external value on a task
*
* @param {Object} element the element to change
* @param {Boolean} external if the task is supposed to be external
*/
export async function setTaskExternal(element, external) {
commandStack.execute('element.updateProperties', {
element,
properties: { external: external || null },
});
}