Adventures in Custom Elements... making the html A tag


#1

First word and note… I am a complete new person to this software. I have done my best to not only understand the format but as much of the underlying code as I can. It is still impressively complex. Still I am possibly asking a stupid or maybe non-sensible question, but I am wanting to see what I can learn.

My department wants me to make an new bpmn object that essentially works like a html A tag. You can click on it to open a noted other diagram in a separate tab. My plan was to make a custom object, and have it contain an ID code that I would store in my data base, and when that ID is called from y data base the XML data would load in and open up as a existing bpmn diagram in a separate tab.

So I would need to make or over write an existing object, and have it somehow store the data of the document. Then I can make a custom event when it is clicked on to open the desired data in a new tab.

So. I looked at the bpmn.io examples for a reference. The one named “Custom Elements” seems to sorta make new elements, but they don’t store int he XML. Instead it seems to use data which is off in a json file. I don’t know why they would do that, but that seems to be what is done. I need the data to be stored in the bpmn xml file generated at save.

I also looked at the Nyan cats example which seems to sorta make a custom element. If I am understanding correctly it looks like it is more hijacking an existing element and re-defining it. Sorta. It is shorter and simpler, and I had an easier time understanding it, but I am not sure what I want to do is to hijack an existing element in order to do what I want to do. Not to mention the fact that I still don’t know how I can store the extra data that I want to. My implementation of BPMN uses the commenting system so I can;t store the data as a comment without it looking weird when people open comments.

Any suggestions would be welcome.

As to providing a link showing an example of what I am doing, I can’t as it is behind my companies firewall. Still for the most part is standard BPMN with commenting added much like the example provided, and the properties panel much like shown in its example.

The only thing I have added that is extra is a ajax code that calls to an API and provides the ability to load and save data to a MS sql database.


#2

Hi @jeremy.mone

although your code is behind your companies firewall, I really want to encourage to give us some example code so we can help you in detail. Since we do not know how your actual implementation of your custom / comment element looks like, it’s hard to help you.


#3

Providing any kinds of mock-ups that illustrate your goal would help us tremendously, too.


#4

What I have is mostly the same stuff as in the examples quite honestly. Not much is custom anything on its own.

Still I think part of what I want to know may be more about the BPMN XML standard than the code. What I want to create is a custom object that gets included with the XML written out when the saveXML feature is called. I would like to have it save to a bpmn xml that won’t freak out Cawemo or other BPMN editors if they load. So I am wondering if there is a type of custom elements in the xml that I can manpulate and fill with special data that other editors would just turn into something benign or just straight ignore but not crash. A number of my co-workers use Cawemo so it would be nice if I made it so at least Cawemo can read the bpmn.xml doc produced on save without freaking out.

So… The example I understand the best when it comes to custom objects is the nyan cat example. I have what I feel is a firm grasp of how it works code wise, but the output xml is like this:

<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="2750ca85-1a00-a369-7c07-1fbe929dd217" targetNamespace="http://bpmn.io/schema/bpmn" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
  <bpmn2:process id="Process_1" isExecutable="false">
	  ... stuff clipped ...
    <bpmn2:serviceTask id="ServiceTask_14wq29l">
      <bpmn2:documentation textFormat="text/x-comments">:Comment on Nyan</bpmn2:documentation>
      <bpmn2:incoming>SequenceFlow_1mpw14n</bpmn2:incoming>
    </bpmn2:serviceTask>
	  ... stuff clipped ...
  </bpmn2:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
	  ... stuff clipped ...
      <bpmndi:BPMNShape id="ServiceTask_14wq29l_di" bpmnElement="ServiceTask_14wq29l">
        <dc:Bounds x="862" y="357" width="100" height="80" />
      </bpmndi:BPMNShape>
	  ... stuff clipped ...
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn2:definitions>

I know what the nyan cat is doing… it is essentially hijacking the bpmn2:serviceTask object, and making it look different mostly. Not much else. I assume all the business rules it has are the same as the serviceTask and the whole nine yards. It just has a cute cat graphic running in place.

