Source: management-system/src/frontend/views/ProcessBpmnEditor.vue

<template>
  <div
    class="flex-container"
    :style="{
      '--non-selectable-colors': nonExecutableColors,
    }"
  >
    <v-toolbar class="flex-toolbar">
      <v-toolbar-title>
        <v-form v-if="isEditingProcessName" ref="process-name">
          <v-text-field
            class="mt-3"
            id="name-input"
            ref="name-input"
            v-model="newName"
            :rules="[nameRules.required, nameRules.counter]"
            counter="150"
            dense
            autofocus
            @keydown.enter.stop=""
            @keyup.enter.stop="saveName"
            @blur.stop="saveName"
          />
        </v-form>
        <div v-else>
          <span v-if="name.length < 20">
            {{ name }}
          </span>
          <v-tooltip v-else bottom>
            <template v-slot:activator="{ on }">
              <span v-on="on">{{ name.substr(0, 20) }}...</span>
            </template>
            <span> {{ name }}</span>
          </v-tooltip>
          <v-btn icon @click="showEditProcessName" v-if="$can('update', process)">
            <v-icon>mdi-pencil</v-icon>
          </v-btn>
        </div>
      </v-toolbar-title>
      <v-spacer />
      <v-menu v-if="!isHtmlFormDialogVisible && $vuetify.breakpoint.smAndDown" bottom left>
        <template v-slot:activator="{ on, attrs }">
          <v-btn icon v-bind="attrs" v-on="on">
            <v-icon>mdi-dots-vertical</v-icon>
          </v-btn>
        </template>

        <v-list>
          <v-list-item v-if="process.shared" @click="clipUrl">
            <v-list-item-title>Share</v-list-item-title>
          </v-list-item>
          <v-list-item v-if="!isXmlDialogVisible" @click="toggleXMLViewer">
            <v-list-item-title>Show XML</v-list-item-title>
          </v-list-item>
          <v-list-item
            v-else-if="isXmlDialogVisible && (!xmlChanged || disableEditing)"
            @click="toggleXMLViewer"
          >
            <v-list-item-title>Hide XML</v-list-item-title>
          </v-list-item>
          <v-list-item
            v-else-if="isXmlDialogVisible && xmlChanged && !disableEditing"
            @click="saveXmlAndClose"
          >
            <v-list-item-title>Save XML</v-list-item-title>
          </v-list-item>
          <v-list-item v-if="!areNonExecutableElementsHidden" @click="hideNonExecutableElements">
            <v-list-item-title>Hide non-executable elements</v-list-item-title>
          </v-list-item>
          <v-list-item v-if="areNonExecutableElementsHidden" @click="showNonExecutableElements">
            <v-list-item-title>Show non-executable elements</v-list-item-title>
          </v-list-item>
          <v-list-item @click="showExportDialog">
            <v-list-item-title>Export</v-list-item-title>
          </v-list-item>
        </v-list>
      </v-menu>
      <div v-if="!isHtmlFormDialogVisible && $vuetify.breakpoint.mdAndUp">
        <v-row no-gutters>
          <v-col v-if="process.shared">
            <v-btn @click="clipUrl">Share</v-btn>
          </v-col>
          <v-col>
            <v-btn v-if="!isXmlDialogVisible" @click="toggleXMLViewer">Show XML</v-btn>
            <v-btn
              v-else-if="isXmlDialogVisible && (!xmlChanged || disableEditing)"
              @click="toggleXMLViewer"
              >Hide XML</v-btn
            >
            <v-btn
              v-else-if="isXmlDialogVisible && xmlChanged && !disableEditing"
              @click="saveXmlAndClose"
              >Save XML</v-btn
            >
          </v-col>
          <v-col>
            <v-tooltip bottom v-if="!areNonExecutableElementsHidden">
              <template v-slot:activator="{ on }">
                <v-btn v-on="on" @click="hideNonExecutableElements"
                  >Hide non-executable elements</v-btn
                >
              </template>
              <span>{{ nonExecutableElementsExplanation }}</span>
            </v-tooltip>
            <v-tooltip bottom v-if="areNonExecutableElementsHidden">
              <template v-slot:activator="{ on }">
                <v-btn v-on="on" @click="showNonExecutableElements"
                  >Show non-executable elements</v-btn
                >
              </template>
              <span>{{ nonExecutableElementsExplanation }}</span>
            </v-tooltip>
          </v-col>
          <v-col>
            <v-btn @click="showExportDialog">Export</v-btn>
          </v-col>
        </v-row>
      </div>
    </v-toolbar>

    <div class="flex-content">
      <div v-if="!disableEditing" class="task-edit-button-wrapper">
        <div v-if="showAdditionalOptions && $can('update', process)">
          <v-btn
            v-if="selectedElement.type === 'bpmn:ScriptTask'"
            color="primary"
            @click="openScriptDialog"
          >
            Edit Script
          </v-btn>
          <v-btn
            v-if="taskHtml || metaData['_5i-Inspection-Order-ID']"
            color="primary"
            @click="openHtmlForm"
            >Edit User Task Form</v-btn
          >

          <v-btn v-if="timeDuration !== undefined" color="primary" @click="openDialog('TimerEvent')"
            >Edit Timer Event</v-btn
          >
          <v-btn v-if="canHaveCondition" color="primary" @click="openDefaultFlowDialog"
            >Make default flow</v-btn
          >
          <v-btn v-if="canHaveCondition" color="primary" @click="openConditionDialog"
            >Edit Condition</v-btn
          >
          <v-btn v-if="canHaveConstraints" color="primary" @click="openTaskConstraintEditor"
            >Edit Task Constraints</v-btn
          >
          <v-btn
            v-if="selectedElement.type === 'bpmn:CallActivity'"
            color="primary"
            @click="openDialog('CallActivity')"
            >Select Process</v-btn
          >
          <v-btn
            v-if="
              selectedElement.type === 'bpmn:CallActivity' &&
              selectedCallActivityProcessDefinitionsId
            "
            color="primary"
            @click="openSubprocessModeler"
            >Open Call Activity in Editor</v-btn
          >

          <v-btn
            v-if="selectedElement.type === 'bpmn:SubProcess'"
            color="primary"
            @click="openSubprocessModeler"
            >Edit SubProcess</v-btn
          >
        </div>
        <div v-if="$can('update', process)">
          <v-btn v-if="isSubprocess" color="primary" @click="openTaskConstraintEditor"
            >Edit Task Constraints for Subprocess</v-btn
          >
          <v-btn v-else color="primary" @click="openDialog('ProcessConstraints')"
            >Edit Process Constraints</v-btn
          >
        </div>
      </div>
      <div
        class="history-mode-overlay"
        style="background-color: transparent"
        v-if="$can('update', process)"
      >
        <v-btn v-if="isSaveMessageVisible" text small color="green">
          Changes saved
          <v-icon right small>mdi-check</v-icon>
        </v-btn>
        <v-icon v-if="isElectron" @click="modeler.get('commandStack').undo()">mdi-undo</v-icon>
        <v-icon v-if="isElectron" @click="modeler.get('commandStack').redo()">mdi-redo</v-icon>
      </div>
      <v-tooltip left>
        <template v-slot:activator="{ on, attrs }">
          <v-btn
            class="properties-toggle"
            icon
            @click="togglePropertiesPanel"
            v-bind="attrs"
            v-on="on"
          >
            <v-icon v-if="isPropertiesPanelVisible">mdi-chevron-right</v-icon>
            <v-icon v-else>mdi-chevron-left</v-icon>
          </v-btn>
        </template>
        <span v-if="isPropertiesPanelVisible">Close Properties Panel</span>
        <span v-else>Open Properties Panel</span>
      </v-tooltip>
      <PropertiesPanel
        v-if="selectedElement"
        :id="propertiesID"
        :ref="propertiesID"
        :modeler="modeler"
        :element="selectedElement"
        :meta="metaData"
        :rootMeta="rootMetaData"
        :initialMilestones="milestones"
        :initialResources="resources"
        :initialLocations="locations"
        :documentation="elementDocumentation"
        :fill-color="elementFillColor"
        :stroke-color="elementStrokeColor"
        :processType="process.type"
        class="properties-panel"
        :style="{
          display: isPropertiesPanelVisible ? 'block' : 'none',
        }"
        :disableEditing="disableEditing"
        @change="updateMetaData"
        @changeExternal="changeTaskExternal"
        @saveDocumentation="updateDocumentation"
        @saveBackgroundColor="updateBackgroundColor"
        @saveStrokeColor="updateStrokeColor"
        @saveMilestones="updateMilestones($event.milestones, $event.element)"
        @saveLocations="updateLocations($event.locations, $event.element)"
        @saveResources="updateResources($event.resources, $event.element)"
      />
      <div :id="canvasID" class="diagram-container" />
    </div>
    <v-btn class="loading-message" v-if="loading" :loading="loading" text color="green">
      <template v-slot:loader>
        <span>BPMN Diagram is Loading...</span>
      </template>
    </v-btn>
    <VariableToolbar
      :processVariables="process.variables"
      :key="editVariableIndex"
      @add="openDialog('AddVariable')"
      @edit="editVariable"
    />

    <popup :popupData="popupData" @close="resetDialog = true" />
    <popup :popupData="xmlWarningData" :centered="false" />
    <popup :popupData="connectionWarningData" :centered="false" />

    <confirmation
      title="this flow the default. This will erase its condition!"
      text="Do you want to continue?"
      :show="isDefaultFlowDialogVisible"
      maxWidth="390px"
      @cancel="closeDialog('DefaultFlow')"
      @continue="makeFlowDefault"
    />

    <resizable-window v-show="isXmlDialogVisible" title="BPMN XML" :canvasID="canvasID">
      <v-card style="height: calc(100% - 48px)">
        <div style="height: 100%" id="xml-container" @mousedown.stop @keydown="xmlChanged = true" />
      </v-card>
    </resizable-window>

    <resizable-window
      fullscreen-only
      v-if="isHtmlFormDialogVisible"
      style="z-index: 999"
      :canvasID="canvasID"
    >
      <FormBuilder
        :content="taskHtml"
        :filename="userTaskFileName"
        :milestonesHtml="milestonesHtml"
        @saveHTML="saveHTML"
        @close="closeUserTaskForm"
      />
    </resizable-window>

    <ProcessConstraintsModal
      :id="process.id"
      :showDialog="isProcessConstraintsDialogVisible"
      :elementConstraintMapping="elementConstraintMapping"
      :elementId="selectedElementId"
      @close="closeDialog('ProcessConstraints')"
      @update="addConstraints"
    />

    <v-dialog eager fullscreen v-model="isScriptDialogVisible">
      <ScriptingIde
        :isOpen="isScriptDialogVisible"
        :selected-element="selectedElement"
        :process-definitions-id="processDefinitionsId"
        @close="closeScriptEditor"
        @updated="saveScript($event)"
        :key="process.id"
      />
    </v-dialog>

    <!-- TODO: see if style still works-->
    <confirmation
      id="revertDialog"
      title="revert your changes? This action cannot be undone"
      text="Do you want to continue?"
      :show="resetDialog"
      maxWidth="390px"
      @cancel="resetDialog = false"
      @continue="reset"
    />

    <VariableForm
      :show="isAddVariableDialogVisible"
      @cancel="closeDialog('AddVariable')"
      @add="addVariable"
    />

    <VariableForm
      :variable="editingVariable"
      :deleteOption="true"
      :show="isEditVariableDialogVisible"
      @cancel="closeVariableEditor"
      @update="updateVariable"
      @delete="deleteVariable"
    />

    <DurationForm
      title="Timer Event Duration"
      :show="isTimerEventDialogVisible"
      :formalExpression="timeDuration"
      @save="saveTimerEventInfo"
      @close="isTimerEventDialogVisible = false"
    />

    <ConditionForm
      :show="isConditionDialogVisible"
      :selected-element="selectedElement"
      @close="isConditionDialogVisible = false"
      @save="saveGatewayCondition"
    />

    <subprocess-selection
      :process="process"
      :currentlySelectedId="selectedCallActivityProcessDefinitionsId"
      :show="isCallActivityDialogVisible"
      @cancel="closeDialog('CallActivity')"
      @process="setCallActivity"
      @done="closeDialog('CallActivity')"
    />

    <ExportModal
      :loading="exportRunning"
      title="Export selected process"
      text="Please select a format for the file export"
      error="Please select a format"
      :max="max"
      :show="exportSelectedProcessesDialog"
      @cancel="exportSelectedProcessesDialog = false"
      @continue="exportSelected"
    />

    <ConstraintEditor
      :show="isEditConstraintDialogVisible"
      title="Task Constraints"
      level="task"
      :elementId="selectedElementId"
      :elementConstraintMapping="elementConstraintMapping"
      @save="addConstraints"
      @close="closeTaskConstraintsDialog"
    />
  </div>
