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

Through the use of a custom renderer, and a custom replace menu provider I have successfully gotten the email task to load and save all attributes. Though I will note that I am using a custom property panel loader, the code I am unable to share.

I will list the code for the other custom components below though.

It is also important to note that in order for my custom attributes to save correctly I had to add my custom namespace to the header of namespaces.

Within a custom folder with my custom file:
Index.ts:

import CustomReplaceMenuProvider from './CustomReplaceMenuProvider';
import CustomRenderer from './CustomRenderer';

export default {
    __depends__: ['popupMenu', 'bpmnReplace', 'modeling', 'bpmnFactory', 'eventBus', 'bpmnRenderer', 'textRenderer'],
    __init__: ['customReplaceMenuProvider', 'CustomRenderer'],
    customReplaceMenuProvider: ['type', CustomReplaceMenuProvider],
    CustomRenderer: ['type', CustomRenderer]
}

email-task.json

{
  "name": "MailTask",
  "uri": "http://custom/schema/bpmn/moTask",
  "prefix": "moTask",
  "types": [
    {
      "name": "MailTask",
      "superClass": [ "bpmn:Task" ],
      "properties": [
        {
          "name": "mailtaskto",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "mailtaskfrom",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "mailtasksubject",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "mailtaskcc",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "mailtasktext",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "mailtasktextvar",
          "isAttr": true,
          "type": "String"
        }
      ]
    }
  ]
}

Custom Renderer:

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, textRenderer) {
  BaseRenderer.call(this, eventBus, 1500);

  this.canRender = function(element) {
    return is(element, 'moTask:MailTask');
  };

  this.drawShape = function(parentNode, element) {
    const rect = svgCreate('rect');
    
    svgAttr(rect, {
      width: 100,
      height: 80,
      rx: 10,
      ry: 10,
      stroke: 'black',
      strokeWidth: 2,
      fill: 'none'
    });
    svgAppend(parentNode, rect);

    const imgAtters = svgCreate('image');
    svgAttr(imgAtters, {
      x: 5,
      y: 5,
      width: 20,
      height: 20,
      href: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M16.1 260.2c-22.6 12.9-20.5 47.3 3.6 57.3L160 376v103.3c0 18.1 14.6 32.7 32.7 32.7 9.7 0 18.9-4.3 25.1-11.8l62-74.3 123.9 51.6c18.9 7.9 40.8-4.5 43.9-24.7l64-416c1.9-12.1-3.4-24.3-13.5-31.2s-23.3-7.5-34-1.4l-448 256zm52.1 25.5L409.7 90.6 190.1 336l1.2 1L68.2 285.7zM403.3 425.4L236.7 355.9 450.8 116.6 403.3 425.4z'/%3E%3C/svg%3E"
    });
    svgAppend(parentNode, imgAtters);

    const labelAttrs = {
      box: {
          width: 90,
          height: 80
      },
      align: 'center-middle',
      padding: 7,
      fitBox: true
    };
    const text = textRenderer.createText(element.businessObject.name || 'Mail Task', labelAttrs);
    svgAttr(text, { transform: 'translate(20, 0)', 'fill': 'rgb(34, 36, 42)' });
    svgAppend(parentNode, text);
  
    return rect;
  }
}

inherits(CustomRenderer, BaseRenderer);

CustomRenderer.$inject = [ 'eventBus', 'textRenderer' ];

Custom Replace Menu Provider:

import { isAny } from 'bpmn-js/lib/util/ModelUtil';
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 (isAny(element, [ 'bpmn:Task' ])) {
      entries["replace-with-email-task"] = {
        label: "Email Task",
        className: "bpmn-icon-receive",
        action: () => {
          this.replaceElement(element, "moTask:MailTask", { moTask: "MailTask" });
        },
      };
    }

    return entries;
  }

  replaceElement(element, _newType, customOptions) {
    const businessObject = this.bpmnFactory.create('moTask:MailTask');
  
    this.modeling.updateProperties(element, {
      'moTask:mailtaskto': '1',
      'moTask:mailtaskfrom': '2',
      'moTask:mailtasksubject': '3',
      'moTask:mailtaskcc': '4',
      'moTask:mailtasktext': '5',
      'moTask:mailtasktextvar': '6'
    });
  
    const newElement = this.bpmnReplace.replaceElement(
      element,
      assign(
        { type: 'moTask:MailTask', businessObject },
        customOptions
      )
    );
  
    return newElement;  
  }
  
}

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