So it got me thinking and wanting to ask… Is there a bpmn2:ignorableObjectThatWontKillCawemo that I can override or extend. It would be great if I could give it bpmn2:documentation sub elements that I can use to store data in as well.

I want anything I create and edit to store and save into the XML, and I would highly prefer if this XML could be loaded into Cawemo, but simply ignored or looks like something else like the data document or some such.

I have been searching for a list of all the possible acceptable XML elements that is part of the standard with the hope they may a couple of handful of bpmn2:customElement1(2, 3, 4, etc) that I can use to make special stuff I can save in the xml and not break other things.

So I think I am repeating myself, and I will leave it there.

This is my first time reaching out to a for help in a written form so I am a bit unused to it. I do apologize, and I appreciate your time and help int his matter. I am hoping to spark some ideas or find more resources that may help guide me a bit better down this rabbit hole I am travelling.

Thank you for your time and patience!

x Jeremy Mone


#5

A_tag_like_element

So you click on the Plus sign, or maybe just on the element itself, and it fires off some javascript code that opens a “linked” diagram in a different tab.

That is the main idea. So it gives me a way to create objects that can be used to represent other diagrams which can be opened on their own. You can do that in the diagram opened up as well and link to yet more diagrams. Keep going this way inception style as much as you like. You want to get back to where you were? Close the browser tab it was opened up in, or shift back the the tab you came from.

So you can make a process diagram that can lead to other process diagrams. I would probably make an api that would open the diagram with my modeler with some code that references a saved bpmn xml in my microsoft sql database (already have that part done actually).

Does that make sense as to what the mock-up design is intended to be?

Hoping I am making this more clear instead of less. Thank you immensely for your time and help!

x Jeremy M.


#6

If I understand you correctly, you request two features:

(1) Opening multiple diagrams in different tabs: This essentially means you got multiple modeler instances, that show different diagrams. Just build it and it would work (cf. Camunda Modeler, an application that implements this).

(2) Opening linked diagrams: Following the interaction example you may be able to register to ‘click’ events on an element. Simply intercept that click, figure out which diagram is linked (this is the tricky part) and proceed to step (1).

As far as I am aware non of your requirements requires an actual HTML<a/> tag to be rendered / injected.


#7

Yes. I think you got it right. I didn’t want a literal A tag to be rendered. I wanted to make something that worked much like an A tag. Gives me the same ability to click on an element, and it will open a different document or different full instance of the modeler in a different browser tab or browser window. I would click on it and it would work much like (but not exactly as):

<a href="https://MyReallyCoolModelerInstances.com/bpmn_modeler/?database_id=3bsdj354" target="_blank"></a>

Again I don’t need it to create a literal A tag to be clicked on. I just need to make it so when the element is clicked on it will have the data somehow stored on the XML that has “3bsdj354”, and it will know to fire off the javascript code:

let url = "https://MyReallyCoolModelerInstances.com/bpmn_modeler/?database_id=" + database_id;
window.open(url, "_blank");

I am pretty sure I can accomplish this with the existing features and code already in BPMN. The hard part as you said is making it so the database_id is saved in and loaded from the bpmn xml. Then I need to either create a new custom object that can do the actions, or I need to be able to add that data to an existing object and make it so if that existing object has said extra data then it responds a bit different when clicked. Something like that.

Also trying very hard to make the saved bpmn xml be something that can be loaded without issue into other bpmn xml based modelers like Cawemo and the like. Why I would like to try to give maybe an existing element a bit of double duty if I can somehow add additional data to it in a way that the base modeler will validate properly.


#8

We’re exporting valid XML and Cawemo uses our toolkit under the hood :drum:. Everyting you can feed into bpmn-js as a library you’ll also be able to feed into Cawemo.


#9

Very true. Just afraid that if I make a custom object/element that it will stop working on cawemo so I want to do something that will mesh well without losing the data even if it may not quite work right on cawemo. If that makes sense at all.