</template>

<script>
import uuid from 'uuid';
import Modeler from 'bpmn-js/lib/Modeler';
import NavigatedViewer from 'bpmn-js/dist/bpmn-navigated-viewer.production.min';
import ViewerPassiveModelingExtensionsModule from '@/frontend/helpers/override-modules/viewer-passive-modeling-extensions.js';
import Ids from 'ids';

import proceedModdleExtension from '@proceed/bpmn-helper/customSchema.json';

import CliModule from 'bpmn-js-cli/lib';
import { is } from 'bpmn-js/lib/util/ModelUtil';

import * as monaco from 'monaco-editor';
import CustomRules from '@/frontend/helpers/override-modules/custom-rules.js';
import DisableKeyboardBinding from '@/frontend/helpers/override-modules/custom-keyboard-bindings.js';
import CustomPopUpMenuProvider from '@/frontend/helpers/override-modules/custom-popup-menu-provider.js';

import {
  getTaskConstraintMapping,
  getProcessConstraints,
  ensureCorrectProceedNamespace,
  generateUserTaskFileName,
  generateBpmnId,
  addSubprocessContentToProcessXML,
} from '@proceed/bpmn-helper';
import { defaultHtml, defaultCss } from '@/frontend/assets/user-task.js';
import { exportSelectedProcesses } from '@/frontend/helpers/process-export/process-export.js';
import { getMaximumResolution } from '@/frontend/helpers/process-export/process-max-resolution.js';

import ScriptingIde from '@/frontend/components/scripting-ide/ScriptingIde.vue';
import ProcessConstraintsModal from '@/frontend/components/constraints/ProcessConstraintsModal.vue';
import ConstraintEditor from '@/frontend/components/constraints/ConstraintEditor.vue';
import ResizableWindow from '@/frontend/components/resizable-window/ResizableWindow.vue';
import AlertWindow from '@/frontend/components/universal/Alert.vue';
import Confirmation from '@/frontend/components/universal/Confirmation.vue';
import FormBuilder from '@/frontend/components/form-builder/FormBuilder.vue';
import VariableForm from '@/frontend/components/processes/editor/VariableForm.vue';
import VariableToolbar from '@/frontend/components/processes/editor/VariableToolbar.vue';
import DurationForm from '@/frontend/components/processes/editor/DurationForm.vue';
import ConditionForm from '@/frontend/components/processes/editor/ConditionForm.vue';
import ExportModal from '@/frontend/components/processes/ExportModal.vue';
import SubprocessSelection from '@/frontend/components/processes/editor/SubprocessSelection.vue';
import PropertiesPanel from '@/frontend/components/processes/editor/PropertiesPanel.vue';
import { getUpdatedTaskConstraintMapping } from '@/frontend/helpers/usertask-helper.js';
import { getBusinessObject } from 'bpmn-js/lib/util/ModelUtil';
import { getFillColor, getStrokeColor } from 'bpmn-js/lib/draw/BpmnRenderUtil';
import { subject } from '@casl/ability';

import { eventHandler } from '@/frontend/backend-api/index.js';

import * as customCommands from '@/frontend/helpers/bpmn-modeler-events/custom-modeler-commands.js';
import * as eventDistribution from '@/frontend/helpers/bpmn-modeler-events/event-distribution.js';

import { getProcessHierarchy } from '@/shared-frontend-backend/helpers/process-hierarchy.js';

import { getEnclosingProcess } from '@/frontend/helpers/5thIndustry-helper.js';