Json properties for my custom property panel:

"MailTask": [
    {
      "id": "id",
      "type": "String",
      "title": "Id",
      "value": "",
      "description": "Unique identifier of the element.",
      "popular": true
    },
    {
      "id": "name",
      "type": "String",
      "title": "Name",
      "value": "",
      "description": "The descriptive name of the BPMN element.",
      "popular": true,
      "refToView": "text_name"
    },
    {
      "id": "documentation",
      "type": "Text",
      "title": "Documentation",
      "value": "",
      "description": "Documentation of the BPMN element.",
      "popular": true
    },
    {
      "id": "mailtaskto",
      "type": "String",
      "title": "To",
      "value": "",
      "description": "The recipients if the e-mail. Multiple recipients are defined in a comma-separated list.",
      "popular": true
    },
    {
      "id": "mailtaskfrom",
      "type": "String",
      "title": "From",
      "value": "",
      "description": "The sender e-mail address. If not provided, the default configured from address is used.",
      "popular": true
    },
    {
      "id": "mailtasksubject",
      "type": "String",
      "title": "Subject",
      "value": "",
      "description": "The subject of the e-mail.",
      "popular": true
    },
    {
      "id": "mailtaskcc",
      "type": "String",
      "title": "Cc",
      "value": "",
      "description": "The cc's of the e-mail. Multiple recipients are defined in a comma-separated list",
      "popular": true
    },
    {
      "id": "mailtasktext",
      "type": "String",
      "title": "Content",
      "value": "",
      "description": "The content of the e-mail, in case one needs to send plain -rich e-mails. Can be used in combination with html, for e-mail clients that don't support rich content. The client will then fall back to this text-only alternative.",
      "popular": true
    },
    {
      "id": "mailtasktextvar",
      "type": "String",
      "title": "ContentVar",
      "value": "",
      "description": "The name of a process variable that holds the text that is the content of the e-mail, in case one needs to send plain -rich e-mails. Can be used in combination with html, for e-mail clients that don't support rich content. The client will then fall back to this text-only alternative.",
      "popular": true
    },
    {
      "id": "asynchronousdefinition",
      "type": "Boolean",
      "title": "Asynchronous",
      "value": "false",
      "description": "Define the activity as asynchronous.",
      "popular": true
    },
    {
      "id": "exclusivedefinition",
      "type": "Boolean",
      "title": "Exclusive",
      "value": "false",
      "description": "Define the activity as exclusive.",
      "popular": true
    },
    {
      "id": "executionlisteners",
      "type": "multiplecomplex",
      "title": "Execution listeners",
      "value": "",
      "description": "Listeners for an activity, process, sequence flow, start and end event.",
      "popular": true
    },
    {
      "id": "multiinstance_type",
      "type": "multiinstance-type",
      "title": "Multi-instance type",
      "value": "",
      "description": "Repeated activity execution (parallel or sequential) can be displayed through different loop types",
      "popular": true,
      "refToView": "multiinstance"
    },
    {
      "id": "multiinstance_cardinality",
      "type": "String",
      "title": "Cardinality (Multi-instance)",
      "value": "",
      "description": "Define the cardinality of multi instance.",
      "popular": true
    },
    {
      "id": "multiinstance_collection",
      "type": "String",
      "title": "Collection (Multi-instance)",
      "value": "",
      "description": "Define the collection for the multi instance.",
      "popular": true
    },
    {
      "id": "multiinstance_variable",
      "type": "String",
      "title": "Element variable (Multi-instance)",
      "value": "",
      "description": "Define the element variable for the multi instance.",
      "popular": true
    },
    {
      "id": "multiinstance_condition",
      "type": "String",
      "title": "Completion condition (Multi-instance)",
      "value": "",
      "description": "Define the completion condition for the multi instance.",
      "popular": true
    },
    {
      "id": "isforcompensation",
      "type": "Boolean",
      "title": "Is for compensation",
      "value": "false",
      "description": "A flag that identifies whether this activity is intended for the purposes of compensation.",
      "popular": true,
      "refToView": "compensation"
    }
  ],

within my main file:

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