#10

I have been looking at these items:

which also references:

and I am wondering if this is closer to the mark I seem to be wanting to hit? Am I barking up the right tree you think?


#11

Regardless of the way you implement your hyperlinks it’s not possible to implement them in way that a URL is opened whenever you click on an element. That’s not possible without customizing the modeler itself.


#12

I have seen where you can detect when an element is clicked on you can do things. If I can detect that the element has additional data on it (I am assuming that event bus broadcast will have the element info attached to it), then I can check to see if a particular set of data is there, and then run a function tied to that event which runs some javascript. I grab the Database_ID from the data on the element, and run the javascript that does a window.open(“my url”+Database_ID, “_blank”); and presto a new window is opened with the script on that url know what to do with the code and opening a new modeler with a different bpmn.xml in it.

So of that I know how I am going to do it. When I actually get something put together that can do it, then I will be happy to share that code on here as well. What I don’t know is how I am going to get that data into the model in such a way that it will save and load in the bpmn.xml that is generated when calling a modeler.saveXML(). I also have the constraint that the way it saves the data should be something that preferably will not prevent it from being loaded “as is” into cawemo. Even if used in cawemo it shouldn’t lose that data either. Should go in and come back out of cawemo unaffected, and my modeler would know what to do with it when present.

So what I am mostly trying to figure out is how to get the data into the model in a compliant way, and have it save and load without issue. How I use the data will be a special thing I do by branching off a element click event and pulling desired data and doing something special.

A lot hinges on what I can and cannot put into the xml save in a way that it will be loaded later without issue.


#13

You can use custom click handlers to open URLs on click. That approach will not help users of Cawemo, though, as you can’t customize the Cawemo modeler.


#14

That is fine so long as the save file of bpmn xml still has the data that can be read by my site. It is ok that it is not used in cawemo.


#15

Do you want these hyperlink elements to be seperate elements? You could also just have a custom attribute on an element and offer opening the specified link.


#16

Not separate elements. Just custom attributes that I will set my modeler to react to when the element is clicked on. Yet something that properly saves into the XML, and doesn’t create a fuss in cawemo the the xml is passed into that app, and will pass out of cawemo to if exported as a bpmn xml file.

Basically overload an existing object (element?) with additional data, and I will have a click event that uses that data to open a new browser window or tab with the info in the tagged into the URL so the modeler/viewer knows with database data to load and use for the xml of that new tab or window.


#17

That is easily possible. Have you had a look at this example?


#18

Ironically… I had mention that URL and link in a above post of my own where I thought it might be an example that will lead me in the right direction. With your suggestion on top of my hunch, I think this may be more of the way and direction I will go. I will need to do some research and testing to see how it turns out. I will try to pop back on the thread to drop any useful results of my work so it can hopefully help out others who face similar challenges.

Thank you for your help with this. If I have any other questions about this I may also pop back into this thread to address them.

x Jeremy M.


#19

OK wall of code time:

import diagramStyle from 'bpmn-js/dist/assets/diagram-js.css'; // eslint-disable-line no-unused-vars
import bpmnStyle from 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
import icons from 'bpmn-font/dist/css/bpmn-embedded.css'; // eslint-disable-line no-unused-vars
import commentsStyle from 'bpmn-js-embedded-comments/assets/comments.css';
import propertiesStyle from 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css';
import appStyle from './css/app.css';
// Yes this is technically loaded twice, but but the load in html makes it easier to find where the css items are to edit and debug.
// Since it is loaded here though that means the app.css is technically the last thing to set any CSS rule thus I can more easily override
// classes and rules set in the other CSS files previously loaded.

import $ from 'jquery';

// import CustomModeler from './custom-modeler'; // This already calls BpmnModeler inside of itself.
// import CustomModeler from './custom-modeler-reorganized'; // This already calls BpmnModeler inside of itself.

import BpmnModeler from 'bpmn-js/lib/Modeler';
import diagramXML from '../resources/newDiagram.bpmn';
import EmbeddedComments from 'bpmn-js-embedded-comments';