import hiddenElements from '@/frontend/assets/styles/hidden-non-executable-element.lazy.css';

/**
 * @module views
 */
/**
 * Main Component for Manipulating the BPMN processes
 *
 * @memberof module:views
 * @module Vue:ProcessBpmnEditor
 *
 * @vue-computed config
 * @vue-computed isElectron
 * @vue-computed isPropertiesPanelVisible
 * @vue-computed xml
 * @vue-computed name
 * @vue-computed process
 * @vue-computed taskHtmlMapping
 * @vue-computed editingVariable
 * @vue-computed selectedElementId
 * @vue-computed canHaveConstraints
 * @vue-computed showAdditionalOptions
 * @vue-computed canHaveCondition
 * @vue-computed taskHtml
 */
export default {
  name: 'process-bpmn-editor',
  components: {
    ConstraintEditor,
    ScriptingIde,
    ResizableWindow,
    ProcessConstraintsModal,
    VariableForm,
    FormBuilder,
    popup: AlertWindow,
    Confirmation,
    VariableToolbar,
    ExportModal,
    SubprocessSelection,
    PropertiesPanel,
    DurationForm,
    ConditionForm,
  },
  props: {
    processDefinitionsId: {
      type: String,
      required: true,
    },
    subprocessId: {
      type: String,
      required: false,
    },
  },
  computed: {
    isSubprocess() {
      return !!this.subprocessId;
    },
    config() {
      return this.$store.getters['configStore/config'];
    },
    isElectron() {
      return process.env.IS_ELECTRON;
    },
    isPropertiesPanelVisible: {
      get() {
        return this.$store.getters['userPreferencesStore/getPropertiesPanelVisibility'];
      },
      set(newValue) {
        this.$store.dispatch('userPreferencesStore/setPropertiesPanelVisibility', newValue);
      },
    },
    xml() {
      return this.$store.getters['processEditorStore/xml'];
    },
    name() {
      const name = this.subprocessId
        ? this.subprocessId
        : this.$store.getters['processEditorStore/processName'];

      // prevent error that occurs when name isn't already set in processEditorStore
      return name ? name : 'Loading...';
    },
    process() {
      const process = this.$store.getters['processStore/processById'](this.processDefinitionsId);

      if (this.subprocessId) {
        const subprocess = { ...process, subprocessId: this.subprocessId };
        return subprocess;
      }
      const type = process.type[0].toUpperCase() + process.type.slice(1);
      subject(type, process);
      return process;
    },
    taskHtmlMapping() {
      return this.$store.getters['processEditorStore/taskHtmlMapping'];
    },
    editingVariable() {
      return this.process.variables[this.editVariableIndex];
    },
    selectedElementId() {
      return this.selectedElement ? this.selectedElement.id : undefined;
    },
    nonExecutableColors() {
      const config = this.$store.getters['userPreferencesStore/getUserConfig'];

      if (!config.highlightNonExecutableElements) {
        return 'black';
      }

      return config.nonExecutableElementsColor;
    },
    /**
     * returns true if the selected element can have constraints
     */
    canHaveConstraints() {
      if (this.selectedElement && this.selectedElement.type) {
        const elements = [
          new RegExp('Task'),
          new RegExp('SubProcess'),
          new RegExp('CallActivity'),
          new RegExp('Gateway'),
          new RegExp('Event'),
          new RegExp('Process'),
        ];
        if (elements.some((expression) => expression.test(this.selectedElement.type))) {
          return true;
        }
      }

      return false;
    },
    /**
     * Checks if some element was selected that needs additional option buttons displayed
     */
    showAdditionalOptions() {
      if (this.selectedElement && this.selectedElement.type) {
        const elements = [
          new RegExp('Task'),
          new RegExp('SubProcess'),
          new RegExp('CallActivity'),
          new RegExp('Gateway'),
          new RegExp('Event'),
          new RegExp('SequenceFlow'),
        ];
        if (elements.some((expression) => expression.test(this.selectedElement.type))) {
          return true;
        }
      }

      return false;
    },
    /**
     * Checks if the selected element can have a condition added to it (sequence flow after a OR/XOR split)
     */
    canHaveCondition() {
      if (this.selectedElement && this.selectedElement.type === 'bpmn:SequenceFlow') {
        // check if the sequence flow is coming from an XOR or OR Gateway
        const { source } = this.selectedElement;
        if (
          (source.type === 'bpmn:ExclusiveGateway' || source.type === 'bpmn:InclusiveGateway') &&
          (!source.businessObject.default ||
            source.businessObject.default.id !== this.selectedElement.id)
        ) {
          return true;
        }
      }

      return false;
    },
    taskHtml() {
      if (this.userTaskFileName) {
        const html = this.taskHtmlMapping[this.userTaskFileName];
        return this.removeMilestoneTemplateNotation(html);
      }

      return undefined;
    },
    milestonesHtml() {
      const allMilestonesHTML = this.milestones.reduce((acc, milestone) => {
        const milestoneHTML = `
        <div>
          <label>
            Milestone ID: ${milestone.id} | Name: ${milestone.name} | Description: ${milestone.description}
            <input type="range" min="0" max="100" value="0" class="milestone-${milestone.id}" oninput="this.nextElementSibling.value = this.value + '%'">
            <output name="fulfillment_${milestone.id}">0%</output>
          </label>
        </div>`;
        return acc.concat(milestoneHTML);
      }, '');

      return `<p>Update your Milestones:</p> ${allMilestonesHTML}`;
    },
  },
  data() {
    return {
      /** */
      nameRules: {
        required: (n) => !!n || 'Name is required',
        counter: (n) => n.length <= 150 || 'Name should not be greater than 150 characters',
      },
      /** */
      newName: '',
      /** */
      canvasID: 'canvas_' + uuid.v4(),
      /** */
      propertiesID: 'properties_' + uuid.v4(),
      /** */
      isXmlDialogVisible: false,
      /** */
      xmlViewer: null,
      /** */
      isAddVariableDialogVisible: false,
      /** */
      isEditVariableDialogVisible: false,
      /** */
      editVariableIndex: -1,
      /** */
      modeler: null,
      /** */
      isProcessConstraintsDialogVisible: false,
      /** */
      isDefaultFlowDialogVisible: false,
      /** */
      isHtmlFormDialogVisible: false,
      /** */
      isEditConstraintDialogVisible: false,
      /** */
      isTimerEventDialogVisible: false,
      /** */
      isConditionDialogVisible: false,
      /** */
      isEditingProcessName: false,
      /** */
      timeout: null,
      /** */
      resetDialog: false,
      /** */
      confirm: false,
      /** */
      oldXml: null,
      /** */
      popupData: {
        body: '',
        display: 'none',
        color: 'error',
      },
      /** */
      xmlWarningData: {
        body: 'This process is being edited by multiple users, editing the xml might overwrite other users changes',
        display: 'none',
        color: 'error',
      },
      /** */
      connectionWarningData: {
        body: '',
        display: 'none',
        color: 'error',
      },
      /** */
      xmlChanged: false,
      /** */
      downloadProcessDialog: false,
      /** */
      selectedExtension: '',
      /** */
      exportSelectedProcessesDialog: false,
      /** */
      exportRunning: false,
      /** */
      selectedElement: null,
      /** */
      elementConstraintMapping: {},
      /** */
      metaData: {},
      /** */
      rootMetaData: {},
      /** */
      milestones: [],
      /** */
      locations: {},
      /** */
      resources: {},
      /** */
      elementDocumentation: '',
      /** */
      elementFillColor: '#FFFFFFFF',
      /** */
      elementStrokeColor: '#000000FF',
      /** */
      isScriptDialogVisible: false,
      /** */
      isSaveMessageVisible: false,
      /** */
      selectedCallActivityProcessDefinitionsId: null,
      /** */
      isCallActivityDialogVisible: false,
      /** */
      userTaskFileName: '',
      /** */
      areNonExecutableElementsHidden: false,
      /** */
      incomingLabelChanged: '',
      /** */
      queuedLabelChange: null,
      /** */
      timeDuration: undefined,
      /**
       * eventHandling callbacks
       */
      xmlChangedCallback: null,
      /** */
      bpmnEventCallback: null,
      /** */
      constraintChangedCallback: null,
      /** */
      milestonesChangedCallback: null,
      /** */
      locationsChangedCallback: null,
      /** */
      resourcesChangedCallback: null,
      /** */
      connectionLostCallback: null,
      /** */
      connectionRecoveredCallback: null,
      /** */
      max: 10,
      /** */
      disableEditing: false,
      nonExecutableElementsExplanation:
        'The so called non-executable elements can be used when creating a bpmn diagram, but cannot be executed on the bpmn engine',
      loading: false,
    };
  },
  methods: {
    changeTaskExternal({ task, external }) {
      customCommands.setTaskExternal(task, external);
    },
    /** */
    addConstraints(constraints, element = this.selectedElement, dontPropagate = false) {
      customCommands.addConstraintsToElement(element, constraints, dontPropagate);
    },
    updateMilestones(milestones, element = this.selectedElement, dontPropagate = false) {
      customCommands.addMilestonesToElement(element, milestones, dontPropagate);
    },
    updateLocations(locations, element = this.selectedElement, dontPropagate = false) {
      customCommands.addLocationsToElement(element, locations, dontPropagate);
    },
    updateResources(resources, element = this.selectedElement, dontPropagate = false) {
      customCommands.addResourcesToElement(element, resources, dontPropagate);
    },
    /** */
    togglePropertiesPanel() {
      this.isPropertiesPanelVisible = !this.isPropertiesPanelVisible;
    },
    /** */
    hideNonExecutableElements() {
      hiddenElements.use();
      this.areNonExecutableElementsHidden = true;
    },
    /** */
    showNonExecutableElements() {
      hiddenElements.unuse();
      this.areNonExecutableElementsHidden = false;
    },
    /** */
    async showExportDialog() {
      this.max = await getMaximumResolution([this.process]);
      this.exportSelectedProcessesDialog = true;
    },
    /** */
    async reset() {
      await this.saveXml(this.oldXml);
      this.resetDialog = false;
    },
    /** */
    async saveScript({ elementId, script }) {
      customCommands.addJSToElement(elementId, script);
    },
    /** */
    saveGatewayCondition(condition) {
      this.$store.dispatch('processStore/stopEditingTask', {
        taskId: this.selectedElementId,
        processDefinitionsId: this.processDefinitionsId,
      });
      customCommands.addJSToElement(this.selectedElement.id, condition.script);
      this.closeDialog('Condition');
    },
    /** */
    closeUserTaskForm() {
      this.$store.dispatch('processStore/stopEditingTask', {
        taskId: this.selectedElementId,
        processDefinitionsId: this.processDefinitionsId,
      });
      this.closeDialog('HtmlForm');
    },
    /** */
    closeScriptEditor() {
      this.$store.dispatch('processStore/stopEditingTask', {
        taskId: this.selectedElementId,
        processDefinitionsId: this.processDefinitionsId,
      });
      this.closeDialog('Script');
    },
    /** */
    openDefaultFlowDialog() {
      // warn the user if he is about to implicitly delete a condition
      if (
        this.selectedElement.businessObject.conditionExpression &&
        this.selectedElement.businessObject.conditionExpression
      ) {
        this.openDialog('DefaultFlow');
      } else {
        this.makeFlowDefault();
      }
    },
    /** */
    openDialog(dialogName) {
      this.modeler.get('keyboard').unbind();
      this[`is${dialogName}DialogVisible`] = true;
    },
    /** */
    closeDialog(dialogName) {
      this.modeler.get('keyboard').bind(document);
      this[`is${dialogName}DialogVisible`] = false;
    },
    /** */
    openHtmlForm() {
      this.checkIfTaskBlocked();

      if (
        this.selectedElement &&
        this.selectedElement.businessObject.implementation === '5thIndustry'
      ) {
        const planId = this.rootMetaData['_5i-Inspection-Plan-ID'];
        const templateId = this.rootMetaData['_5i-Inspection-Plan-Template-ID'];
        const applicationAddress = this.rootMetaData['_5i-Application-Address'];
        if (applicationAddress) {
          if (planId) {
            window.open(`${applicationAddress}/plans/edit/${planId}`);
          } else if (templateId) {
            window.open(`${applicationAddress}/planTemplates/edit/${templateId}`);
          }
        }
      } else {
        this.openDialog('HtmlForm');
      }
    },
    /** */
    openTaskConstraintEditor() {
      this.checkIfTaskBlocked();
      this.openDialog('EditConstraint');
    },
    /** */
    openConditionDialog() {
      this.checkIfTaskBlocked();
      this.openDialog('Condition');
    },
    /** */
    closeTaskConstraintsDialog() {
      this.$store.dispatch('processStore/stopEditingTask', {
        taskId: this.selectedElementId,
        processDefinitionsId: this.processDefinitionsId,
      });
      this.closeDialog('EditConstraint');
    },
    /** */
    openCallAcvitiyDialog() {
      this.showCallAcvitiyDialog = true;
    },
    /** */
    openSubprocessModeler() {
      if (this.selectedCallActivityProcessDefinitionsId) {
        this.$emit('addTab', this.selectedCallActivityProcessDefinitionsId);
      } else {
        this.$emit('addTab', this.processDefinitionsId, this.selectedElementId);
      }
    },
    /** */
    async setCallActivity(process) {
      if (process) {
        const calledBpmn = await this.$store.getters['processStore/xmlById'](process.id);
        await customCommands.addCallActivityReference(
          this.selectedElementId,
          calledBpmn,
          process.id
        );
      } else {
        customCommands.removeCallActivityReference(this.selectedElementId);
      }
      this.selectedCallActivityProcessDefinitionsId = process && process.id;
    },
    /** */
    saveXmlAndClose() {
      this.saveXmlFromXmlViewer();
      this.closeDialog('Xml');
      this.xmlChanged = false;
    },
    /** */
    toggleXMLViewer() {
      if (!this.isXmlDialogVisible) {
        this.showXml();
      } else {
        this.closeDialog('Xml');
      }
    },
    /** */
    showXml() {
      this.oldXml = this.xml;
      const contentWindow = document.getElementById('content');
      contentWindow.style.overflowX = 'hidden';
      if (this.process.inEditingBy && this.process.inEditingBy.length > 1) {
        this.xmlWarningData.display = 'block';
      }

      this.openDialog('Xml');
    },
    /** */
    checkIfTaskBlocked() {
      this.$store.dispatch('processStore/startEditingTask', {
        taskId: this.selectedElementId,
        processDefinitionsId: this.processDefinitionsId,
      });
    },
    addMilestoneTemplateNotation(html) {
      const parser = new DOMParser();
      const serializer = new XMLSerializer();

      const userTaskDocument = parser.parseFromString(html, 'text/html');

      const milestoneInputs = userTaskDocument.querySelectorAll('input[class^="milestone-"]');
      Array.from(milestoneInputs).forEach((milestoneInput) => {
        const milestoneName = Array.from(milestoneInput.classList)
          .find((className) => className.includes('milestone-'))
          .split('milestone-')
          .slice(1)
          .join('');

        const label = `{if ${milestoneName}}{${milestoneName}}%{else}0%{/if}`;
        milestoneInput.setAttribute('value', `{if ${milestoneName}}{${milestoneName}}{else}0{/if}`);
        milestoneInput.nextElementSibling.innerHTML = label;
      });

      return serializer.serializeToString(userTaskDocument);
    },
    removeMilestoneTemplateNotation(html) {
      const parser = new DOMParser();
      const serializer = new XMLSerializer();

      const userTaskDocument = parser.parseFromString(html, 'text/html');

      const milestoneInputs = userTaskDocument.querySelectorAll('input[class^="milestone-"]');
      Array.from(milestoneInputs).forEach((milestoneInput) => {
        milestoneInput.setAttribute('value', '0');
        milestoneInput.nextElementSibling.innerHTML = '0%';
      });

      return serializer.serializeToString(userTaskDocument);
    },
    /** */
    saveHTML(htmlObject, taskId = this.selectedElementId) {
      const html = this.addMilestoneTemplateNotation(
        `<html><head><style>${htmlObject.css}</style></head> <body>${htmlObject.html}</body> </html>`
      );
      const fileName = this.userTaskFileName;

      if (!fileName) {
        throw new Error('No user task fileName to store html with!');
      }

      this.$store.dispatch('processStore/saveUserTask', {
        processDefinitionsId: this.process.id,
        html: html,
        taskFileName: fileName,
      });

      this.$store.dispatch('processEditorStore/setTaskHtmlMapping', {
        elementID: this.userTaskFileName,
        html,
      });

      this.addConstraints(
        getUpdatedTaskConstraintMapping(this.elementConstraintMapping[taskId], html),
        this.modeler.get('elementRegistry').get(taskId)
      );
    },
    /** */
    async saveName() {
      if (this.$refs['process-name'].validate()) {
        customCommands.setName(this.newName);

        const nameInput = this.$refs['name-input'];
        if (nameInput) nameInput.blur();

        this.newName = '';
      }

      this.isEditingProcessName = false;
    },
    /** */
    showEditProcessName() {
      this.isEditingProcessName = true;
      this.newName = this.name;
    },
    /** */
    async saveVariables(variables) {
      await this.$store.dispatch('processStore/update', {
        id: this.process.id,
        changes: { variables },
      });
      this.showSaveMessage();
    },
    /** */
    async addVariable(newVar) {
      const newVars = this.process.variables;
      if (!newVars.some((variable) => variable.name === newVar.name)) {
        newVars.push(newVar);
        await this.saveVariables(newVars);
        this.closeDialog('AddVariable');
      }
    },
    /** */
    async editVariable(tab) {
      await this.$nextTick();
      this.editVariableIndex = tab;
      this.openDialog('EditVariable');
    },
    /** */
    async deleteVariable() {
      const newVars = this.process.variables.filter(
        (variable) => variable.name != this.process.variables[this.editVariableIndex].name
      );
      await this.saveVariables(newVars);
      this.closeVariableEditor();
    },
    /** */
    async updateVariable(updateVar) {
      this.process.variables[this.editVariableIndex] = updateVar;
      await this.saveVariables();
      this.closeVariableEditor();
    },
    /** */
    closeVariableEditor() {
      this.editVariableIndex = -1;
      this.closeDialog('EditVariable');
    },
    /** */
    openPopup() {
      if (!this.resetDialog) {
        this.popupData.display = 'block';
      }
    },
    /** */
    openScriptDialog() {
      this.checkIfTaskBlocked();
      this.openDialog('Script');
    },
    /** */
    async setupModeler() {
      let xml;
      if (this.$can('update', this.process)) {
        this.modeler = new Modeler({
          container: '#' + this.canvasID,
          keyboard: { bindTo: document },

          additionalModules: [
            CliModule,
            CustomRules,
            DisableKeyboardBinding,
            CustomPopUpMenuProvider,
          ],
          moddleExtensions: {
            proceed: proceedModdleExtension,
          },
        });

        xml = ensureCorrectProceedNamespace(this.xml);
        await this.saveXml(xml);
      } else {
        this.disableEditing = true;

        this.modeler = new NavigatedViewer({
          container: '#' + this.canvasID,
          moddleExtensions: {
            proceed: proceedModdleExtension,
          },
          additionalModules: [ViewerPassiveModelingExtensionsModule, CliModule],
        });

        this.modeler.get('moddle').ids = new Ids([32, 36, 1]);
      }

      try {
        const { warnings } = await this.modeler.importXML(xml || this.xml);
        if (warnings && warnings.length) {
          console.warn(warnings);
        }
        this.loading = false;
      } catch (err) {
        this.popupData.body = '--> Error importing BPMN XML';
        this.popupData.color = 'error';
        this.openPopup();
        return;
      }

      customCommands.registerModeler(this.modeler);
      eventDistribution.registerModeler(this.modeler);
      eventDistribution.setProcessDefinitionsId(this.processDefinitionsId, this.subprocessId);
      eventDistribution.activateDistribution();

      // initializing constraintMapping from xml
      const canvas = this.modeler.get('canvas');
      const elementRegistry = this.modeler.get('elementRegistry');
      const processElement = canvas.getRootElement();
      const processConstraints = await getProcessConstraints(this.xml);
      const taskConstraintMapping = await getTaskConstraintMapping(this.xml);
      taskConstraintMapping[processElement.id] = processConstraints;
      this.elementConstraintMapping = taskConstraintMapping;

      this.modeler.on('commandStack.postExecuted', 0, async ({ command, context }) => {
        // update local representation of the meta data of the selected element
        if (context.element === this.selectedElement) {
          this.metaData = customCommands.getMetaData(this.selectedElement);
        }

        if (command === 'element.updateMetaData') {
          // if the meta data change is on a userTask => update its label
          if (
            !context.isExternalEvent &&
            context.element.type === 'bpmn:UserTask' &&
            context.metaData['_5i-Inspection-Order-Shortdescription']
          ) {
            const commandStack = this.modeler.get('commandStack');
            commandStack.execute('element.updateProperties', {
              element: context.element,
              properties: { name: context.metaData['_5i-Inspection-Order-Shortdescription'] },
            });
          }

          const enclosingProcess = getEnclosingProcess(this.selectedElement);
          if (context.element === enclosingProcess) {
            this.rootMetaData = customCommands.getMetaData(enclosingProcess);
            customCommands.changeUserTasksImplementation(
              this.rootMetaData['_5i-Inspection-Plan-ID'] ||
                this.rootMetaData['_5i-Inspection-Plan-Template-ID']
            );
          }
        }

        if (command === 'element.updateDocumentation' && context.element === this.selectedElement) {
          this.elementDocumentation = customCommands.getDocumentation(this.selectedElement);
        }

        if (
          command === 'element.updateEventDefinition' &&
          context.elementId === this.selectedElementId
        ) {
          if (context.hasOwnProperty('newTimerCondition')) {
            this.timeDuration = context.newTimerCondition.body || '';
          }

          if (command === 'element.updateLabel') {
            const { businessObject } = context.element;
            // if the element is some kind of event
            if (businessObject.eventDefinitions && businessObject.eventDefinitions.length > 0) {
              const [eventDefinition] = businessObject.eventDefinitions;
              if (
                !context.isExternalEvent &&
                (eventDefinition.$type === 'bpmn:ErrorEventDefinition' ||
                  eventDefinition.$type === 'bpmn:EscalationEventDefinition')
              ) {
                // label of error/escalation updated (created/updated/deleted) => update error/escalation ref
                // this happens implicitly before removing/replacing an event => don't need to separatly handle that case
                const refId = generateBpmnId();
                customCommands.updateErrorOrEscalation(
                  businessObject.id,
                  refId,
                  context.newLabel,
                  true
                );
              }
            }
          }
        }
      });

      const eventBus = this.modeler.get('eventBus');

      eventBus.on('commandStack.changed', async () => {
        this.saveXmlFromModeler();
      });

      eventBus.on('commandStack.shape.delete.canExecute', 10000, ({ context }) => {
        const { shape, isExternalEvent } = context;
        // deletion of labels is trigerred by other events that get distributed too => prevent label from being deleted twice
        if (isExternalEvent && shape.includes('_label')) {
          return false;
        }
      });

      // cleanup before removing an element
      eventBus.on('commandStack.shape.delete.preExecute', 10000, ({ context }) => {
        let { shape } = context;

        if (shape.type === 'bpmn:UserTask') {
          if (!context.isExternalEvent) {
            this.$store.dispatch('processStore/deleteUserTask', {
              processDefinitionsId: this.processDefinitionsId,
              taskFileName: shape.businessObject.fileName,
            });
          }
        }
      });

      eventBus.on('commandStack.shape.replace.preExecute', 10000, ({ context }) => {
        const { oldShape } = context;

        if (oldShape.type === 'bpmn:UserTask') {
          this.userTaskFileName = undefined;

          if (context.newData) {
            context.newData.businessObject.fileName = undefined;
          }
        }
      });

      eventBus.on('commandStack.shape.replace.postExecute', async ({ context }) => {
        const { newShape, oldShape } = context;

        // make sure that the label has the id of the element so the id will always be the same on all machines
        // normally it would have id of the type [intermediate-element-id]_label which would differ from machine to machine
        if (newShape.labels && newShape.labels.length) {
          newShape.labels.forEach((label) => {
            label.labelTarget = oldShape;
            label.businessObject = oldShape.businessObject;
          });
          oldShape.labels.forEach((label) => {
            label.labelTarget = newShape;
            label.businessObject = newShape.businessObject;
          });
        }

        // clear commandStack so that these events get distributed
        await new Promise((resolve) => setTimeout(resolve));

        if (newShape.type === 'bpmn:UserTask' && !context.isExternalEvent) {
          if (
            this.rootMetaData['_5i-Inspection-Plan-ID'] ||
            this.rootMetaData['_5i-Inspection-Plan-Template-ID']
          ) {
            customCommands.setUserTaskImplementation(newShape.id, '5thIndustry');
          } else {
            customCommands.setUserTaskFileName(newShape.id, generateUserTaskFileName());
          }
        }

        const { businessObject, id: elementId } = newShape;
        // if the element is some kind of event
        if (businessObject.eventDefinitions && businessObject.eventDefinitions.length > 0) {
          const [eventDefinition] = businessObject.eventDefinitions;
          if (
            !context.isExternalEvent &&
            (eventDefinition.$type === 'bpmn:ErrorEventDefinition' ||
              eventDefinition.$type === 'bpmn:EscalationEventDefinition')
          ) {
            // element became an error or escalation event => create reference to error or escalation based on its name
            const refId = generateBpmnId();
            customCommands.updateErrorOrEscalation(elementId, refId, businessObject.name);
          }
        }
        if (!context.isExternalEvent && oldShape.type === 'bpmn:UserTask') {
          let constraints = this.elementConstraintMapping[oldShape.id];
          if (constraints.hardConstraints) {
            constraints.hardConstraints = constraints.hardConstraints.filter(
              (h) => h.name !== 'machine.online'
            );
            const elementRegistry = this.modeler.get('elementRegistry');
            this.addConstraints(constraints, elementRegistry.get(oldShape.id), true);
          }
        }
      });

      eventBus.on('commandStack.element.updateProperties.postExecute', ({ context }) => {
        const { element, properties, oldProperties, additionalInfo, dontPropagate } = context;
        if (properties && additionalInfo) {
          if (additionalInfo.constraints) {
            this.elementConstraintMapping[element.id] = additionalInfo.constraints;
            if (!dontPropagate) {
              this.$store.dispatch('processStore/updateConstraints', {
                processDefinitionsId: this.process.id,
                elementId: element.id,
                constraints: additionalInfo.constraints,
              });
            }
          } else if (additionalInfo.milestones) {
            this.milestones = additionalInfo.milestones;

            const document = new DOMParser().parseFromString(this.taskHtml, 'text/html');
            const milestonesWrapper = document.querySelectorAll('.milestones-wrapper');

            if (this.milestones.length > 0 && milestonesWrapper.length > 0) {
              milestonesWrapper.forEach(
                (wrapper) => (wrapper.innerHTML = `${this.milestonesHtml}`)
              );
            } else if (this.milestones.length > 0 && milestonesWrapper.length === 0) {
              const submitButton = document.querySelector('form.form [type=submit]');
              submitButton.insertAdjacentHTML(
                'beforebegin',
                `<div class="if91m milestones-wrapper">${this.milestonesHtml}</div>`
              );
            } else {
              milestonesWrapper.forEach((wrapper) => wrapper.remove());
            }

            const usertask = {};
            usertask.html = document.body.innerHTML;
            usertask.css = document.querySelector('style').innerText;
            this.saveHTML(usertask);
            if (!dontPropagate) {
              this.$store.dispatch('processStore/updateMilestones', {
                processDefinitionsId: this.process.id,
                elementId: element.id,
                milestones: additionalInfo.milestones,
              });
            }
          } else if (additionalInfo.resources) {
            this.resources = additionalInfo.resources;

            if (!dontPropagate) {
              this.$store.dispatch('processStore/updateResources', {
                processDefinitionsId: this.process.id,
                elementId: element.id,
                resources: additionalInfo.resources,
              });
            }
          } else if (additionalInfo.locations) {
            this.locations = additionalInfo.locations;

            if (!dontPropagate) {
              this.$store.dispatch('processStore/updateLocations', {
                processDefinitionsId: this.process.id,
                elementId: element.id,
                locations: additionalInfo.locations,
              });
            }
          }
        }

        if (this.selectedElement.type === 'bpmn:CallActivity' && element === this.selectedElement) {
          if (properties.calledElement) {
            this.selectedCallActivityProcessDefinitionsId =
              customCommands.getDefinitionsIdForCallActivity(this.selectedElement.id);
          } else {
            this.selectedCallActivityProcessDefinitionsId = undefined;
          }
        }

        if (element.type === 'bpmn:UserTask' && properties.fileName) {
          this.userTaskFileName = properties.fileName;
          // create new file for newly added filename
          if (properties.fileName !== oldProperties.fileName && !context.isExternalEvent) {
            const code = {};
            code.html = defaultHtml;
            code.css = defaultCss;
            this.saveHTML(code, element.id);
          }
        }
      });

      eventBus.on('commandStack.definitions.updateProperties.postExecute', ({ context }) => {
        const { properties, oldProperties } = context;

        if (properties.name && properties.name !== oldProperties.name) {
          this.$store.dispatch('processEditorStore/setProcessName', {
            name: properties.name,
          });
        }
      });
      const elements = elementRegistry.filter((element) => {
        return !is(element, 'bpmn:Process');
      });
      if (elements.length > 1) {
        canvas.zoom('fit-viewport', 'auto');
      } else {
        canvas.zoom('fit-viewport', 0, 0);
      }

      let el = canvas.getRootElement();

      this.selectedElement = el;

      this.metaData = customCommands.getMetaData(el);
      this.rootMetaData = customCommands.getMetaData(el);
      this.elementDocumentation = customCommands.getDocumentation(el);

      eventBus.on('selection.changed', ({ newSelection }) => {
        this.selectedCallActivityProcessDefinitionsId = null;
        this.selectedElement = null;
        this.userTaskFileName = null;
        this.timeDuration = undefined;

        let selected;
        if (newSelection.length === 1) {
          [selected] = newSelection;
        } else {
          selected = canvas.getRootElement();
        }
        this.selectBpmnElement(selected);
      });
    },
    /** */
    async updateMetaData(info) {
      customCommands.updateMetaData(info.elementId, info.metaData);
      // sync timerevent with duration of element
      if (info.metaData.hasOwnProperty('timePlannedDuration') && this.timeDuration !== undefined) {
        customCommands.updateTimer(info.elementId, info.metaData.timePlannedDuration);
      }
    },
    /** */
    async updateDocumentation(info) {
      customCommands.updateDocumentation(info.elementId, info.documentation);
    },
    /** */
    async updateBackgroundColor(info) {
      const modeling = this.modeler.get('modeling');
      modeling.setColor(info.element, {
        fill: info.color,
      });
    },
    /** */
    async updateStrokeColor(info) {
      const modeling = this.modeler.get('modeling');
      modeling.setColor(info.element, {
        stroke: info.color,
      });
    },
    /** */
    async saveTimerEventInfo(info) {
      customCommands.updateTimer(this.selectedElementId, info.formalExpression);
      // sync duration of element with timerevent
      customCommands.updateMetaData(this.selectedElementId, {
        timePlannedDuration: info.formalExpression,
      });
      this.closeDialog('TimerEvent');
    },
    /** */
    async selectBpmnElement(element) {
      this.selectedElement = element;

      if (element) {
        this.resources = customCommands.getResources(element);
        this.locations = customCommands.getLocations(element);
      }

      if (element && element.type === 'bpmn:CallActivity') {
        this.selectedCallActivityProcessDefinitionsId =
          customCommands.getDefinitionsIdForCallActivity(element.id);
      }

      if (element && element.type === 'bpmn:UserTask') {
        this.milestones = customCommands.getMilestones(element);
        this.userTaskFileName = element.businessObject.fileName;
      }

      if (element && this.canHaveConstraints && !this.elementConstraintMapping[element.id]) {
        // make the newly created mapping entry reactive so that it allows the constraint editor to react to changes to it
        this.$set(this.elementConstraintMapping, element.id, {
          hardConstraints: [],
          softConstraints: [],
        });
      }

      const businessObject = getBusinessObject(this.selectedElement);
      // if the selected element is some kind of event
      if (businessObject.eventDefinitions && businessObject.eventDefinitions.length > 0) {
        const [eventDefinition] = businessObject.eventDefinitions;

        if (eventDefinition.$type === 'bpmn:TimerEventDefinition') {
          this.timeDuration = '';
          if (eventDefinition.timeDuration && eventDefinition.timeDuration.body) {
            this.timeDuration = eventDefinition.timeDuration.body;
          }
        }
      }

      this.metaData = customCommands.getMetaData(element);

      this.elementDocumentation = customCommands.getDocumentation(element);
      if (element && element.type !== 'bpmn:Process') {
        this.elementFillColor = getFillColor(element, '#FFFFFFFF');
        this.elementStrokeColor = getStrokeColor(element, '#000000FF');
      }
    },
    /** */
    async setupXmlViewer() {
      this.xmlViewer = monaco.editor.create(document.getElementById('xml-container'), {
        language: 'xml',
        theme: 'vs-light',
        glyphMargin: true,
        automaticLayout: true,
        scrollBeyondLastLine: false,
        overviewRulerLanes: 0,
        hideCursorInOverviewRuler: true,
        overviewRulerBorder: false,
        minimap: {
          enabled: false,
        },
        wordWrap: 'on',
        wrappingStrategy: 'advanced',
        wrappingIndent: 'same',
      });
      this.xmlViewer.setValue(this.xml);
      if (!process.env.IS_ELECTRON) {
        const dbs = document.getElementById(this.canvasID).childNodes[0];
        const djs = dbs.getElementsByClassName('djs-container')[0];
        const svg = djs.getElementsByTagName('svg')[0];
        svg.classList.add('svg-height-fix');
      }
    },
    /** */
    async saveXml(xml, name = this.name) {
      await this.$store.dispatch('processEditorStore/setXml', {
        xml,
      });

      if (this.subprocessId) {
        const mainProcessXml = await this.$store.getters['processStore/xmlById'](
          this.processDefinitionsId
        );
        const mainProcessWithSubprocessContentXml = await addSubprocessContentToProcessXML(
          mainProcessXml,
          xml,
          this.subprocessId
        );
        await this.$store.dispatch('processStore/updateBpmn', {
          id: this.process.id,
          name: this.process.name,
          bpmn: mainProcessWithSubprocessContentXml,
        });
      } else {
        await this.$store.dispatch('processStore/updateBpmn', {
          id: this.process.id,
          name: this.process.name,
          bpmn: xml,
        });
      }
      this.showSaveMessage();
      clearTimeout(this.timeout);
      this.autoClose();
    },
    /** */
    showSaveMessage() {
      this.isSaveMessageVisible = true;
      setTimeout(() => {
        this.isSaveMessageVisible = false;
      }, 3000);
    },
    /** */
    async reloadModelerXml() {
      try {
        const { warnings } = await this.modeler.importXML(this.xml);
        if (warnings && warnings.length) {
          console.warn(warnings);
        }
        this.oldXml = this.xml;
        // make sure that the process element in the newly loaded process definition is selected
        this.selectBpmnElement(this.modeler.get('canvas').getRootElement());
      } catch (err) {
        console.error(err);
        this.popupData.body = '--> Error importing BPMN XML';
        this.popupData.color = 'error';
        this.openPopup();
      }
    },
    /** */
    async saveXmlFromModeler() {
      try {
        const { xml, warnings } = await this.modeler.saveXML({ format: true });
        if (warnings && warnings.length) {
          console.warn(warnings);
        }

        await this.saveXml(xml);
      } catch (err) {
        console.error(err);
        this.popupData.body = '--> Error in Xml';
        this.popupData.color = 'error';
        this.openPopup();
      }
    },
    /** */
    async saveXmlFromXmlViewer() {
      let xml = this.xmlViewer.getValue();
      // prevent problems if the user somehow changed the proceed namespace URI
      xml = ensureCorrectProceedNamespace(xml);
      this.$store.dispatch('processStore/update', {
        id: this.processDefinitionsId,
        changes: {},
        bpmn: xml,
      });

      this.saveXml(xml);
      this.reloadModelerXml();
    },
    /** */
    async exportSelected(selectedOption) {
      if (!selectedOption) {
        this.exportSelectedProcessesDialog = true;
        return;
      }

      this.exportRunning = true;
      const allProcesses = await this.$store.getters['processStore/processes'];
      await exportSelectedProcesses(allProcesses, [{ ...this.process }], selectedOption);
      this.exportSelectedProcessesDialog = false;
      this.exportRunning = false;
    },
    /** */
    autoClose() {
      if (!process.env.IS_ELECTRON) {
        this.timeout = setTimeout(() => {
          this.$emit('inactivityTimeout');
          this.$store.commit('warningStore/setWarning', true);
        }, this.config.closeOpenEditorsInMs || 300000);
      }
    },
    /** */
    resetTimeout() {
      clearTimeout(this.timeout);
      this.autoClose();
    },
    /** */
    makeFlowDefault() {
      if (!this.canHaveCondition) {
        return;
      }
      const modeling = this.modeler.get('modeling');

      modeling.updateProperties(this.selectedElement.source, {
        default: this.selectedElement.businessObject,
      });
      // refresh selected element to trigger recalculation of canHaveCondition
      const selected = this.selectedElement;
      this.selectedElement = undefined;
      this.selectedElement = selected;
      this.closeDialog('DefaultFlow');
    },
    clipUrl() {
      // Hacky workaround to copy the current url into the clipboard: https://stackoverflow.com/a/49618964
      const dummyInput = document.createElement('input');
      document.body.appendChild(dummyInput);
      dummyInput.value = window.location.href;
      dummyInput.select();
      dummyInput.setSelectionRange(0, 99999);
      document.execCommand('copy');
      document.body.removeChild(dummyInput);
    },
  },
  beforeMount() {
    this.loading = true;
    const process = this.$store.getters['processStore/processById'](this.processDefinitionsId);
    if (process && this.subprocessId) {
      this.resolver = this.$store.dispatch('processEditorStore/loadSubprocessFromStore', {
        process,
      });
    } else if (process && !this.subprocessId) {
      this.resolver = this.$store.dispatch('processEditorStore/loadProcessFromStore', {
        process,
      });
    } else {
      this.$router.back();
    }
  },
  async mounted() {
    try {
      await this.resolver;
      this.setupModeler();
      this.setupXmlViewer();
      this.autoClose();
    } catch (reason) {
      console.error(reason);

      this.popupData.body = 'Error importing process BPMN XML';
      this.popupData.color = 'error';
      this.openPopup();
    }
    this.xmlChangedCallback = eventHandler.on(
      'processXmlChanged',
      async ({ processDefinitionsId, newXml }) => {
        if (processDefinitionsId === this.processDefinitionsId) {
          await this.$store.dispatch('processEditorStore/setXml', { xml: newXml });
          this.reloadModelerXml();
        }
      }
    );
    this.bpmnEventCallback = eventHandler.on(
      'processBPMNEvent',
      ({ processDefinitionsId, type, context }) => {
        if (this.modeler && this.processDefinitionsId === processDefinitionsId) {
          eventDistribution.applyExternalEvent(type, context);
        }
      }
    );
    this.constraintChangedCallback = eventHandler.on(
      'elementConstraintsChanged',
      ({ processDefinitionsId, elementId, constraints }) => {
        if (this.modeler && this.process.id === processDefinitionsId) {
          const elementRegistry = this.modeler.get('elementRegistry');
          if (this.subprocessId && this.subprocessId !== elementId) {
            const elementInProcess = elementRegistry.get(elementId);
            if (!elementInProcess) {
              return;
            }
          }
          this.addConstraints(constraints, elementRegistry.get(elementId), true);
        }
      }
    );

    this.milestonesChangedCallback = eventHandler.on(
      'elementMilestonesChanged',
      ({ processDefinitionsId, elementId, milestones }) => {
        if (this.modeler && this.process.id === processDefinitionsId) {
          const elementRegistry = this.modeler.get('elementRegistry');
          if (this.subprocessId && this.subprocessId !== elementId) {
            const elementInProcess = elementRegistry.get(elementId);
            if (!elementInProcess) {
              return;
            }
          }
          this.updateMilestones(milestones, elementRegistry.get(elementId), true);
        }
      }
    );

    this.locationsChangedCallback = eventHandler.on(
      'elementLocationsChanged',
      ({ processDefinitionsId, elementId, locations }) => {
        if (this.modeler && this.process.id === processDefinitionsId) {
          const elementRegistry = this.modeler.get('elementRegistry');
          if (this.subprocessId && this.subprocessId !== elementId) {
            const elementInProcess = elementRegistry.get(elementId);
            if (!elementInProcess) {
              return;
            }
          }
          this.updateLocations(locations, elementRegistry.get(elementId), true);
        }
      }
    );

    this.resourcesChangedCallback = eventHandler.on(
      'elementResourcesChanged',
      ({ processDefinitionsId, elementId, resources }) => {
        if (this.modeler && this.process.id === processDefinitionsId) {
          const elementRegistry = this.modeler.get('elementRegistry');
          if (this.subprocessId && this.subprocessId !== elementId) {
            const elementInProcess = elementRegistry.get(elementId);
            if (!elementInProcess) {
              return;
            }
          }
          this.updateResources(resources, elementRegistry.get(elementId), true);
        }
      }
    );
    this.connectionLostCallback = eventHandler.on('connectionLost', async ({ reason }) => {
      // Prevent user from editing when the process is shared
      if (this.process.shared) {
        this.connectionWarningData.body =
          'You seem to have lost connection to the server. You will not be able to edit the process anymore and will get the latest updates once you are back online';
        this.connectionWarningData.color = 'error';

        this.modeler.destroy();
        this.modeler = new NavigatedViewer({
          container: '#' + this.canvasID,
          moddleExtensions: {
            proceed: proceedModdleExtension,
          },
        });

        try {
          const { warnings } = await this.modeler.importXML(this.xml);
          if (warnings && warnings.length) {
            console.warn(warnings);
          }
          this.viewer.get('canvas').zoom('fit-viewport', 'auto');
        } catch (err) {
          console.error(err);
        }
        this.disableEditing = true;
      } else {
        this.connectionWarningData.body =
          "You lost seem to have lost connection to the server. You are still able to edit this process since it is not shared. You won't be able to edit shared processes.";
        this.connectionWarningData.color = 'primary';
      }
      this.connectionWarningData.display = 'block';
    });
    this.connectionRecoveredCallback = eventHandler.on('reconnected', async ({ attempt }) => {
      const newestXml = await this.$store.getters['processStore/xmlById'](
        this.processDefinitionsId
      );
      await this.saveXml(newestXml);
      this.modeler.destroy();
      this.setupModeler();
      this.disableEditing = false;
    });
    document.addEventListener('mousemove', this.resetTimeout, { passive: true });
  },
  beforeDestroy() {
    if (this.modeler) {
      this.modeler.destroy();
    }
    if (this.xmlViewer) {
      this.xmlViewer.dispose();
    }
    document.removeEventListener('mousemove', this.resetTimeout, { passive: true });
    clearTimeout(this.timeout);
    eventHandler.off('processXmlChanged', this.xmlChangedCallback);
    eventHandler.off('processBPMNEvent', this.bpmnEventCallback);
    eventHandler.off('elementConstraintsChanged', this.constraintChangedCallback);
    eventHandler.off('elementMilestonesChanged', this.milestonesChangedCallback);
    eventHandler.off('elementLocationsChanged', this.locationsChangedCallback);
    eventHandler.off('elementResourcesChanged', this.resourcesChangedCallback);
    eventHandler.off('lost_connection', this.connectionLostCallback);
    eventHandler.off('reconnected', this.connectionRecoveredCallback);
    this.$store.dispatch('processStore/stopEditing', {
      id: this.processDefinitionsId,
    });
  },
  watch: {
    async xml() {
      if (this.modeler) {
        this.xmlViewer.setValue(this.xml);
        let subprocesses;

        // get process-hierarchy of main process -> update subprocesses in store if anything changed
        if (this.process.subprocessId) {
          // if currently editing subprocess, get xml of main process
          const mainProcessXml = await this.$store.getters['processStore/xmlById'](
            this.processDefinitionsId
          );
          subprocesses = await getProcessHierarchy(mainProcessXml);
        } else {
          subprocesses = await getProcessHierarchy(this.xml);
        }

        if (JSON.stringify(subprocesses) !== JSON.stringify(this.process.subprocesses)) {
          this.$store.dispatch('processStore/update', {
            id: this.processDefinitionsId,
            changes: { subprocesses },
          });
        }
      }
    },
    async process(newProcess, oldProcess) {
      if (!newProcess) return;
      if (newProcess.id === oldProcess.id && newProcess.subprocessId === oldProcess.subprocessId)
        return;

      if (this.subprocessId) {
        await this.$store.dispatch('processEditorStore/loadSubprocessFromStore', {
          process: this.process,
          subprocessId: this.subprocessId,
        });
      } else {
        await this.$store.dispatch('processEditorStore/loadProcessFromStore', {
          process: this.process,
        });
      }
      await this.$nextTick();
      const xml = ensureCorrectProceedNamespace(this.xml);
      await this.saveXml(xml);
      await this.reloadModelerXml();
      const elRegistry = this.modeler.get('elementRegistry');
      const elements = elRegistry.filter((element) => {
        return !is(element, 'bpmn:Process');
      });
      if (elements.length > 1) {
        this.modeler.get('canvas').zoom('fit-viewport', 'auto');
      } else {
        this.modeler.get('canvas').zoom('fit-viewport', 0, 0);
      }
      eventDistribution.setProcessDefinitionsId(newProcess.id, this.subprocessId);
      if (this.isXmlDialogVisible) this.xmlViewer.setValue(this.xml);
    },
  },
};
</script>

