import { preSerialize, parse } from './event-serialization.js';
import { processInterface } from '@/frontend/backend-api/index.js';
let processDefinitionsId = null;
let subprocessId = null;
/**
* Sets processDefinitionsId and optional subprocessId which is needed when distributing the event
*
* @param {String} pId supposed id of the process insidethe modeler
* @param {String} sId supposed id of the subprocess
*/
export function setProcessDefinitionsId(pId, sId) {
processDefinitionsId = pId;
subprocessId = sId || null;
}
let elementRegistry;
let elementFactory;
let bpmnFactory;
let cli;
let modeling;
let canvas;
let commandStack;
let bpmnModeler;
/**
* Registers the modeler for which we want to distribute and apply the events
*
* @param {Object} modeler the 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');
canvas = modeler.get('canvas');
commandStack = modeler.get('commandStack');
bpmnModeler = modeler;
}
/**
* Sets up callbacks that trigger distribution when certain events are encountered in the registered modeler
*/
export function activateDistribution() {
const propagateEvents = [
'shape.create',
'shape.delete',
'shape.resize',
'shape.replace',
'shape.toggleCollapse',
'connection.create',
'connection.delete',
'connection.updateWaypoints',
'element.updateLabel',
'element.updateProperties',
'element.updateScript',
'element.updateMetaData',
'element.updateDocumentation',
'element.updateEventDefinition',
'elements.move',
'connection.reconnect',
'canvas.updateRoot',
'definitions.updateProperties',
'element.updateCalledProcess',
];
// stores events we want to send and the ones that are implicitly triggered by them which we dont want to send
const executionHeap = [];
// removing the last participant implicitly changes the root from a collaboration to a process
// remember the new root process id to give it to other clients
let implicitRootChange;
bpmnModeler.on('commandStack.preExecute', ({ command, context }) => {
// check if the event awaiting execution is among the ones we send to other engines
if (propagateEvents.includes(command)) {
executionHeap.unshift({ command, context });
}
});
// react to changes and send them to other clients if they are originally from this one
bpmnModeler.on('commandStack.postExecuted', ({ command, context }) => {
if (propagateEvents.includes(command)) {
executionHeap.shift();
} else {
return;
}
// check if the event was triggered by the local user or was trigerred by the event distribution system
let wasInjected = context.isExternalEvent;
if (executionHeap.length > 0) {
wasInjected = wasInjected || executionHeap[executionHeap.length - 1].context.isExternalEvent;
}
//check for an implicit root change and store it
if (command === 'canvas.updateRoot' && executionHeap.length > 0 && !wasInjected) {
implicitRootChange = context;
}
// if the event was neither triggered by an outside event nor by another event we already sent, send it
// this check can be overridden by setting a flag in the event
if ((!wasInjected && !executionHeap.length) || context.forceDistribution) {
if (subprocessId) {
context.parent = { id: subprocessId };
context.newParent = { id: subprocessId };
context.changedBySubprocessId = subprocessId;
context.hints
? (context.hints = { ...context.hints, autoResize: false })
: (context.hints = { autoResize: false });
}
// transform the context into a reproducable form
const information = preSerialize(command, context);
//send command and context
processInterface.broadcastBPMNEvents(processDefinitionsId, command, information);
// the last particpant of the root collaboration was removed, make the other clients change their root
// according to how it was done locally (use same id for new root Process)
if (implicitRootChange) {
processInterface.broadcastBPMNEvents(
processDefinitionsId,
'canvas.updateRoot',
preSerialize('canvas.updateRoot', implicitRootChange)
);
implicitRootChange = null;
}
}
});
}
// stores the process that was the root before we made the root a collaboration
// allows us to reuse it when creating the first participant
let oldRootProcess;
/**
* Use some modules from the modeler to trigger similar events to the ones coming from other machines inside the local modeler
*
* @param {String} command the event that is supposed to be applied
* @param {Object} context information about the current state and the expected changes
*/
export function applyExternalEvent(command, context) {
if (!elementRegistry || !cli || !modeling) {
return;
}
// transform the context so we can use it to trigger the event in the local modeler
context = parse(
elementRegistry,
elementFactory,
bpmnFactory,
command,
JSON.parse(JSON.stringify(context))
);
if (subprocessId && subprocessId !== context.changedBySubprocessId) {
const subprocessInProcess = elementRegistry.get(context.changedBySubprocessId);
if (!subprocessInProcess) {
return;
}
}
// store root process before changing to collaboration to use it in the first particpant that will be created
if (command === 'canvas.updateRoot') {
const root = canvas.getRootElement();
if (root.type === 'bpmn:Process') {
oldRootProcess = root;
}
}
// on creating the first participant: set its referenced process to the one that was previously the root process
if (command === 'shape.create' && context.shape.type === 'bpmn:Participant' && oldRootProcess) {
context.shape.businessObject.processRef = oldRootProcess.businessObject;
oldRootProcess = null;
}
context.isExternalEvent = true; // mark the event as one that comes from another machine
commandStack.execute(command, context); // trigger the event in the local modeler
// workarounds to correct implicitly created objects after the event finished
// on creating a dataObjectReference a dataObject is implicitly created => set dataObject with correct id
if (command === 'shape.create' && context.dataObjectId) {
commandStack.execute('element.updateModdleProperties', {
element: context.shape,
moddleElement: context.shape.businessObject.dataObjectRef,
properties: { id: context.dataObjectId },
isExternalEvent: true,
});
}
// make sure the eventDefinition has the correct id after it was created
if ((command === 'shape.create' || command === 'shape.replace') && context.eventDefinitionId) {
let shape = context.shape;
if (context.newShape) {
shape = context.newShape;
}
commandStack.execute('element.updateModdleProperties', {
element: shape,
moddleElement: shape.businessObject.eventDefinitions[0],
properties: { id: context.eventDefinitionId },
isExternalEvent: true,
});
}
// make sure the id of the property the dataInputAssociation points to is correct
if (command === 'connection.create' && context.targetPropertyIds) {
const localProperties = context.target.businessObject.properties;
// the property ids of the local instance of the connection target
const localPropertyIds = localProperties.map((property) => property.id);
// find the id the new property is supposed to have
const newId = context.targetPropertyIds.find((id) => !localPropertyIds.includes(id));
if (!newId) {
return;
}
// find the id the local porperty got when it was created
const tmpId = localPropertyIds.find((id) => !context.targetPropertyIds.includes(id));
const newProperty = localProperties.find((property) => property.id === tmpId);
commandStack.execute('element.updateModdleProperties', {
element: context.target,
moddleElement: newProperty,
properties: { id: newId },
isExternalEvent: true,
});
}
// make sure that a implicitly created laneset containing the new lane has the correct id
if (command === 'shape.create' && context.shape.type === 'bpmn:Lane') {
context.shape.businessObject.$parent.set('id', context.laneSetId);
}
// replacing some activity with an collapsed/expanded subprocess implicitly creates a startEvent inside the subprocess
// => make sure that this startEvent has the correct id
if (command === 'shape.replace' && context.subProcessStartEventId) {
if (
context.newShape.children.length === 1 &&
context.newShape.children[0].type === 'bpmn:StartEvent'
) {
commandStack.execute('element.updateProperties', {
element: context.newShape.children[0],
properties: { id: context.subProcessStartEventId },
isExternalEvent: true,
});
}
}
}