Creating an Email Task as a "custom task" within angular

Hello all,

I have been struggling to implement a custom element. Specifically I am trying to mimic the Email Task that is available in flowable.
For reference it is a modified service task with xml code that looks like this

<serviceTask id="email task id" name="email task name" flowable:type="mail">
  <extensionElements>
    <flowable:field name="headers">
      <flowable:string>
        <![CDATA[ headerField ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="to">
      <flowable:string>
        <![CDATA[ tofield@gmail.com ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="from">
      <flowable:string>
        <![CDATA[ fromField@gmail.com ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="subject">
      <flowable:string>
        <![CDATA[ subjectField ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="cc">
      <flowable:string>
        <![CDATA[ ccField ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="bcc">
      <flowable:string>
        <![CDATA[ bccField ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="text">
      <flowable:string>
        <![CDATA[ textField body of email? ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="html">
      <flowable:string>
        <![CDATA[ htmlField ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="htmlVar">
      <flowable:string>
        <![CDATA[ htmlVar ]]>
      </flowable:string>
    </flowable:field>
    <flowable:field name="textVar">
      <flowable:string>
        <![CDATA[ textVar ]]>
      </flowable:string>
    </flowable:field>
  </extensionElements>
  </serviceTask>

I am coding within angular and would like to keep all my files to typescript if possible. I have been struggling to get the email task to be separated from the service task, the closest I have gotten so far is getting the “Email Task” to appear within the replacement options of the popup menu when clicking on the wrench icon on a task element. But after I click on the Email Task type, it is then replaced by a service task. This is probably due to something I have wrong in my custom replace menu provider (my code below).

import { assign } from 'lodash';

export default class CustomReplaceMenuProvider {
  private bpmnReplace: any;
  private popupMenu: any;
  private modeling: any;
  private bpmnFactory: any;
  private rules: any;
  private translate: any;

  constructor(bpmnReplace, popupMenu, modeling, bpmnFactory, rules, translate) {
    this.bpmnReplace = bpmnReplace;
    this.popupMenu = popupMenu;
    this.modeling = modeling;
    this.bpmnFactory = bpmnFactory;
    this.rules = rules;
    this.translate = translate;
    this.popupMenu.registerProvider("bpmn-replace", this);
  }

  getPopupMenuEntries(element) {
    const entries = {};

    if (element.type === "bpmn:Task") {
      entries["replace-with-email-task"] = {
        label: "Email Task",
        className: "bpmn-icon-receive",
        action: () => {
          this.replaceElement(element, "bpmn:ServiceTask", { custom: "EmailTask" });
        },
      };
    }

    return entries;
  }

  replaceElement(element, _newType, customOptions) {
    const businessObject = this.bpmnFactory.create("bpmn:ServiceTask");
  
    this.modeling.updateProperties(element, {
      "custom:emailSubject": "Default Subject",
      "custom:recipient": "recipient@example.com",
      "custom:emailBody": "Test body text"
    });
  
    const newElement = this.bpmnReplace.replaceElement(
      element,
      assign(
        { type: "bpmn:ServiceTask", businessObject },
        customOptions
      )
    );
  
    return newElement;  
  }
  
}

(CustomReplaceMenuProvider as any).$inject = [
  "bpmnReplace",
  "popupMenu",
  "modeling",
  "bpmnFactory",
  "rules",
  "translate",
];

Here is my moddle extension of this new Email Task

{
  "name": "EmailTask",
  "uri": "http://custom-bpmn/schema/email-task",
  "prefix": "custom",
  "types": [
    {
      "name": "EmailTask",
      "superClass": ["bpmn:Task"],
      "properties": [
        {
          "name": "emailSubject",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "emailBody",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "recipient",
          "isAttr": true,
          "type": "String"
        }
      ]
    }
  ]
}

My index.ts

import CustomReplaceMenuProvider from "./CustomReplaceMenuProvider";

export default {
    __depends__: ["popupMenu", "bpmnReplace", "modeling", "bpmnFactory"],
    __init__: ["customReplaceMenuProvider"],
    customReplaceMenuProvider: ["type", CustomReplaceMenuProvider]
}

and this is where i declare my bpmnjs in my main file

bpmnJS: BpmnJS = new BpmnJS({
    additionalModules: [
      BpmnPropertiesPanelModule,
      BpmnPropertiesProviderModule,
      CustomReplaceModule
    ],
    moddleExtensions: {
      custom: emailTask
    }
  });

Currently after exporting my bpmn while using this configuration my “Email Task” xml looks like this:

<serviceTask id="Activity_15olasd" name="ServiceTask 140652" flowable:servicetasktriggerable="false" flowable:servicetaskUseLocalScopeForResultVariable="false" flowable:class="class" flowable:servicetaskstoreresultvariabletransient="false"/>

And as you can see my attributes provided in my custom moddle are missing. So I am not sure what I doing wrong at the moment.

I would provide a codeSandbox but this is for a fairly large project and I can not publicly distribute our code ATM.

I have also looked at a lot of the bpmn.io examples and the closest one would be the Custom Element example, but this is a bit different to what I want since I would like to have a custom properties panel for filling out the required fields for my email task. I also do not have diagram-js imported for a custom renderer and would prefer to not have to add more imports than necessary.

I am assuming I would need to do a combination of the custom element example and the properties panel extension example. But I have been really struggling to wrap my head around what all is needed and what all needs to be changed.

Any help and/or guidance would be greatly appreciated,
Thank you!

Update:
I added a CustomRenderer which seemed to “sort of” work. I now have my custom element appearing with the type as custom:MailTask, with some additional edits to my CustomReplaceMenuProvider file.
only issue now is adjusting the renderer to remove my lint errors that are currently appearing, and to actually add the styling so that I do not have a blank white box.


See the image for what I am talking about. I also need to fix my attr saving as only the name is currently saving to the xml export and I would like all of my properties to get exported.

For reference if anyone else is looking for solutions:
eddited CustomReplaceMenuProvider:

import { assign } from 'lodash';

export default class CustomReplaceMenuProvider {
  private readonly bpmnReplace;
  private readonly popupMenu;
  private readonly modeling;
  private readonly bpmnFactory;
  private readonly rules;
  private readonly translate;

  constructor(bpmnReplace, popupMenu, modeling, bpmnFactory, rules, translate) {
    this.bpmnReplace = bpmnReplace;
    this.popupMenu = popupMenu;
    this.modeling = modeling;
    this.bpmnFactory = bpmnFactory;
    this.rules = rules;
    this.translate = translate;
    this.popupMenu.registerProvider("bpmn-replace", this);
  }

  getPopupMenuEntries(element) {
    const entries = {};

    if (element.type === "bpmn:Task") {
      entries["replace-with-email-task"] = {
        label: "Email Task",
        className: "bpmn-icon-receive",
        action: () => {
          this.replaceElement(element, "custom:MailTask", { custom: "MailTask" });
        },
      };
    }

    return entries;
  }

  replaceElement(element, _newType, customOptions) {
    const businessObject = this.bpmnFactory.create("custom:MailTask");
  
    this.modeling.updateProperties(element, {
      "custom:emailSubject": "Default Subject",
      "custom:recipient": "recipient@example.com",
      "custom:emailBody": "Test body text"
    });
  
    const newElement = this.bpmnReplace.replaceElement(
      element,
      assign(
        { type: "custom:MailTask", businessObject },
        customOptions
      )
    );
  
    return newElement;  
  }
  
}

(CustomReplaceMenuProvider as any).$inject = [
  "bpmnReplace",
  "popupMenu",
  "modeling",
  "bpmnFactory",
  "rules",
  "translate",
];

CustomRenderer:

import inherits from 'inherits-browser';

import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';

import {
    append as svgAppend,
    create as svgCreate,
    attr as svgAttr
  } from 'tiny-svg';

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

export default function CustomRenderer(eventBus) {
  BaseRenderer.call(this, eventBus, 1500);

  this.canRender = function(element) {
    return is(element, 'custom:MailTask');
  };
  this.drawShape = function(parentNode) {
    const rect = svgCreate('rect');

    svgAttr(rect, {
      width: 100,
      height: 80,
      rx: 10,
      ry: 10,
      stroke: 'black',
      strokeWidth: 2,
      fill: 'white'
    });

    svgAppend(parentNode, rect);

    return rect;
  }
}

inherits(CustomRenderer, BaseRenderer);

CustomRenderer.$inject = [ 'eventBus' ];