Source: management-system/src/frontend/components/processes/processForm/userTasks.vue

<template>
  <div></div>
</template>
<script>
import processesDataInjectorMixin from './ProcessesDataInjectorMixin.vue';
import onSubmitInjectorMixin from './OnSubmitInjectorMixin.vue';

import {
  toBpmnObject,
  toBpmnXml,
  getUserTaskFileNameMapping,
  generateUserTaskFileName,
  getElementById,
  getTaskConstraintMapping,
  addConstraintsToElementById,
  setUserTaskData,
  getAllUserTaskFileNamesAndUserTaskIdsMapping,
} from '@proceed/bpmn-helper';
import { defaultHtml, defaultCss } from '@/frontend/assets/user-task.js';

import { asyncForEach } from '@/shared-frontend-backend/helpers/javascriptHelpers.js';

import { getUpdatedTaskConstraintMapping } from '@/frontend/helpers/usertask-helper.js';

/**
 * @module components
 */
/**
 * @memberof module:components
 * @module processes
 */
/**
 * This component handles the user task data inside the current entries of processesData
 *
 * @memberof module:components.module:processes
 * @module Vue:userTasks
 *
 */
export default {
  mixins: [processesDataInjectorMixin, onSubmitInjectorMixin],
  data() {
    return {
      beforeSubmitPriority: 500,
    };
  },
  methods: {
    /**
     * This function will update the htmlData inside the process entry using its already existing data
     *
     * It is called when specific attributes inside an entry have changed
     *
     * @param {Object} entry the entry to update the htmlData in
     */
    async calculateHtmlData(currentData) {
      // early exit if there is still ambiguity on how to handle the process itself
      if (
        !currentData.id &&
        (currentData.possibleOverrideProcess ||
          (currentData.possibleDerivedProcesses && currentData.possibleDerivedProcesses.length))
      ) {
        return currentData.htmlData;
      }

      const htmlData = new Map();

      // take over user provided htmlData if there is any
      if (currentData.htmlData) {
        currentData.htmlData.forEach((value, fileName) => {
          htmlData.set(fileName, { provided: { ...value.provided, info: 'user-provided' } });
        });
      }

      // if the current process entry is related to an existing process, import its html data
      if (currentData.originalStoredProcessId) {
        // get the html data from the store
        const userTasksHtml = await this.$store.getters['processStore/htmlMappingById'](
          currentData.originalStoredProcessId
        );

        // add html info to form htmlData
        Object.keys(userTasksHtml).forEach((fileName) => {
          const htmlInfo = {
            html: userTasksHtml[fileName],
            originalStoredProcessId: currentData.originalStoredProcessId,
            info: 'existing',
          };

          if (htmlData.has(fileName)) {
            const htmlInfos = htmlData.get(fileName);
            // if the user provided html and the existing html are the same just use the existing html
            if (htmlInfos.provided.html === htmlInfo.html) {
              delete htmlInfos.provided;
            }

            htmlInfos.existing = htmlInfo;
          } else {
            htmlData.set(fileName, { existing: htmlInfo });
          }
        });
      }

      return htmlData;
    },
    async calculateUserTaskData(currentData) {
      // early exit if there is still ambiguity on how to handle the process itself
      if (
        !currentData.htmlData ||
        (!currentData.id &&
          (currentData.possibleOverrideProcess ||
            (currentData.possibleDerivedProcesses && currentData.possibleDerivedProcesses.length)))
      ) {
        return [];
      }

      const { htmlData } = currentData;

      // get the current assignments of fileNames to userTasks
      const userTaskMapping = await getUserTaskFileNameMapping(currentData.bpmn);

      const userTasks = [];

      // if 5thIndustry is to be used don't add any user tasks
      if (!currentData.isUsing5i) {
        Object.entries(userTaskMapping).forEach(([taskId, { fileName }]) => {
          let userTaskData = {
            taskFileName: fileName || generateUserTaskFileName(),
            taskId,
          };

          if (!htmlData || !htmlData.has(fileName)) {
            // add default html if there is no html data for this task
            userTaskData.html = `<html><head><style>${defaultCss}</style></head> <body>${defaultHtml}</body> </html>`;
            userTaskData.info = 'missing';
            userTaskData.additionalInfo = 'default';
          } else {
            // htmlOptions should be an array with the existing or provided html for this task or both
            const htmlOptions = { ...htmlData.get(fileName) };

            // check if a one of the options might have already been selected by the user (this will be used inside the formWarning component)
            if (htmlOptions.provided && htmlOptions.provided.chosen) {
              delete htmlOptions.existing;
            }
            if (htmlOptions.existing && htmlOptions.existing.chosen) {
              delete htmlOptions.provided;
            }

            if (htmlOptions.provided && htmlOptions.existing) {
              // if there are two options for the current task and none is selected flag it as a conflict
              userTaskData.info = 'conflict';
              userTaskData.options = htmlOptions;
            } else {
              // if there is only one option use it
              const htmlInfo = htmlOptions.provided ? htmlOptions.provided : htmlOptions.existing;

              userTaskData.html = htmlInfo.html;

              if (currentData.id && currentData.id === htmlInfo.originalStoredProcessId) {
                // this user task info does already exist on the target process => we don't need to add it
                userTaskData.info = 'existing';
              } else {
                // this user task info doesn't currently exist for the target process => we need to add it
                userTaskData.info = 'missing';
                userTaskData.originalStoredProcessId = htmlInfo.originalStoredProcessId;
              }
            }
          }

          userTasks.push(userTaskData);
        });
      }

      // if the current entry is supposed to update the information of an existing process check if some user task data is becoming obsolete
      if (this.$store.getters['processStore/processById'](currentData.id)) {
        // get the fileNames that already exist for the process to overwrite
        const overwriteFileNameMapping = await this.$store.getters['processStore/htmlMappingById'](
          currentData.id
        );

        const overwriteBPMN = await toBpmnObject(
          await this.$store.getters['processStore/xmlById'](currentData.id)
        );

        // get the current assignments of fileNames to userTasks
        const overwriteUserTaskMapping = await getAllUserTaskFileNamesAndUserTaskIdsMapping(
          overwriteBPMN
        );

        await asyncForEach(
          Object.entries(overwriteUserTaskMapping),
          async ([fileName, taskIds]) => {
            overwriteUserTaskMapping[fileName] = taskIds.map((id) => {
              const userTask = getElementById(overwriteBPMN, id);

              if (userTask.name) {
                return `'${userTask.name}'`;
              }

              return id;
            });
          }
        );

        Object.keys(overwriteFileNameMapping).forEach((fileName) => {
          if (
            currentData.isUsing5i ||
            !userTasks.some((userTask) => userTask.taskFileName === fileName)
          ) {
            // this fileName is not used by any user task in the new bpmn => remove it on commit
            userTasks.push({
              taskFileName: fileName,
              usedBy: overwriteUserTaskMapping[fileName],
              info: 'obsolete',
            });
          }
        });
      }

      return userTasks;
    },
    /**
     * React to changes in a processesData entry
     */
    async watchOtherChanges(currentData, currentChanges) {
      let newChanges = {};

      const mergedData = { ...currentData, ...currentChanges };

      // we only need htmlData and userTasks data if there is a process bpmn with user tasks using it
      if (mergedData.bpmn) {
        // the htmlData has to change if:
        if (
          currentChanges.bpmn || // the bpmn that defines html usage changed
          currentChanges.hasOwnProperty('id') || // we might have changed if we overwrite an existing process or not
          currentChanges.hasOwnProperty('originalStoredProcessId') // the process this one is based on changed
        ) {
          newChanges.htmlData = await this.calculateHtmlData(mergedData);
        }

        // the userTasks data has to change if:
        if (
          currentChanges.htmlData || // we have new html Data
          (currentData.htmlData && // there is existing html Data and:
            (currentChanges.hasOwnProperty('id') || // we might have changed if we overwrite an existing process or not
              currentChanges.hasOwnProperty('originalStoredProcessId') || // the process this one is based on changed
              currentChanges.hasOwnProperty('isUsing5i'))) // if 5thIndustry is to be used has changed
        ) {
          newChanges.userTasks = await this.calculateUserTaskData(mergedData);
        }
      }

      return newChanges;
    },
    // this will be called for an entry when the bpmn inside it is removed
    async removeBPMNRelatedData() {
      return { userTasks: undefined, htmlData: undefined };
    },
    /**
     * This function will be called before the process is added to the application
     *
     * It will ensure that the userTask data is set corretly on the bpmn
     */
    async beforeSubmit(processData) {
      // user task data is only needed when there is a bpmn file
      if (!processData.bpmn || !processData.userTasks) {
        return;
      }

      const conflictTask = processData.userTasks.find((userTask) => userTask.info === 'conflict');
      // at this point there should only be valid entries inside the userTasks array
      if (conflictTask) {
        throw new Error(
          `There is no valid user task data for user task with id ${conflictTask.taskId}`
        );
      }

      const bpmnObj = await toBpmnObject(processData.bpmn);

      const constraintMapping = await getTaskConstraintMapping(bpmnObj);

      await asyncForEach(processData.userTasks, async (userTaskData) => {
        // obsolete processes are not part of the new bpmn => do nothing
        if (userTaskData.info === 'obsolete') {
          return;
        }

        const userTask = getElementById(bpmnObj, userTaskData.taskId);

        // make sure the user tasks use the correct fileName and implementation
        setUserTaskData(userTask, userTaskData.taskId, userTaskData.taskFileName);

        // make sure to add html specific constraints
        const newConstraints = getUpdatedTaskConstraintMapping(
          constraintMapping[userTask.id],
          userTaskData.html
        );

        await addConstraintsToElementById(userTask, userTask.id, newConstraints);
      });

      processData.bpmn = await toBpmnXml(bpmnObj);
    },
    /**
     * This function will add missing user tasks to the application or remove obsolete ones
     */
    async afterSubmit(processData, finalProcess) {
      if (!processData.userTasks) {
        return;
      }

      await asyncForEach(processData.userTasks, async (userTask) => {
        if (userTask.info === 'missing') {
          await this.$store.dispatch('processStore/saveUserTask', {
            processDefinitionsId: finalProcess.id,
            taskFileName: userTask.taskFileName,
            html: userTask.html,
          });
        } else if (userTask.info === 'obsolete') {
          await this.$store.dispatch('processStore/deleteUserTask', {
            processDefinitionsId: finalProcess.id,
            taskFileName: userTask.taskFileName,
          });
        }
      });
    },
  },
};
</script>