// import nyanObjectModule from './custom-modeler-reorganized/nyan';

import propertiesPanelModule from 'bpmn-js-properties-panel';
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/bpmn';
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda';

import qaPackage from './moddleExtensions/qaPackage.json'
import urlRefPackage from './moddleExtensions/urlRefPackage.json'

import colorPickerModule from './color-picker';
import { b64EncodeUnicode, b64DecodeUnicode} from './functions/b64_unicode.js';
import { apiGet, apiPost } from './functions/apiGetPost.js';

var container = $('#js-drop-zone');

// window.modeler = new CustomModeler({
window.modeler = new BpmnModeler({
  container: '#js-canvas',
  propertiesPanel: {
    parent: '#js-properties'
  },
  additionalModules: [
    // nyanObjectModule,
    colorPickerModule,
    EmbeddedComments,
    propertiesPanelModule,
    propertiesProviderModule
  ],
  moddleExtensions: {
    camunda: camundaModdleDescriptor,
    qa: qaPackage,
    urlRef: urlRefPackage
  },
  keyboard: {
    bindTo: document
  }
});
window.modelerEventBus = modeler.get('eventBus');
window.modelerChanged = false;

window.currentProject = { username: "", foldername: "", filename: "" };

window.file_set_username = file_set_username;
function file_set_username(newName, event) {
  // console.log("file_set_username: "+newName);
  // console.log(event);
  window.currentProject.username = newName;
  document.getElementById("leftSidebar-username").value = newName;
}
window.file_set_foldername = file_set_foldername;
function file_set_foldername(newName, event) {
  // console.log("file_set_foldername: "+newName);
  // console.log(event);
  window.currentProject.foldername = newName;
  document.getElementById("leftSidebar-foldername").value = newName;
}
window.file_set_filename = file_set_filename;
function file_set_filename(newName, event) {
  // console.log("file_set_filename: "+newName);
  // console.log(event);
  window.currentProject.filename = newName;
  document.getElementById("leftSidebar-filename").value = newName;
  if (modeler._definitions) { modeler._definitions.$attrs['diagram-name'] = newName; }
  let diagramName = document.getElementById("diagramName")
  diagramName.value = newName;
  diagramName.size = newName.trim().length;
}
window.file_clean_up = file_clean_up;
function file_clean_up() {
  file_set_username(window.currentProject.username.trim());
  file_set_foldername(window.currentProject.foldername.trim());
  file_set_filename(window.currentProject.filename.trim());
}

function createNewDiagram() {
  openDiagram(diagramXML);
}

function guid() {
  function s4() {
    return Math.floor((1 + Math.random()) * 0x10000)
      .toString(16)
      .substring(1);
  }
  return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
}

modeler.on('element.click', function(event) {
  // console.log("modeler on element click");

  let element = event.element,
      moddle = modeler.get('moddle'),

      // the underlaying BPMN 2.0 element
      businessObject = element.businessObject,
      analysis,
      score,
      message,
      url_ref;

  function getExtension(element, type) {
    if (!element.extensionElements) {
      return null;
    }

    console.log(element.extensionElements);
    return element.extensionElements.values.filter(function(e) {
      return e.$instanceOf(type);
    })[0];
  }

  // analysis = getExtension(businessObject, 'qa:AnalysisDetails');
  // console.log(analysis);

  // if (analysis) {
  //   score = businessObject.suitable;

  //   if (isNaN(score)) {
  //     message = 'No suitability score yet, dblclick to assign one';
  //   } else {
  //     message = 'Diagram element has a suitability score of ' + score;
  //   }

  //   if (analysis) {
  //     message += '\n Last analyzed at ' + analysis.lastChecked;
  //   }

  //   window.alert(message);
  // }

  url_ref = getExtension(businessObject, 'url:ref');
  if (!url_ref) {
    url_ref = moddle.create('url:ref');

    if (!businessObject.extensionElements) {
      businessObject.extensionElements = moddle.create('bpmn:ExtensionElements');
    }

    url_ref.location = "https://www.google.com/";

    businessObject.extensionElements.get('values').push(url_ref);
    console.log(businessObject);
  }
});

