Source: management-system/src/frontend/helpers/bpmn-modeler-events/custom-modeler-commands.js

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 },
  });
}