<style lang="scss">
/* https://sass-lang.com/documentation/syntax#scss */

@import '../assets/styles/non-executable-elements-controlls.css';

.diagram-container {
  width: 100%;
  height: 100%;
}

.select-user-icon:before {
  content: '\F004';
  font: normal normal normal 20px 'Material Design Icons';
  text-decoration: inherit;
  /*--adjust as necessary--*/
  color: #000;
}

.task-edit-button-wrapper {
  display: flex;
  justify-content: center;
  margin-top: 10px;
  position: absolute;
  left: 0;
  right: 0;
  z-index: 1;
}

.append-users-or-roles {
  visibility: visible !important;
}

.bpmn-icon-sequential-mi-marker::after {
  position: relative;
  top: 3.25px;
  left: 3px;
  display: inline-block;
  content: ' ';
  width: 14px;
  height: 14px;
  border-radius: 7.5px;
}

.bpmn-icon-sequential-mi-marker.connected::after {
  background-color: #0a0;
}

.bpmn-icon-sequential-mi-marker.disconnected::after {
  background-color: #c22;
}

#dialog-content {
  position: relative;
}

#capability-container {
  position: absolute;
  top: -50px;
  right: 0px;
  display: inline-block;
  max-width: 50%;
  height: 110%;
  border-left: 1px solid #c0c0c0;
}

.htmlDialog {
  height: 604px;
}

#revertDialog {
  position: fixed;
  top: 50vh;
  z-index: 998;
  left: 50%;
}

.flex-container {
  display: flex;
  flex-direction: column;
  flex: 1;
}

.flex-toolbar {
  flex-grow: 0;
  flex-basis: 0;
}

.flex-content {
  flex-grow: 1;
  flex-basis: auto;
}

.history-mode-overlay {
  z-index: 1;
  margin: 10px;
  box-shadow: none;
  position: absolute;
  right: 0;
}

.properties-panel {
  z-index: 1;
  position: absolute;
  overflow-y: scroll;
  top: 180px;
  right: 10px;
}
.properties-toggle {
  z-index: 1;
  position: absolute;
  top: 140px;
  right: 10px;
}
.svg-height-fix {
  height: calc(100vh - 168px) !important;
}

.loading-message {
  position: absolute;
  bottom: 40%;
  left: 50%;
  right: 50%;
}
</style>