modeler.on('import.parse.complete', ({ error, definitions, context }) => {
  // manipulate definitions before import
  if(definitions.id === "sample-diagram") definitions.id = guid();
  // console.log(definitions);
  // document.querySelector('#diagramName').innerText = definitions.$attrs['diagram-name'] || "Diagram Name";
  let newName = definitions.$attrs['diagram-name'] || "Diagram Name";
  file_set_filename(newName);
  file_clean_up();
  // make sure to return the manipulated defintions
  return definitions;
});

// This takes care of updates and changes to the name of the document.  I.E. the file name.
// document.getElementById("diagramName").addEventListener("input", updateFileName, false);
// function updateFileName(event) { modeler._definitions.$attrs['diagram-name'] = document.querySelector('#diagramName').innerText; }

function openDiagram(xml) {
  modeler.importXML(xml, function(err) {
    if (err) {
      container
        .removeClass('with-diagram')
        .addClass('with-error');

      container.find('.error pre').text(err.message);

      console.error(err);
    } else {
      container
        .removeClass('with-error')
        .addClass('with-diagram');
      $('.show-with-diagram').addClass('shown-with-diagram').removeClass('show-with-diagram');
    }

    modeler.on('saveXML.start', ({ definitions }) => {
      definitions.$attrs['diagram-name'] = window.currentProject.filename.trim();
      return definitions;
    });
    modeler.on('saveXML.serialized', ({ xml }) => {
      //this is where we will send it to the database to save
      //console.log(xml);
      return xml;
    });

    modeler.get('canvas').zoom('fit-viewport');
  });
}

function saveSVG(done) {
  modeler.saveSVG(done);
}

function saveDiagram(done) {
  modeler.saveXML({ format: true }, function(err, xml) {
    done(err, xml);
  });
}

function loadFromDB(event) {
  let prompt_filename = window.prompt("Filename to load:");
  prompt_filename = prompt_filename.trim();
  if (prompt_filename === "") return;

  let data_id = {
    "csrfToken": csrfToken,
    "bpmn_username": window.currentProject.username.trim() || "jeremy.mone",
    "bpmn_foldername": window.currentProject.foldername.trim() || "testFolder2",
    "bpmn_filename": prompt_filename.trim()
  };

  apiPost("api/api.php?args=/bpmn_load/true", JSON.stringify(data_id),
    function(result){ // Success
      // console.log('success!');
      // console.log(result);
      let bpmn_json = JSON.parse(result.responseText);
      // console.log(bpmn_json);
      // console.log(bpmn_json.results[0].data.length);
      // console.log(bpmn_json.results[0].data);
      // console.log(atob(bpmn_json.results[0].data));
      // let loaded_xml = decodeURIComponent(atob(bpmn_json.results[0].data));
      let loaded_xml = b64DecodeUnicode(bpmn_json.results[0].data);
      // console.log(loaded_xml);
      openDiagram(loaded_xml);

      file_set_username(bpmn_json.results[0].username);
      file_set_foldername(bpmn_json.results[0].foldername);
      file_set_filename(bpmn_json.results[0].filename);
      file_clean_up();
    },
    function(result){ // Fail
      console.log('The request failed!');
      console.log(xhr);
    }
  );
}

// Added a button in html and this is the setting up and function for the click action of the button to save to db.
function saveToDB(event) {
  // console.log("saveToDB", event);

  modeler.saveXML({ format: true }, function(err, xml) {
    if (err) { console.log(err); } // We got an error so we show an error.
    else if (xml) { // We got data back.  Assuming this is good.
      // var bpmn_base64_xml = btoa(encodeURIComponent(xml));
      var bpmn_base64_xml = b64EncodeUnicode(xml);
      // console.log(bpmn_base64_xml.length);

      file_clean_up();
      
      let data_output = {
        "csrfToken": csrfToken,
        "bpmn_username": window.currentProject.username.trim() || "jeremy.mone",
        "bpmn_foldername": window.currentProject.foldername.trim() || "testFolder2",
        "bpmn_filename": window.currentProject.filename.trim(),
        "bpmn_data": bpmn_base64_xml
      };

      // apiGet("api/api.php?args=/bpmn_load/true", function(result){
      //   console.log('success!', result);
      //   let bpmn_json = JSON.parse(result.responseText);
      //   console.log(bpmn_json);
      // },
      // function(result){
      //   console.log('The request failed!');
      //   console.log(xhr);
      // });

      apiPost("api/api.php?args=/bpmn_update/true", JSON.stringify(data_output),
        function(result){ // Success
          // console.log('success!');
          // console.log(result);
          let bpmn_json = JSON.parse(result.responseText);
          // console.log(bpmn_json);
        },
        function(result){ // Fail
          console.log('The request failed!');
          console.log(xhr);
        }
      );
    } else { // We got nothing on either err or xml.  No idea what would do this, but noting something wrong.
      console.error("saveToDB - modeler.saveXML: err and xml both empty or invalid.");
    }
  });
}

function registerFileDrop(container, callback) {
  function handleFileSelect(e) {
    e.stopPropagation();
    e.preventDefault();

    var files = e.dataTransfer.files;
    var file = files[0];
    var reader = new FileReader();

    reader.onload = function(e) {
      var xml = e.target.result;
      callback(xml);
    };

    reader.readAsText(file);
  }

  function handleDragOver(e) {
    e.stopPropagation();
    e.preventDefault();

    e.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
  }

  container.get(0).addEventListener('dragover', handleDragOver, false);
  container.get(0).addEventListener('drop', handleFileSelect, false);
}

// file drag / drop ///////////////////////
// check file api availability
if (!window.FileList || !window.FileReader) {
  window.alert(
    'Looks like you use an older browser that does not support drag and drop. ' +
    'Try using Chrome, Firefox or the Internet Explorer > 10.');
} else {
  registerFileDrop(container, openDiagram);
}

// bootstrap diagram functions
$(function() {
  $('#js-create-diagram').click(function(e) {
    e.stopPropagation();
    e.preventDefault();

    createNewDiagram();
  });

  var downloadLink = $('#js-download-diagram');
  var downloadSvgLink = $('#js-download-svg');

  $('.buttons a').click(function(e) {
    if (!$(this).is('.active')) {
      e.preventDefault();
      e.stopPropagation();
    }
  });

  function setEncoded(link, name, data) {
    var encodedData = encodeURIComponent(data);

    if (data) {
      link.addClass('active').attr({
        'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
        'download': name.replace(/[\^\/:\*\?"<>\|]/, '-').replace(/ /g, '_')
      });
    } else {
      link.removeClass('active');
    }
  }

  var exportArtifacts = debounce(function() {
    // console.log("exportArtifacts called");
    modelerChanged = true;

    saveSVG(function(err, svg) {
      setEncoded(downloadSvgLink, `${window.currentProject.filename.trim()}.svg`, err ? null : svg);
    });

    saveDiagram(function(err, xml) {
      setEncoded(downloadLink, `${window.currentProject.filename.trim()}.bpmn`, err ? null : xml);
    });
  }, 500);

  $("#js-load-from-db").on("click", loadFromDB);
  $("#js-save-to-db").on("click", saveToDB);

  // This hopefully captures the proper data for things that may not naturally trigger the commandStack.changed event.
  $("#js-download-diagram").on("mouseenter", function() {
    if (modelerChanged) { exportArtifacts(); }
    // console.log("#js-download-diagram mouseenter");
  });
  $("#js-download-svg").on("mouseenter", function() {
    if (modelerChanged) { exportArtifacts(); }
    // console.log("#js-download-svg mouseenter");
  });

  modeler.on('comments.updated', exportArtifacts);
  modeler.on('commandStack.changed', exportArtifacts);

  modelerEventBus.on("element.click", function(e) {
    // e.element = the model element
    // e.gfx = the graphical element

    // console.log("element.click on", e);
    modeler.get('comments').collapseAll();
  });
});

// helpers //////////////////////
function debounce(fn, timeout) {
  var timer;
  return function() {
    if (timer) { clearTimeout(timer); }
    timer = setTimeout(fn, timeout);
  };
}

In that wall of code I have defined two moddle extensions:

{
  "name": "QualityAssurance",
  "uri": "http://some-company/schema/bpmn/qa",
  "prefix": "qa",
  "xml": {
    "tagAlias": "lowerCase"
  },
  "types": [
    {
      "name": "AnalyzedNode",
      "extends": [
        "bpmn:FlowNode"
      ],
      "properties": [
        {
          "name": "suitable",
          "isAttr": true,
          "type": "Float"
        }
      ]
    },
    {
      "name": "AnalysisDetails",
      "superClass": [ "Element" ],
      "properties": [
        {
          "name": "lastChecked",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "nextCheck",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "comments",
          "isMany": true,
          "type": "Comment"
        }
      ]
    },
    {
      "name": "Comment",
      "properties": [
        {
          "name": "author",
          "isAttr": true,
          "type": "String"
        },
        {
          "name": "text",
          "isBody": true,
          "type": "String"
        }
      ]
    }
  ],
  "emumerations": [],
  "associations": []
}
{
  "name": "UrlReference",
  "uri": "http://url_reference",
  "prefix": "url",
  "xml": {
    "tagAlias": "lowerCase"
  },
  "types": [
    {
      "name": "ref",
      "superClass": [ "Element" ],
      "properties": [
        {
          "name": "location",
          "isAttr": true,
          "type": "String"
        }
      ]
    }
  ],
  "emumerations": [],
  "associations": []
}

QA is just from the example to see that it works, and it does. Saves out, and loads in with no issue.

The one I made called URL REF seems to work, and it shows up in the BPMN XML that it saves out when I click to save it. Yet when I try to load it I get this error:

read.js:677 could not parse node
handleError @ read.js:677
handleOpen @ read.js:778
(anonymous) @ read.js:828
parse @ index.esm.js:1005
Parser.parse @ index.esm.js:297
(anonymous) @ read.js:847
setTimeout (async)
defer @ read.js:81
./node_modules/moddle-xml/lib/read.js.Reader.fromXML @ read.js:843
./node_modules/bpmn-moddle/lib/bpmn-moddle.js.BpmnModdle.fromXML @ bpmn-moddle.js:56
./node_modules/bpmn-js/lib/Viewer.js.Viewer.importXML @ Viewer.js:168
openDiagram @ app.js:182
reader.onload @ app.js:320
load (async)
handleFileSelect @ app.js:318
read.js:678 Error: unknown type <url:Ref>
    at Registry../node_modules/moddle/lib/registry.js.Registry.mapTypes (registry.js:159)
    at Registry../node_modules/moddle/lib/registry.js.Registry.getEffectiveDescriptor (registry.js:184)
    at BpmnModdle../node_modules/moddle/lib/moddle.js.Moddle.getType (moddle.js:97)
    at ElementHandler../node_modules/moddle-xml/lib/read.js.ElementHandler.getPropertyForNode (read.js:404)
    at ElementHandler../node_modules/moddle-xml/lib/read.js.ElementHandler.handleChild (read.js:458)
    at ElementHandler../node_modules/moddle-xml/lib/read.js.BaseElementHandler.handleNode (read.js:257)
    at handleOpen (read.js:775)
    at read.js:828
    at parse (index.esm.js:1005)
    at Parser.parse (index.esm.js:297)
handleError @ read.js:678
handleOpen @ read.js:778
(anonymous) @ read.js:828
parse @ index.esm.js:1005
Parser.parse @ index.esm.js:297
(anonymous) @ read.js:847
setTimeout (async)
defer @ read.js:81
./node_modules/moddle-xml/lib/read.js.Reader.fromXML @ read.js:843
./node_modules/bpmn-moddle/lib/bpmn-moddle.js.BpmnModdle.fromXML @ bpmn-moddle.js:56
./node_modules/bpmn-js/lib/Viewer.js.Viewer.importXML @ Viewer.js:168
openDiagram @ app.js:182
reader.onload @ app.js:320
load (async)
handleFileSelect @ app.js:318

I don’t really know why. I guess that I am defining improperly the URL REF definition. Yet that seems odd because it seems to create the xml items without issue, but it can’t seem to understand what they are on load.

This is the file I am trying to load in that produces the error:

<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:url="http://url_reference" xmlns:qa="http://some-company/schema/bpmn/qa" id="6d9687b2-4884-6387-8109-a0afe142b518" targetNamespace="http://bpmn.io/schema/bpmn" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" diagram-name="test_2018-12-14-Dec">
  <bpmn2:process id="Process_1" isExecutable="false">
    <bpmn2:startEvent id="StartEvent_1">
      <bpmn2:extensionElements>
        <url:ref location="https://www.google.com/" />
      </bpmn2:extensionElements>
      <bpmn2:outgoing>SequenceFlow_1a2fxeb</bpmn2:outgoing>
    </bpmn2:startEvent>
    <bpmn2:task id="Task_0fa8dnk">
      <bpmn2:extensionElements>
        <qa:analysisDetails lastChecked="2015-01-20" nextCheck="2015-07-15">
          <qa:comment author="Klaus">Our operators always have a hard time to figure out, what they need to do here.</qa:comment>
          <qa:comment author="Walter">I believe this can be split up in a number of activities and partly automated.</qa:comment>
        </qa:analysisDetails>
      </bpmn2:extensionElements>
      <bpmn2:incoming>SequenceFlow_1a2fxeb</bpmn2:incoming>
      <bpmn2:outgoing>SequenceFlow_0adlmng</bpmn2:outgoing>
    </bpmn2:task>
    <bpmn2:endEvent id="EndEvent_0cl9wo8">
      <bpmn2:incoming>SequenceFlow_0adlmng</bpmn2:incoming>
    </bpmn2:endEvent>
    <bpmn2:sequenceFlow id="SequenceFlow_1a2fxeb" sourceRef="StartEvent_1" targetRef="Task_0fa8dnk" />
    <bpmn2:sequenceFlow id="SequenceFlow_0adlmng" sourceRef="Task_0fa8dnk" targetRef="EndEvent_0cl9wo8" />
  </bpmn2:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds x="412" y="240" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Task_0fa8dnk_di" bpmnElement="Task_0fa8dnk">
        <dc:Bounds x="505" y="218" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="EndEvent_0cl9wo8_di" bpmnElement="EndEvent_0cl9wo8">
        <dc:Bounds x="670" y="240" width="36" height="36" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="SequenceFlow_1a2fxeb_di" bpmnElement="SequenceFlow_1a2fxeb">
        <di:waypoint x="448" y="258" />
        <di:waypoint x="505" y="258" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="SequenceFlow_0adlmng_di" bpmnElement="SequenceFlow_0adlmng">
        <di:waypoint x="605" y="258" />
        <di:waypoint x="670" y="258" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn2:definitions>

As you can see… the QA parts are still in there, and the newly created URL REFs are in there. I don’t seem to get an error for the QA on the load, but the URL REFs do error on load. I feel like I am doing the same thing in both cases… and I don’t know why one works, and the other doesn’t.

If you need me to grab in more code and drop it, then I can.

Any help or suggestions on this?

x Jeremy M.


#20

The problem is within the moddle extension. If you want your type name to be lowercased (like "name": "ref"), you should never use "tagAlias": "lowerCase". Instead of that, either remove the whole tagAlias or set it to upperCase.

Best,

Maciej