Latest commit

History

History
345 lines (255 loc) · 11.5 KB

File metadata and controls

345 lines (255 loc) · 11.5 KB

ProcessMaker BPMN modeler

CircleCI

@processmaker/modeler is a Vue.js based BPMN modeler scaffolded using Vue CLI 3.

Project setup

Clone the repository and cd into the modeler directory:

git clone git@github.com:ProcessMaker/modeler.git
cd modeler

You can now run any of the below commands. Most commands make calls vue-cli-service. You can read more about those commands at https://cli.vuejs.org/guide/cli-service.html#cli-service.

# Compile the app and start a local development server
npm run serve
# Create a production build of the app to be distributed as an npm package
npm run build-bundle
# Report and fix ESLint errors
npm run lint

Docker Env

If you prefer to run Modeler using Docker

# Using Docker Compose
docker compose up

Testing

Unit tests are set up using jest and end-to-end tests are set up using Cypress. Unit and end-to-end tests can be run separately or together. Code coverage is collected for both types of tests and combined into a single coverage report for the entire project.

Tests can be run locally with the following commands:

# Run the Jest unit test suite
npm run test-unit
# Open Cypress to run the end-to-end (e2e) test suite
npm run open-cypress
# Run the Cypress end-to-end (e2e) test suite in headless mode
npm run test-ci
# Run the Jest unit test tests and then the Cypress tests in headless mode
npm test

Tests are run automatically on CircleCI when new branches are created and updated. The CI uses the npm test command to run both the unit and e2e test suites and to collect code coverage.

For more information on Cypress, visit https://cli.vuejs.org/config/#cypress.

For more information on Jest, visit https://jestjs.io.

Extending the Modeler with modeler-init

The modeler-init event is the main entry point for plugins and extensions to customize the modeler at runtime. It provides several extension points, including:

  • Registering custom nodes
  • Registering BPMN extensions
  • Registering inspector extensions
  • Registering custom dropdown items for nodes

Unified Example

Below is a unified example that demonstrates how to use modeler-init to register a custom node and, after the process is loaded, inject a custom dropdown item for a Start Event node.

importMyCustomNodeConfigfrom'./MyCustomNodeConfig';// Listen for modeler-init to register nodes and extensionswindow.ProcessMaker.EventBus.$on('modeler-init',({ registerNode, registerCustomDropdownData })=>{// Register a custom node (if needed)registerNode(MyCustomNodeConfig);// Wait for the process to load before injecting dropdown itemswindow.ProcessMaker.EventBus.$on('modeler-start',({ modeler })=>{// Find the first Start Event node (or your custom node)conststartEventNode=modeler.$store.getters.nodes.find(node=>node.type==='processmaker-modeler-start-event');if(startEventNode){// Inject a custom dropdown itemmodeler.registerCustomDropdownData(startEventNode,{label: 'Custom Start Option',id: 'custom-start-option',dataTest: 'switch-to-custom-start-option',});}});});
  • Use registerNode to add custom nodes.
  • Use registerCustomDropdownData to add dropdown items, but only after the process and nodes are loaded (in modeler-start).
  • You can also use other extension points provided in the modeler-init payload as needed.

Architecture

The entry point for the application is src/main.js; this is the "starting point" which is used when running npm run serve or npm run build.

Global event bus

window.ProcessMaker.EventBus points to an independent Vue instance which acts as the application's global event bus. The modeler currently emits two events on the event bus which can be listened to in application code to hook into, customize, and extend the modeler's behaviour: modeler-init, and modeler-start.

modeler-init

This event is fired before the modeler and BpmnModdle are set up and mounted. Listeners to this event are passed an object with three methods: registerInspectorExtension, registerBpmnExtension, and registerNode.

importbpmnExtensionfrom'@processmaker/processmaker-bpmn-moddle/resources/processmaker.json';import{intermediateMessageCatchEvent}from'@/components/nodes';window.ProcessMaker.EventBus.$on('modeler-init',({ registerBpmnExtension, registerNode, registerInspectorExtension })=>{/* Add a BPMN extension */registerBpmnExtension('pm',bpmnExtension);/* Register a component to be used in the modeler */registerNode(intermediateMessageCatchEvent);/* Add custom properties to inspector */registerInspectorExtension(intermediateMessageCatchEvent,{id: 'pm-condition',component: 'FormInput',config: {label: 'Condition',helper: 'Expression to be evaluated on webhook to activate event. Leave blank to accept always.',name: 'pm:condition',},});});

modeler-start

This event is fired after the modeler has been set up and mounted. Listeners to this event are passed an object with a single method, loadXML.

window.ProcessMaker.EventBus.$on('modeler-start',({ loadXML })=>{loadXML('<?xml version="1.0" encoding="UTF-8"?> ... </bpmn:definitions>');});

For the modeler to function correctly, loadXML must be called when the application loads.

modeler-validate

This event is fired during validation, and can be used to add custom validation rules. See Adding validation rules during runtime.

modeler-change

This event is fired anytime a change is made to the modeler that causes the underlying XML to change. This event is fired immediately after new state is pushed to the undo/redo stack.

window.ProcessMaker.EventBus.$on('modeler-change',()=>{console.log('The diagram has changed');});

Undo/redo store

The undo/redo feature is implemented using Vuex, with the undo/redo Vuex store initialized in src/undoRedoStore.js. The undo/redo store keeps track of every change in the underlying BPMN XML, recording a copy of the XML string in a stack. Traversing the undo/redo stack simply uses the loadXML function to load the XML string from the current position in the stack.

Validation

Adding a new lint rule

By default, the modeler automatically validates your diagram as you create it. This validation can be toggled on and off using the switch in the status bar. Validation is handled using https://github.com/bpmn-io/bpmnlint.

To add a new validation rule, create a new file in processmaker-plugin/rules named after your rule, for example, node-id.js. This file should export a function that returns an object with a check method. The check method will receive two arguments—node and reporter—and must return undefined if validation passes, or reporter.report to raise an error. For exmaple:

// processmaker-plugin/rules/node-id.js/** * Rule that checks node IDs start with "node_" */module.exports=function(){functioncheck(node,reporter){if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}}return{ check };};

When you are done writing the rule, add it to processmaker-plugin/index.js:

module.exports={configs: {all: {rules: {'processmaker/custom-validation': 'error','processmaker/gateway-direction': 'error','processmaker/node-id': 'error',},},},};

For more examples, see the list of default rules at https://github.com/bpmn-io/bpmnlint/tree/master/rules.

Adding validation rules during runtime

To add custom validation when the linter runs, use the global event bus:

window.ProcessMaker.EventBus.$on('modeler-validate',(node,reporter)=>{if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}});

Examples

Adding a new component

Creating a new modeler component requires creating a Vue component, a component config, and calling the registerNode method to register your component.

First, create your component config, and save it in a .js file:

// CustomComponentConfig.jsexportdefault{// A unique ID that will be used to identify your componentid: 'unique-custom-component-id',// A reference to the Vue component representing your componentcomponent: CustomComponent,// The bpmn type for your component, which will be used to save the component under in XML// It has the form prefix:namebpmnType: 'custom-namespace:CustomComponent',// A toggle to show/hide the component in the controls panelcontrol: true,// The category to place the component under in the controls panelcategory: 'BPMN',// The icon representing the component in the controls panelicon: require('@/assets/toolpanel/scriptTask.svg'),// The label for the component in the controls panellabel: 'Script Task',// The function used to create the BPMN definition object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddledefinition(moddle){returnmoddle.create('bpmn:ScriptTask',{name: 'Script Task',});},// The function used to create the BPMN diagram object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddlediagram(moddle){returnmoddle.create('bpmndi:BPMNShape',{bounds: moddle.create('dc:Bounds',{height: taskHeight,width: 116,}),});},// The configuration for the inspector panelinspectorConfig: [{name: 'CustomComponent',items: [// Each item corresponds to a form element. {// Component can be a custom Vue component or a reference to a form component from @processmaker/vue-form-elementscomponent: 'FormText',config: {label: 'Custom Component Label',fontSize: '2em',},},// ...],},// ...],};

Then, create a Vue component for your custom element:

// CustomComponent.vue
<template>
<div />
</template>
<script>importTaskfrom'@/components/nodes/task/task';exportdefault { extends: Task,mounted() {// Do things with this.shape },};</script>

Finally, register your custom component:

registerNode(CustomComponentConfig);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

History
345 lines (255 loc) · 11.5 KB

File metadata and controls

345 lines (255 loc) · 11.5 KB

ProcessMaker BPMN modeler

CircleCI

@processmaker/modeler is a Vue.js based BPMN modeler scaffolded using Vue CLI 3.

Project setup

Clone the repository and cd into the modeler directory:

git clone git@github.com:ProcessMaker/modeler.git
cd modeler

You can now run any of the below commands. Most commands make calls vue-cli-service. You can read more about those commands at https://cli.vuejs.org/guide/cli-service.html#cli-service.

# Compile the app and start a local development server
npm run serve
# Create a production build of the app to be distributed as an npm package
npm run build-bundle
# Report and fix ESLint errors
npm run lint

Docker Env

If you prefer to run Modeler using Docker

# Using Docker Compose
docker compose up

Testing

Unit tests are set up using jest and end-to-end tests are set up using Cypress. Unit and end-to-end tests can be run separately or together. Code coverage is collected for both types of tests and combined into a single coverage report for the entire project.

Tests can be run locally with the following commands:

# Run the Jest unit test suite
npm run test-unit
# Open Cypress to run the end-to-end (e2e) test suite
npm run open-cypress
# Run the Cypress end-to-end (e2e) test suite in headless mode
npm run test-ci
# Run the Jest unit test tests and then the Cypress tests in headless mode
npm test

Tests are run automatically on CircleCI when new branches are created and updated. The CI uses the npm test command to run both the unit and e2e test suites and to collect code coverage.

For more information on Cypress, visit https://cli.vuejs.org/config/#cypress.

For more information on Jest, visit https://jestjs.io.

Extending the Modeler with modeler-init

The modeler-init event is the main entry point for plugins and extensions to customize the modeler at runtime. It provides several extension points, including:

  • Registering custom nodes
  • Registering BPMN extensions
  • Registering inspector extensions
  • Registering custom dropdown items for nodes

Unified Example

Below is a unified example that demonstrates how to use modeler-init to register a custom node and, after the process is loaded, inject a custom dropdown item for a Start Event node.

importMyCustomNodeConfigfrom'./MyCustomNodeConfig';// Listen for modeler-init to register nodes and extensionswindow.ProcessMaker.EventBus.$on('modeler-init',({ registerNode, registerCustomDropdownData })=>{// Register a custom node (if needed)registerNode(MyCustomNodeConfig);// Wait for the process to load before injecting dropdown itemswindow.ProcessMaker.EventBus.$on('modeler-start',({ modeler })=>{// Find the first Start Event node (or your custom node)conststartEventNode=modeler.$store.getters.nodes.find(node=>node.type==='processmaker-modeler-start-event');if(startEventNode){// Inject a custom dropdown itemmodeler.registerCustomDropdownData(startEventNode,{label: 'Custom Start Option',id: 'custom-start-option',dataTest: 'switch-to-custom-start-option',});}});});
  • Use registerNode to add custom nodes.
  • Use registerCustomDropdownData to add dropdown items, but only after the process and nodes are loaded (in modeler-start).
  • You can also use other extension points provided in the modeler-init payload as needed.

Architecture

The entry point for the application is src/main.js; this is the "starting point" which is used when running npm run serve or npm run build.

Global event bus

window.ProcessMaker.EventBus points to an independent Vue instance which acts as the application's global event bus. The modeler currently emits two events on the event bus which can be listened to in application code to hook into, customize, and extend the modeler's behaviour: modeler-init, and modeler-start.

modeler-init

This event is fired before the modeler and BpmnModdle are set up and mounted. Listeners to this event are passed an object with three methods: registerInspectorExtension, registerBpmnExtension, and registerNode.

importbpmnExtensionfrom'@processmaker/processmaker-bpmn-moddle/resources/processmaker.json';import{intermediateMessageCatchEvent}from'@/components/nodes';window.ProcessMaker.EventBus.$on('modeler-init',({ registerBpmnExtension, registerNode, registerInspectorExtension })=>{/* Add a BPMN extension */registerBpmnExtension('pm',bpmnExtension);/* Register a component to be used in the modeler */registerNode(intermediateMessageCatchEvent);/* Add custom properties to inspector */registerInspectorExtension(intermediateMessageCatchEvent,{id: 'pm-condition',component: 'FormInput',config: {label: 'Condition',helper: 'Expression to be evaluated on webhook to activate event. Leave blank to accept always.',name: 'pm:condition',},});});

modeler-start

This event is fired after the modeler has been set up and mounted. Listeners to this event are passed an object with a single method, loadXML.

window.ProcessMaker.EventBus.$on('modeler-start',({ loadXML })=>{loadXML('<?xml version="1.0" encoding="UTF-8"?> ... </bpmn:definitions>');});

For the modeler to function correctly, loadXML must be called when the application loads.

modeler-validate

This event is fired during validation, and can be used to add custom validation rules. See Adding validation rules during runtime.

modeler-change

This event is fired anytime a change is made to the modeler that causes the underlying XML to change. This event is fired immediately after new state is pushed to the undo/redo stack.

window.ProcessMaker.EventBus.$on('modeler-change',()=>{console.log('The diagram has changed');});

Undo/redo store

The undo/redo feature is implemented using Vuex, with the undo/redo Vuex store initialized in src/undoRedoStore.js. The undo/redo store keeps track of every change in the underlying BPMN XML, recording a copy of the XML string in a stack. Traversing the undo/redo stack simply uses the loadXML function to load the XML string from the current position in the stack.

Validation

Adding a new lint rule

By default, the modeler automatically validates your diagram as you create it. This validation can be toggled on and off using the switch in the status bar. Validation is handled using https://github.com/bpmn-io/bpmnlint.

To add a new validation rule, create a new file in processmaker-plugin/rules named after your rule, for example, node-id.js. This file should export a function that returns an object with a check method. The check method will receive two arguments—node and reporter—and must return undefined if validation passes, or reporter.report to raise an error. For exmaple:

// processmaker-plugin/rules/node-id.js/** * Rule that checks node IDs start with "node_" */module.exports=function(){functioncheck(node,reporter){if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}}return{ check };};

When you are done writing the rule, add it to processmaker-plugin/index.js:

module.exports={configs: {all: {rules: {'processmaker/custom-validation': 'error','processmaker/gateway-direction': 'error','processmaker/node-id': 'error',},},},};

For more examples, see the list of default rules at https://github.com/bpmn-io/bpmnlint/tree/master/rules.

Adding validation rules during runtime

To add custom validation when the linter runs, use the global event bus:

window.ProcessMaker.EventBus.$on('modeler-validate',(node,reporter)=>{if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}});

Examples

Adding a new component

Creating a new modeler component requires creating a Vue component, a component config, and calling the registerNode method to register your component.

First, create your component config, and save it in a .js file:

// CustomComponentConfig.jsexportdefault{// A unique ID that will be used to identify your componentid: 'unique-custom-component-id',// A reference to the Vue component representing your componentcomponent: CustomComponent,// The bpmn type for your component, which will be used to save the component under in XML// It has the form prefix:namebpmnType: 'custom-namespace:CustomComponent',// A toggle to show/hide the component in the controls panelcontrol: true,// The category to place the component under in the controls panelcategory: 'BPMN',// The icon representing the component in the controls panelicon: require('@/assets/toolpanel/scriptTask.svg'),// The label for the component in the controls panellabel: 'Script Task',// The function used to create the BPMN definition object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddledefinition(moddle){returnmoddle.create('bpmn:ScriptTask',{name: 'Script Task',});},// The function used to create the BPMN diagram object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddlediagram(moddle){returnmoddle.create('bpmndi:BPMNShape',{bounds: moddle.create('dc:Bounds',{height: taskHeight,width: 116,}),});},// The configuration for the inspector panelinspectorConfig: [{name: 'CustomComponent',items: [// Each item corresponds to a form element. {// Component can be a custom Vue component or a reference to a form component from @processmaker/vue-form-elementscomponent: 'FormText',config: {label: 'Custom Component Label',fontSize: '2em',},},// ...],},// ...],};

Then, create a Vue component for your custom element:

// CustomComponent.vue
<template>
<div />
</template>
<script>importTaskfrom'@/components/nodes/task/task';exportdefault { extends: Task,mounted() {// Do things with this.shape },};</script>

Finally, register your custom component:

registerNode(CustomComponentConfig);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

History
345 lines (255 loc) · 11.5 KB

File metadata and controls

345 lines (255 loc) · 11.5 KB

ProcessMaker BPMN modeler

CircleCI

@processmaker/modeler is a Vue.js based BPMN modeler scaffolded using Vue CLI 3.

Project setup

Clone the repository and cd into the modeler directory:

git clone git@github.com:ProcessMaker/modeler.git
cd modeler

You can now run any of the below commands. Most commands make calls vue-cli-service. You can read more about those commands at https://cli.vuejs.org/guide/cli-service.html#cli-service.

# Compile the app and start a local development server
npm run serve
# Create a production build of the app to be distributed as an npm package
npm run build-bundle
# Report and fix ESLint errors
npm run lint

Docker Env

If you prefer to run Modeler using Docker

# Using Docker Compose
docker compose up

Testing

Unit tests are set up using jest and end-to-end tests are set up using Cypress. Unit and end-to-end tests can be run separately or together. Code coverage is collected for both types of tests and combined into a single coverage report for the entire project.

Tests can be run locally with the following commands:

# Run the Jest unit test suite
npm run test-unit
# Open Cypress to run the end-to-end (e2e) test suite
npm run open-cypress
# Run the Cypress end-to-end (e2e) test suite in headless mode
npm run test-ci
# Run the Jest unit test tests and then the Cypress tests in headless mode
npm test

Tests are run automatically on CircleCI when new branches are created and updated. The CI uses the npm test command to run both the unit and e2e test suites and to collect code coverage.

For more information on Cypress, visit https://cli.vuejs.org/config/#cypress.

For more information on Jest, visit https://jestjs.io.

Extending the Modeler with modeler-init

The modeler-init event is the main entry point for plugins and extensions to customize the modeler at runtime. It provides several extension points, including:

  • Registering custom nodes
  • Registering BPMN extensions
  • Registering inspector extensions
  • Registering custom dropdown items for nodes

Unified Example

Below is a unified example that demonstrates how to use modeler-init to register a custom node and, after the process is loaded, inject a custom dropdown item for a Start Event node.

importMyCustomNodeConfigfrom'./MyCustomNodeConfig';// Listen for modeler-init to register nodes and extensionswindow.ProcessMaker.EventBus.$on('modeler-init',({ registerNode, registerCustomDropdownData })=>{// Register a custom node (if needed)registerNode(MyCustomNodeConfig);// Wait for the process to load before injecting dropdown itemswindow.ProcessMaker.EventBus.$on('modeler-start',({ modeler })=>{// Find the first Start Event node (or your custom node)conststartEventNode=modeler.$store.getters.nodes.find(node=>node.type==='processmaker-modeler-start-event');if(startEventNode){// Inject a custom dropdown itemmodeler.registerCustomDropdownData(startEventNode,{label: 'Custom Start Option',id: 'custom-start-option',dataTest: 'switch-to-custom-start-option',});}});});
  • Use registerNode to add custom nodes.
  • Use registerCustomDropdownData to add dropdown items, but only after the process and nodes are loaded (in modeler-start).
  • You can also use other extension points provided in the modeler-init payload as needed.

Architecture

The entry point for the application is src/main.js; this is the "starting point" which is used when running npm run serve or npm run build.

Global event bus

window.ProcessMaker.EventBus points to an independent Vue instance which acts as the application's global event bus. The modeler currently emits two events on the event bus which can be listened to in application code to hook into, customize, and extend the modeler's behaviour: modeler-init, and modeler-start.

modeler-init

This event is fired before the modeler and BpmnModdle are set up and mounted. Listeners to this event are passed an object with three methods: registerInspectorExtension, registerBpmnExtension, and registerNode.

importbpmnExtensionfrom'@processmaker/processmaker-bpmn-moddle/resources/processmaker.json';import{intermediateMessageCatchEvent}from'@/components/nodes';window.ProcessMaker.EventBus.$on('modeler-init',({ registerBpmnExtension, registerNode, registerInspectorExtension })=>{/* Add a BPMN extension */registerBpmnExtension('pm',bpmnExtension);/* Register a component to be used in the modeler */registerNode(intermediateMessageCatchEvent);/* Add custom properties to inspector */registerInspectorExtension(intermediateMessageCatchEvent,{id: 'pm-condition',component: 'FormInput',config: {label: 'Condition',helper: 'Expression to be evaluated on webhook to activate event. Leave blank to accept always.',name: 'pm:condition',},});});

modeler-start

This event is fired after the modeler has been set up and mounted. Listeners to this event are passed an object with a single method, loadXML.

window.ProcessMaker.EventBus.$on('modeler-start',({ loadXML })=>{loadXML('<?xml version="1.0" encoding="UTF-8"?> ... </bpmn:definitions>');});

For the modeler to function correctly, loadXML must be called when the application loads.

modeler-validate

This event is fired during validation, and can be used to add custom validation rules. See Adding validation rules during runtime.

modeler-change

This event is fired anytime a change is made to the modeler that causes the underlying XML to change. This event is fired immediately after new state is pushed to the undo/redo stack.

window.ProcessMaker.EventBus.$on('modeler-change',()=>{console.log('The diagram has changed');});

Undo/redo store

The undo/redo feature is implemented using Vuex, with the undo/redo Vuex store initialized in src/undoRedoStore.js. The undo/redo store keeps track of every change in the underlying BPMN XML, recording a copy of the XML string in a stack. Traversing the undo/redo stack simply uses the loadXML function to load the XML string from the current position in the stack.

Validation

Adding a new lint rule

By default, the modeler automatically validates your diagram as you create it. This validation can be toggled on and off using the switch in the status bar. Validation is handled using https://github.com/bpmn-io/bpmnlint.

To add a new validation rule, create a new file in processmaker-plugin/rules named after your rule, for example, node-id.js. This file should export a function that returns an object with a check method. The check method will receive two arguments—node and reporter—and must return undefined if validation passes, or reporter.report to raise an error. For exmaple:

// processmaker-plugin/rules/node-id.js/** * Rule that checks node IDs start with "node_" */module.exports=function(){functioncheck(node,reporter){if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}}return{ check };};

When you are done writing the rule, add it to processmaker-plugin/index.js:

module.exports={configs: {all: {rules: {'processmaker/custom-validation': 'error','processmaker/gateway-direction': 'error','processmaker/node-id': 'error',},},},};

For more examples, see the list of default rules at https://github.com/bpmn-io/bpmnlint/tree/master/rules.

Adding validation rules during runtime

To add custom validation when the linter runs, use the global event bus:

window.ProcessMaker.EventBus.$on('modeler-validate',(node,reporter)=>{if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}});

Examples

Adding a new component

Creating a new modeler component requires creating a Vue component, a component config, and calling the registerNode method to register your component.

First, create your component config, and save it in a .js file:

// CustomComponentConfig.jsexportdefault{// A unique ID that will be used to identify your componentid: 'unique-custom-component-id',// A reference to the Vue component representing your componentcomponent: CustomComponent,// The bpmn type for your component, which will be used to save the component under in XML// It has the form prefix:namebpmnType: 'custom-namespace:CustomComponent',// A toggle to show/hide the component in the controls panelcontrol: true,// The category to place the component under in the controls panelcategory: 'BPMN',// The icon representing the component in the controls panelicon: require('@/assets/toolpanel/scriptTask.svg'),// The label for the component in the controls panellabel: 'Script Task',// The function used to create the BPMN definition object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddledefinition(moddle){returnmoddle.create('bpmn:ScriptTask',{name: 'Script Task',});},// The function used to create the BPMN diagram object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddlediagram(moddle){returnmoddle.create('bpmndi:BPMNShape',{bounds: moddle.create('dc:Bounds',{height: taskHeight,width: 116,}),});},// The configuration for the inspector panelinspectorConfig: [{name: 'CustomComponent',items: [// Each item corresponds to a form element. {// Component can be a custom Vue component or a reference to a form component from @processmaker/vue-form-elementscomponent: 'FormText',config: {label: 'Custom Component Label',fontSize: '2em',},},// ...],},// ...],};

Then, create a Vue component for your custom element:

// CustomComponent.vue
<template>
<div />
</template>
<script>importTaskfrom'@/components/nodes/task/task';exportdefault { extends: Task,mounted() {// Do things with this.shape },};</script>

Finally, register your custom component:

registerNode(CustomComponentConfig);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

History
345 lines (255 loc) · 11.5 KB

File metadata and controls

345 lines (255 loc) · 11.5 KB

ProcessMaker BPMN modeler

CircleCI

@processmaker/modeler is a Vue.js based BPMN modeler scaffolded using Vue CLI 3.

Project setup

Clone the repository and cd into the modeler directory:

git clone git@github.com:ProcessMaker/modeler.git
cd modeler

You can now run any of the below commands. Most commands make calls vue-cli-service. You can read more about those commands at https://cli.vuejs.org/guide/cli-service.html#cli-service.

# Compile the app and start a local development server
npm run serve
# Create a production build of the app to be distributed as an npm package
npm run build-bundle
# Report and fix ESLint errors
npm run lint

Docker Env

If you prefer to run Modeler using Docker

# Using Docker Compose
docker compose up

Testing

Unit tests are set up using jest and end-to-end tests are set up using Cypress. Unit and end-to-end tests can be run separately or together. Code coverage is collected for both types of tests and combined into a single coverage report for the entire project.

Tests can be run locally with the following commands:

# Run the Jest unit test suite
npm run test-unit
# Open Cypress to run the end-to-end (e2e) test suite
npm run open-cypress
# Run the Cypress end-to-end (e2e) test suite in headless mode
npm run test-ci
# Run the Jest unit test tests and then the Cypress tests in headless mode
npm test

Tests are run automatically on CircleCI when new branches are created and updated. The CI uses the npm test command to run both the unit and e2e test suites and to collect code coverage.

For more information on Cypress, visit https://cli.vuejs.org/config/#cypress.

For more information on Jest, visit https://jestjs.io.

Extending the Modeler with modeler-init

The modeler-init event is the main entry point for plugins and extensions to customize the modeler at runtime. It provides several extension points, including:

  • Registering custom nodes
  • Registering BPMN extensions
  • Registering inspector extensions
  • Registering custom dropdown items for nodes

Unified Example

Below is a unified example that demonstrates how to use modeler-init to register a custom node and, after the process is loaded, inject a custom dropdown item for a Start Event node.

importMyCustomNodeConfigfrom'./MyCustomNodeConfig';// Listen for modeler-init to register nodes and extensionswindow.ProcessMaker.EventBus.$on('modeler-init',({ registerNode, registerCustomDropdownData })=>{// Register a custom node (if needed)registerNode(MyCustomNodeConfig);// Wait for the process to load before injecting dropdown itemswindow.ProcessMaker.EventBus.$on('modeler-start',({ modeler })=>{// Find the first Start Event node (or your custom node)conststartEventNode=modeler.$store.getters.nodes.find(node=>node.type==='processmaker-modeler-start-event');if(startEventNode){// Inject a custom dropdown itemmodeler.registerCustomDropdownData(startEventNode,{label: 'Custom Start Option',id: 'custom-start-option',dataTest: 'switch-to-custom-start-option',});}});});
  • Use registerNode to add custom nodes.
  • Use registerCustomDropdownData to add dropdown items, but only after the process and nodes are loaded (in modeler-start).
  • You can also use other extension points provided in the modeler-init payload as needed.

Architecture

The entry point for the application is src/main.js; this is the "starting point" which is used when running npm run serve or npm run build.

Global event bus

window.ProcessMaker.EventBus points to an independent Vue instance which acts as the application's global event bus. The modeler currently emits two events on the event bus which can be listened to in application code to hook into, customize, and extend the modeler's behaviour: modeler-init, and modeler-start.

modeler-init

This event is fired before the modeler and BpmnModdle are set up and mounted. Listeners to this event are passed an object with three methods: registerInspectorExtension, registerBpmnExtension, and registerNode.

importbpmnExtensionfrom'@processmaker/processmaker-bpmn-moddle/resources/processmaker.json';import{intermediateMessageCatchEvent}from'@/components/nodes';window.ProcessMaker.EventBus.$on('modeler-init',({ registerBpmnExtension, registerNode, registerInspectorExtension })=>{/* Add a BPMN extension */registerBpmnExtension('pm',bpmnExtension);/* Register a component to be used in the modeler */registerNode(intermediateMessageCatchEvent);/* Add custom properties to inspector */registerInspectorExtension(intermediateMessageCatchEvent,{id: 'pm-condition',component: 'FormInput',config: {label: 'Condition',helper: 'Expression to be evaluated on webhook to activate event. Leave blank to accept always.',name: 'pm:condition',},});});

modeler-start

This event is fired after the modeler has been set up and mounted. Listeners to this event are passed an object with a single method, loadXML.

window.ProcessMaker.EventBus.$on('modeler-start',({ loadXML })=>{loadXML('<?xml version="1.0" encoding="UTF-8"?> ... </bpmn:definitions>');});

For the modeler to function correctly, loadXML must be called when the application loads.

modeler-validate

This event is fired during validation, and can be used to add custom validation rules. See Adding validation rules during runtime.

modeler-change

This event is fired anytime a change is made to the modeler that causes the underlying XML to change. This event is fired immediately after new state is pushed to the undo/redo stack.

window.ProcessMaker.EventBus.$on('modeler-change',()=>{console.log('The diagram has changed');});

Undo/redo store

The undo/redo feature is implemented using Vuex, with the undo/redo Vuex store initialized in src/undoRedoStore.js. The undo/redo store keeps track of every change in the underlying BPMN XML, recording a copy of the XML string in a stack. Traversing the undo/redo stack simply uses the loadXML function to load the XML string from the current position in the stack.

Validation

Adding a new lint rule

By default, the modeler automatically validates your diagram as you create it. This validation can be toggled on and off using the switch in the status bar. Validation is handled using https://github.com/bpmn-io/bpmnlint.

To add a new validation rule, create a new file in processmaker-plugin/rules named after your rule, for example, node-id.js. This file should export a function that returns an object with a check method. The check method will receive two arguments—node and reporter—and must return undefined if validation passes, or reporter.report to raise an error. For exmaple:

// processmaker-plugin/rules/node-id.js/** * Rule that checks node IDs start with "node_" */module.exports=function(){functioncheck(node,reporter){if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}}return{ check };};

When you are done writing the rule, add it to processmaker-plugin/index.js:

module.exports={configs: {all: {rules: {'processmaker/custom-validation': 'error','processmaker/gateway-direction': 'error','processmaker/node-id': 'error',},},},};

For more examples, see the list of default rules at https://github.com/bpmn-io/bpmnlint/tree/master/rules.

Adding validation rules during runtime

To add custom validation when the linter runs, use the global event bus:

window.ProcessMaker.EventBus.$on('modeler-validate',(node,reporter)=>{if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}});

Examples

Adding a new component

Creating a new modeler component requires creating a Vue component, a component config, and calling the registerNode method to register your component.

First, create your component config, and save it in a .js file:

// CustomComponentConfig.jsexportdefault{// A unique ID that will be used to identify your componentid: 'unique-custom-component-id',// A reference to the Vue component representing your componentcomponent: CustomComponent,// The bpmn type for your component, which will be used to save the component under in XML// It has the form prefix:namebpmnType: 'custom-namespace:CustomComponent',// A toggle to show/hide the component in the controls panelcontrol: true,// The category to place the component under in the controls panelcategory: 'BPMN',// The icon representing the component in the controls panelicon: require('@/assets/toolpanel/scriptTask.svg'),// The label for the component in the controls panellabel: 'Script Task',// The function used to create the BPMN definition object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddledefinition(moddle){returnmoddle.create('bpmn:ScriptTask',{name: 'Script Task',});},// The function used to create the BPMN diagram object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddlediagram(moddle){returnmoddle.create('bpmndi:BPMNShape',{bounds: moddle.create('dc:Bounds',{height: taskHeight,width: 116,}),});},// The configuration for the inspector panelinspectorConfig: [{name: 'CustomComponent',items: [// Each item corresponds to a form element. {// Component can be a custom Vue component or a reference to a form component from @processmaker/vue-form-elementscomponent: 'FormText',config: {label: 'Custom Component Label',fontSize: '2em',},},// ...],},// ...],};

Then, create a Vue component for your custom element:

// CustomComponent.vue
<template>
<div />
</template>
<script>importTaskfrom'@/components/nodes/task/task';exportdefault { extends: Task,mounted() {// Do things with this.shape },};</script>

Finally, register your custom component:

registerNode(CustomComponentConfig);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

History
345 lines (255 loc) · 11.5 KB

File metadata and controls

345 lines (255 loc) · 11.5 KB

ProcessMaker BPMN modeler

CircleCI

@processmaker/modeler is a Vue.js based BPMN modeler scaffolded using Vue CLI 3.

Project setup

Clone the repository and cd into the modeler directory:

git clone git@github.com:ProcessMaker/modeler.git
cd modeler

You can now run any of the below commands. Most commands make calls vue-cli-service. You can read more about those commands at https://cli.vuejs.org/guide/cli-service.html#cli-service.

# Compile the app and start a local development server
npm run serve
# Create a production build of the app to be distributed as an npm package
npm run build-bundle
# Report and fix ESLint errors
npm run lint

Docker Env

If you prefer to run Modeler using Docker

# Using Docker Compose
docker compose up

Testing

Unit tests are set up using jest and end-to-end tests are set up using Cypress. Unit and end-to-end tests can be run separately or together. Code coverage is collected for both types of tests and combined into a single coverage report for the entire project.

Tests can be run locally with the following commands:

# Run the Jest unit test suite
npm run test-unit
# Open Cypress to run the end-to-end (e2e) test suite
npm run open-cypress
# Run the Cypress end-to-end (e2e) test suite in headless mode
npm run test-ci
# Run the Jest unit test tests and then the Cypress tests in headless mode
npm test

Tests are run automatically on CircleCI when new branches are created and updated. The CI uses the npm test command to run both the unit and e2e test suites and to collect code coverage.

For more information on Cypress, visit https://cli.vuejs.org/config/#cypress.

For more information on Jest, visit https://jestjs.io.

Extending the Modeler with modeler-init

The modeler-init event is the main entry point for plugins and extensions to customize the modeler at runtime. It provides several extension points, including:

  • Registering custom nodes
  • Registering BPMN extensions
  • Registering inspector extensions
  • Registering custom dropdown items for nodes

Unified Example

Below is a unified example that demonstrates how to use modeler-init to register a custom node and, after the process is loaded, inject a custom dropdown item for a Start Event node.

importMyCustomNodeConfigfrom'./MyCustomNodeConfig';// Listen for modeler-init to register nodes and extensionswindow.ProcessMaker.EventBus.$on('modeler-init',({ registerNode, registerCustomDropdownData })=>{// Register a custom node (if needed)registerNode(MyCustomNodeConfig);// Wait for the process to load before injecting dropdown itemswindow.ProcessMaker.EventBus.$on('modeler-start',({ modeler })=>{// Find the first Start Event node (or your custom node)conststartEventNode=modeler.$store.getters.nodes.find(node=>node.type==='processmaker-modeler-start-event');if(startEventNode){// Inject a custom dropdown itemmodeler.registerCustomDropdownData(startEventNode,{label: 'Custom Start Option',id: 'custom-start-option',dataTest: 'switch-to-custom-start-option',});}});});
  • Use registerNode to add custom nodes.
  • Use registerCustomDropdownData to add dropdown items, but only after the process and nodes are loaded (in modeler-start).
  • You can also use other extension points provided in the modeler-init payload as needed.

Architecture

The entry point for the application is src/main.js; this is the "starting point" which is used when running npm run serve or npm run build.

Global event bus

window.ProcessMaker.EventBus points to an independent Vue instance which acts as the application's global event bus. The modeler currently emits two events on the event bus which can be listened to in application code to hook into, customize, and extend the modeler's behaviour: modeler-init, and modeler-start.

modeler-init

This event is fired before the modeler and BpmnModdle are set up and mounted. Listeners to this event are passed an object with three methods: registerInspectorExtension, registerBpmnExtension, and registerNode.

importbpmnExtensionfrom'@processmaker/processmaker-bpmn-moddle/resources/processmaker.json';import{intermediateMessageCatchEvent}from'@/components/nodes';window.ProcessMaker.EventBus.$on('modeler-init',({ registerBpmnExtension, registerNode, registerInspectorExtension })=>{/* Add a BPMN extension */registerBpmnExtension('pm',bpmnExtension);/* Register a component to be used in the modeler */registerNode(intermediateMessageCatchEvent);/* Add custom properties to inspector */registerInspectorExtension(intermediateMessageCatchEvent,{id: 'pm-condition',component: 'FormInput',config: {label: 'Condition',helper: 'Expression to be evaluated on webhook to activate event. Leave blank to accept always.',name: 'pm:condition',},});});

modeler-start

This event is fired after the modeler has been set up and mounted. Listeners to this event are passed an object with a single method, loadXML.

window.ProcessMaker.EventBus.$on('modeler-start',({ loadXML })=>{loadXML('<?xml version="1.0" encoding="UTF-8"?> ... </bpmn:definitions>');});

For the modeler to function correctly, loadXML must be called when the application loads.

modeler-validate

This event is fired during validation, and can be used to add custom validation rules. See Adding validation rules during runtime.

modeler-change

This event is fired anytime a change is made to the modeler that causes the underlying XML to change. This event is fired immediately after new state is pushed to the undo/redo stack.

window.ProcessMaker.EventBus.$on('modeler-change',()=>{console.log('The diagram has changed');});

Undo/redo store

The undo/redo feature is implemented using Vuex, with the undo/redo Vuex store initialized in src/undoRedoStore.js. The undo/redo store keeps track of every change in the underlying BPMN XML, recording a copy of the XML string in a stack. Traversing the undo/redo stack simply uses the loadXML function to load the XML string from the current position in the stack.

Validation

Adding a new lint rule

By default, the modeler automatically validates your diagram as you create it. This validation can be toggled on and off using the switch in the status bar. Validation is handled using https://github.com/bpmn-io/bpmnlint.

To add a new validation rule, create a new file in processmaker-plugin/rules named after your rule, for example, node-id.js. This file should export a function that returns an object with a check method. The check method will receive two arguments—node and reporter—and must return undefined if validation passes, or reporter.report to raise an error. For exmaple:

// processmaker-plugin/rules/node-id.js/** * Rule that checks node IDs start with "node_" */module.exports=function(){functioncheck(node,reporter){if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}}return{ check };};

When you are done writing the rule, add it to processmaker-plugin/index.js:

module.exports={configs: {all: {rules: {'processmaker/custom-validation': 'error','processmaker/gateway-direction': 'error','processmaker/node-id': 'error',},},},};

For more examples, see the list of default rules at https://github.com/bpmn-io/bpmnlint/tree/master/rules.

Adding validation rules during runtime

To add custom validation when the linter runs, use the global event bus:

window.ProcessMaker.EventBus.$on('modeler-validate',(node,reporter)=>{if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}});

Examples

Adding a new component

Creating a new modeler component requires creating a Vue component, a component config, and calling the registerNode method to register your component.

First, create your component config, and save it in a .js file:

// CustomComponentConfig.jsexportdefault{// A unique ID that will be used to identify your componentid: 'unique-custom-component-id',// A reference to the Vue component representing your componentcomponent: CustomComponent,// The bpmn type for your component, which will be used to save the component under in XML// It has the form prefix:namebpmnType: 'custom-namespace:CustomComponent',// A toggle to show/hide the component in the controls panelcontrol: true,// The category to place the component under in the controls panelcategory: 'BPMN',// The icon representing the component in the controls panelicon: require('@/assets/toolpanel/scriptTask.svg'),// The label for the component in the controls panellabel: 'Script Task',// The function used to create the BPMN definition object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddledefinition(moddle){returnmoddle.create('bpmn:ScriptTask',{name: 'Script Task',});},// The function used to create the BPMN diagram object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddlediagram(moddle){returnmoddle.create('bpmndi:BPMNShape',{bounds: moddle.create('dc:Bounds',{height: taskHeight,width: 116,}),});},// The configuration for the inspector panelinspectorConfig: [{name: 'CustomComponent',items: [// Each item corresponds to a form element. {// Component can be a custom Vue component or a reference to a form component from @processmaker/vue-form-elementscomponent: 'FormText',config: {label: 'Custom Component Label',fontSize: '2em',},},// ...],},// ...],};

Then, create a Vue component for your custom element:

// CustomComponent.vue
<template>
<div />
</template>
<script>importTaskfrom'@/components/nodes/task/task';exportdefault { extends: Task,mounted() {// Do things with this.shape },};</script>

Finally, register your custom component:

registerNode(CustomComponentConfig);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

History
345 lines (255 loc) · 11.5 KB

File metadata and controls

345 lines (255 loc) · 11.5 KB

ProcessMaker BPMN modeler

CircleCI

@processmaker/modeler is a Vue.js based BPMN modeler scaffolded using Vue CLI 3.

Project setup

Clone the repository and cd into the modeler directory:

git clone git@github.com:ProcessMaker/modeler.git
cd modeler

You can now run any of the below commands. Most commands make calls vue-cli-service. You can read more about those commands at https://cli.vuejs.org/guide/cli-service.html#cli-service.

# Compile the app and start a local development server
npm run serve
# Create a production build of the app to be distributed as an npm package
npm run build-bundle
# Report and fix ESLint errors
npm run lint

Docker Env

If you prefer to run Modeler using Docker

# Using Docker Compose
docker compose up

Testing

Unit tests are set up using jest and end-to-end tests are set up using Cypress. Unit and end-to-end tests can be run separately or together. Code coverage is collected for both types of tests and combined into a single coverage report for the entire project.

Tests can be run locally with the following commands:

# Run the Jest unit test suite
npm run test-unit
# Open Cypress to run the end-to-end (e2e) test suite
npm run open-cypress
# Run the Cypress end-to-end (e2e) test suite in headless mode
npm run test-ci
# Run the Jest unit test tests and then the Cypress tests in headless mode
npm test

Tests are run automatically on CircleCI when new branches are created and updated. The CI uses the npm test command to run both the unit and e2e test suites and to collect code coverage.

For more information on Cypress, visit https://cli.vuejs.org/config/#cypress.

For more information on Jest, visit https://jestjs.io.

Extending the Modeler with modeler-init

The modeler-init event is the main entry point for plugins and extensions to customize the modeler at runtime. It provides several extension points, including:

  • Registering custom nodes
  • Registering BPMN extensions
  • Registering inspector extensions
  • Registering custom dropdown items for nodes

Unified Example

Below is a unified example that demonstrates how to use modeler-init to register a custom node and, after the process is loaded, inject a custom dropdown item for a Start Event node.

importMyCustomNodeConfigfrom'./MyCustomNodeConfig';// Listen for modeler-init to register nodes and extensionswindow.ProcessMaker.EventBus.$on('modeler-init',({ registerNode, registerCustomDropdownData })=>{// Register a custom node (if needed)registerNode(MyCustomNodeConfig);// Wait for the process to load before injecting dropdown itemswindow.ProcessMaker.EventBus.$on('modeler-start',({ modeler })=>{// Find the first Start Event node (or your custom node)conststartEventNode=modeler.$store.getters.nodes.find(node=>node.type==='processmaker-modeler-start-event');if(startEventNode){// Inject a custom dropdown itemmodeler.registerCustomDropdownData(startEventNode,{label: 'Custom Start Option',id: 'custom-start-option',dataTest: 'switch-to-custom-start-option',});}});});
  • Use registerNode to add custom nodes.
  • Use registerCustomDropdownData to add dropdown items, but only after the process and nodes are loaded (in modeler-start).
  • You can also use other extension points provided in the modeler-init payload as needed.

Architecture

The entry point for the application is src/main.js; this is the "starting point" which is used when running npm run serve or npm run build.

Global event bus

window.ProcessMaker.EventBus points to an independent Vue instance which acts as the application's global event bus. The modeler currently emits two events on the event bus which can be listened to in application code to hook into, customize, and extend the modeler's behaviour: modeler-init, and modeler-start.

modeler-init

This event is fired before the modeler and BpmnModdle are set up and mounted. Listeners to this event are passed an object with three methods: registerInspectorExtension, registerBpmnExtension, and registerNode.

importbpmnExtensionfrom'@processmaker/processmaker-bpmn-moddle/resources/processmaker.json';import{intermediateMessageCatchEvent}from'@/components/nodes';window.ProcessMaker.EventBus.$on('modeler-init',({ registerBpmnExtension, registerNode, registerInspectorExtension })=>{/* Add a BPMN extension */registerBpmnExtension('pm',bpmnExtension);/* Register a component to be used in the modeler */registerNode(intermediateMessageCatchEvent);/* Add custom properties to inspector */registerInspectorExtension(intermediateMessageCatchEvent,{id: 'pm-condition',component: 'FormInput',config: {label: 'Condition',helper: 'Expression to be evaluated on webhook to activate event. Leave blank to accept always.',name: 'pm:condition',},});});

modeler-start

This event is fired after the modeler has been set up and mounted. Listeners to this event are passed an object with a single method, loadXML.

window.ProcessMaker.EventBus.$on('modeler-start',({ loadXML })=>{loadXML('<?xml version="1.0" encoding="UTF-8"?> ... </bpmn:definitions>');});

For the modeler to function correctly, loadXML must be called when the application loads.

modeler-validate

This event is fired during validation, and can be used to add custom validation rules. See Adding validation rules during runtime.

modeler-change

This event is fired anytime a change is made to the modeler that causes the underlying XML to change. This event is fired immediately after new state is pushed to the undo/redo stack.

window.ProcessMaker.EventBus.$on('modeler-change',()=>{console.log('The diagram has changed');});

Undo/redo store

The undo/redo feature is implemented using Vuex, with the undo/redo Vuex store initialized in src/undoRedoStore.js. The undo/redo store keeps track of every change in the underlying BPMN XML, recording a copy of the XML string in a stack. Traversing the undo/redo stack simply uses the loadXML function to load the XML string from the current position in the stack.

Validation

Adding a new lint rule

By default, the modeler automatically validates your diagram as you create it. This validation can be toggled on and off using the switch in the status bar. Validation is handled using https://github.com/bpmn-io/bpmnlint.

To add a new validation rule, create a new file in processmaker-plugin/rules named after your rule, for example, node-id.js. This file should export a function that returns an object with a check method. The check method will receive two arguments—node and reporter—and must return undefined if validation passes, or reporter.report to raise an error. For exmaple:

// processmaker-plugin/rules/node-id.js/** * Rule that checks node IDs start with "node_" */module.exports=function(){functioncheck(node,reporter){if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}}return{ check };};

When you are done writing the rule, add it to processmaker-plugin/index.js:

module.exports={configs: {all: {rules: {'processmaker/custom-validation': 'error','processmaker/gateway-direction': 'error','processmaker/node-id': 'error',},},},};

For more examples, see the list of default rules at https://github.com/bpmn-io/bpmnlint/tree/master/rules.

Adding validation rules during runtime

To add custom validation when the linter runs, use the global event bus:

window.ProcessMaker.EventBus.$on('modeler-validate',(node,reporter)=>{if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}});

Examples

Adding a new component

Creating a new modeler component requires creating a Vue component, a component config, and calling the registerNode method to register your component.

First, create your component config, and save it in a .js file:

// CustomComponentConfig.jsexportdefault{// A unique ID that will be used to identify your componentid: 'unique-custom-component-id',// A reference to the Vue component representing your componentcomponent: CustomComponent,// The bpmn type for your component, which will be used to save the component under in XML// It has the form prefix:namebpmnType: 'custom-namespace:CustomComponent',// A toggle to show/hide the component in the controls panelcontrol: true,// The category to place the component under in the controls panelcategory: 'BPMN',// The icon representing the component in the controls panelicon: require('@/assets/toolpanel/scriptTask.svg'),// The label for the component in the controls panellabel: 'Script Task',// The function used to create the BPMN definition object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddledefinition(moddle){returnmoddle.create('bpmn:ScriptTask',{name: 'Script Task',});},// The function used to create the BPMN diagram object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddlediagram(moddle){returnmoddle.create('bpmndi:BPMNShape',{bounds: moddle.create('dc:Bounds',{height: taskHeight,width: 116,}),});},// The configuration for the inspector panelinspectorConfig: [{name: 'CustomComponent',items: [// Each item corresponds to a form element. {// Component can be a custom Vue component or a reference to a form component from @processmaker/vue-form-elementscomponent: 'FormText',config: {label: 'Custom Component Label',fontSize: '2em',},},// ...],},// ...],};

Then, create a Vue component for your custom element:

// CustomComponent.vue
<template>
<div />
</template>
<script>importTaskfrom'@/components/nodes/task/task';exportdefault { extends: Task,mounted() {// Do things with this.shape },};</script>

Finally, register your custom component:

registerNode(CustomComponentConfig);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

History
345 lines (255 loc) · 11.5 KB

File metadata and controls

345 lines (255 loc) · 11.5 KB

ProcessMaker BPMN modeler

CircleCI

@processmaker/modeler is a Vue.js based BPMN modeler scaffolded using Vue CLI 3.

Project setup

Clone the repository and cd into the modeler directory:

git clone git@github.com:ProcessMaker/modeler.git
cd modeler

You can now run any of the below commands. Most commands make calls vue-cli-service. You can read more about those commands at https://cli.vuejs.org/guide/cli-service.html#cli-service.

# Compile the app and start a local development server
npm run serve
# Create a production build of the app to be distributed as an npm package
npm run build-bundle
# Report and fix ESLint errors
npm run lint

Docker Env

If you prefer to run Modeler using Docker

# Using Docker Compose
docker compose up

Testing

Unit tests are set up using jest and end-to-end tests are set up using Cypress. Unit and end-to-end tests can be run separately or together. Code coverage is collected for both types of tests and combined into a single coverage report for the entire project.

Tests can be run locally with the following commands:

# Run the Jest unit test suite
npm run test-unit
# Open Cypress to run the end-to-end (e2e) test suite
npm run open-cypress
# Run the Cypress end-to-end (e2e) test suite in headless mode
npm run test-ci
# Run the Jest unit test tests and then the Cypress tests in headless mode
npm test

Tests are run automatically on CircleCI when new branches are created and updated. The CI uses the npm test command to run both the unit and e2e test suites and to collect code coverage.

For more information on Cypress, visit https://cli.vuejs.org/config/#cypress.

For more information on Jest, visit https://jestjs.io.

Extending the Modeler with modeler-init

The modeler-init event is the main entry point for plugins and extensions to customize the modeler at runtime. It provides several extension points, including:

  • Registering custom nodes
  • Registering BPMN extensions
  • Registering inspector extensions
  • Registering custom dropdown items for nodes

Unified Example

Below is a unified example that demonstrates how to use modeler-init to register a custom node and, after the process is loaded, inject a custom dropdown item for a Start Event node.

importMyCustomNodeConfigfrom'./MyCustomNodeConfig';// Listen for modeler-init to register nodes and extensionswindow.ProcessMaker.EventBus.$on('modeler-init',({ registerNode, registerCustomDropdownData })=>{// Register a custom node (if needed)registerNode(MyCustomNodeConfig);// Wait for the process to load before injecting dropdown itemswindow.ProcessMaker.EventBus.$on('modeler-start',({ modeler })=>{// Find the first Start Event node (or your custom node)conststartEventNode=modeler.$store.getters.nodes.find(node=>node.type==='processmaker-modeler-start-event');if(startEventNode){// Inject a custom dropdown itemmodeler.registerCustomDropdownData(startEventNode,{label: 'Custom Start Option',id: 'custom-start-option',dataTest: 'switch-to-custom-start-option',});}});});
  • Use registerNode to add custom nodes.
  • Use registerCustomDropdownData to add dropdown items, but only after the process and nodes are loaded (in modeler-start).
  • You can also use other extension points provided in the modeler-init payload as needed.

Architecture

The entry point for the application is src/main.js; this is the "starting point" which is used when running npm run serve or npm run build.

Global event bus

window.ProcessMaker.EventBus points to an independent Vue instance which acts as the application's global event bus. The modeler currently emits two events on the event bus which can be listened to in application code to hook into, customize, and extend the modeler's behaviour: modeler-init, and modeler-start.

modeler-init

This event is fired before the modeler and BpmnModdle are set up and mounted. Listeners to this event are passed an object with three methods: registerInspectorExtension, registerBpmnExtension, and registerNode.

importbpmnExtensionfrom'@processmaker/processmaker-bpmn-moddle/resources/processmaker.json';import{intermediateMessageCatchEvent}from'@/components/nodes';window.ProcessMaker.EventBus.$on('modeler-init',({ registerBpmnExtension, registerNode, registerInspectorExtension })=>{/* Add a BPMN extension */registerBpmnExtension('pm',bpmnExtension);/* Register a component to be used in the modeler */registerNode(intermediateMessageCatchEvent);/* Add custom properties to inspector */registerInspectorExtension(intermediateMessageCatchEvent,{id: 'pm-condition',component: 'FormInput',config: {label: 'Condition',helper: 'Expression to be evaluated on webhook to activate event. Leave blank to accept always.',name: 'pm:condition',},});});

modeler-start

This event is fired after the modeler has been set up and mounted. Listeners to this event are passed an object with a single method, loadXML.

window.ProcessMaker.EventBus.$on('modeler-start',({ loadXML })=>{loadXML('<?xml version="1.0" encoding="UTF-8"?> ... </bpmn:definitions>');});

For the modeler to function correctly, loadXML must be called when the application loads.

modeler-validate

This event is fired during validation, and can be used to add custom validation rules. See Adding validation rules during runtime.

modeler-change

This event is fired anytime a change is made to the modeler that causes the underlying XML to change. This event is fired immediately after new state is pushed to the undo/redo stack.

window.ProcessMaker.EventBus.$on('modeler-change',()=>{console.log('The diagram has changed');});

Undo/redo store

The undo/redo feature is implemented using Vuex, with the undo/redo Vuex store initialized in src/undoRedoStore.js. The undo/redo store keeps track of every change in the underlying BPMN XML, recording a copy of the XML string in a stack. Traversing the undo/redo stack simply uses the loadXML function to load the XML string from the current position in the stack.

Validation

Adding a new lint rule

By default, the modeler automatically validates your diagram as you create it. This validation can be toggled on and off using the switch in the status bar. Validation is handled using https://github.com/bpmn-io/bpmnlint.

To add a new validation rule, create a new file in processmaker-plugin/rules named after your rule, for example, node-id.js. This file should export a function that returns an object with a check method. The check method will receive two arguments—node and reporter—and must return undefined if validation passes, or reporter.report to raise an error. For exmaple:

// processmaker-plugin/rules/node-id.js/** * Rule that checks node IDs start with "node_" */module.exports=function(){functioncheck(node,reporter){if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}}return{ check };};

When you are done writing the rule, add it to processmaker-plugin/index.js:

module.exports={configs: {all: {rules: {'processmaker/custom-validation': 'error','processmaker/gateway-direction': 'error','processmaker/node-id': 'error',},},},};

For more examples, see the list of default rules at https://github.com/bpmn-io/bpmnlint/tree/master/rules.

Adding validation rules during runtime

To add custom validation when the linter runs, use the global event bus:

window.ProcessMaker.EventBus.$on('modeler-validate',(node,reporter)=>{if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}});

Examples

Adding a new component

Creating a new modeler component requires creating a Vue component, a component config, and calling the registerNode method to register your component.

First, create your component config, and save it in a .js file:

// CustomComponentConfig.jsexportdefault{// A unique ID that will be used to identify your componentid: 'unique-custom-component-id',// A reference to the Vue component representing your componentcomponent: CustomComponent,// The bpmn type for your component, which will be used to save the component under in XML// It has the form prefix:namebpmnType: 'custom-namespace:CustomComponent',// A toggle to show/hide the component in the controls panelcontrol: true,// The category to place the component under in the controls panelcategory: 'BPMN',// The icon representing the component in the controls panelicon: require('@/assets/toolpanel/scriptTask.svg'),// The label for the component in the controls panellabel: 'Script Task',// The function used to create the BPMN definition object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddledefinition(moddle){returnmoddle.create('bpmn:ScriptTask',{name: 'Script Task',});},// The function used to create the BPMN diagram object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddlediagram(moddle){returnmoddle.create('bpmndi:BPMNShape',{bounds: moddle.create('dc:Bounds',{height: taskHeight,width: 116,}),});},// The configuration for the inspector panelinspectorConfig: [{name: 'CustomComponent',items: [// Each item corresponds to a form element. {// Component can be a custom Vue component or a reference to a form component from @processmaker/vue-form-elementscomponent: 'FormText',config: {label: 'Custom Component Label',fontSize: '2em',},},// ...],},// ...],};

Then, create a Vue component for your custom element:

// CustomComponent.vue
<template>
<div />
</template>
<script>importTaskfrom'@/components/nodes/task/task';exportdefault { extends: Task,mounted() {// Do things with this.shape },};</script>

Finally, register your custom component:

registerNode(CustomComponentConfig);
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

History
345 lines (255 loc) · 11.5 KB

File metadata and controls

345 lines (255 loc) · 11.5 KB

ProcessMaker BPMN modeler

CircleCI

@processmaker/modeler is a Vue.js based BPMN modeler scaffolded using Vue CLI 3.

Project setup

Clone the repository and cd into the modeler directory:

git clone git@github.com:ProcessMaker/modeler.git
cd modeler

You can now run any of the below commands. Most commands make calls vue-cli-service. You can read more about those commands at https://cli.vuejs.org/guide/cli-service.html#cli-service.

# Compile the app and start a local development server
npm run serve
# Create a production build of the app to be distributed as an npm package
npm run build-bundle
# Report and fix ESLint errors
npm run lint

Docker Env

If you prefer to run Modeler using Docker

# Using Docker Compose
docker compose up

Testing

Unit tests are set up using jest and end-to-end tests are set up using Cypress. Unit and end-to-end tests can be run separately or together. Code coverage is collected for both types of tests and combined into a single coverage report for the entire project.

Tests can be run locally with the following commands:

# Run the Jest unit test suite
npm run test-unit
# Open Cypress to run the end-to-end (e2e) test suite
npm run open-cypress
# Run the Cypress end-to-end (e2e) test suite in headless mode
npm run test-ci
# Run the Jest unit test tests and then the Cypress tests in headless mode
npm test

Tests are run automatically on CircleCI when new branches are created and updated. The CI uses the npm test command to run both the unit and e2e test suites and to collect code coverage.

For more information on Cypress, visit https://cli.vuejs.org/config/#cypress.

For more information on Jest, visit https://jestjs.io.

Extending the Modeler with modeler-init

The modeler-init event is the main entry point for plugins and extensions to customize the modeler at runtime. It provides several extension points, including:

  • Registering custom nodes
  • Registering BPMN extensions
  • Registering inspector extensions
  • Registering custom dropdown items for nodes

Unified Example

Below is a unified example that demonstrates how to use modeler-init to register a custom node and, after the process is loaded, inject a custom dropdown item for a Start Event node.

importMyCustomNodeConfigfrom'./MyCustomNodeConfig';// Listen for modeler-init to register nodes and extensionswindow.ProcessMaker.EventBus.$on('modeler-init',({ registerNode, registerCustomDropdownData })=>{// Register a custom node (if needed)registerNode(MyCustomNodeConfig);// Wait for the process to load before injecting dropdown itemswindow.ProcessMaker.EventBus.$on('modeler-start',({ modeler })=>{// Find the first Start Event node (or your custom node)conststartEventNode=modeler.$store.getters.nodes.find(node=>node.type==='processmaker-modeler-start-event');if(startEventNode){// Inject a custom dropdown itemmodeler.registerCustomDropdownData(startEventNode,{label: 'Custom Start Option',id: 'custom-start-option',dataTest: 'switch-to-custom-start-option',});}});});
  • Use registerNode to add custom nodes.
  • Use registerCustomDropdownData to add dropdown items, but only after the process and nodes are loaded (in modeler-start).
  • You can also use other extension points provided in the modeler-init payload as needed.

Architecture

The entry point for the application is src/main.js; this is the "starting point" which is used when running npm run serve or npm run build.

Global event bus

window.ProcessMaker.EventBus points to an independent Vue instance which acts as the application's global event bus. The modeler currently emits two events on the event bus which can be listened to in application code to hook into, customize, and extend the modeler's behaviour: modeler-init, and modeler-start.

modeler-init

This event is fired before the modeler and BpmnModdle are set up and mounted. Listeners to this event are passed an object with three methods: registerInspectorExtension, registerBpmnExtension, and registerNode.

importbpmnExtensionfrom'@processmaker/processmaker-bpmn-moddle/resources/processmaker.json';import{intermediateMessageCatchEvent}from'@/components/nodes';window.ProcessMaker.EventBus.$on('modeler-init',({ registerBpmnExtension, registerNode, registerInspectorExtension })=>{/* Add a BPMN extension */registerBpmnExtension('pm',bpmnExtension);/* Register a component to be used in the modeler */registerNode(intermediateMessageCatchEvent);/* Add custom properties to inspector */registerInspectorExtension(intermediateMessageCatchEvent,{id: 'pm-condition',component: 'FormInput',config: {label: 'Condition',helper: 'Expression to be evaluated on webhook to activate event. Leave blank to accept always.',name: 'pm:condition',},});});

modeler-start

This event is fired after the modeler has been set up and mounted. Listeners to this event are passed an object with a single method, loadXML.

window.ProcessMaker.EventBus.$on('modeler-start',({ loadXML })=>{loadXML('<?xml version="1.0" encoding="UTF-8"?> ... </bpmn:definitions>');});

For the modeler to function correctly, loadXML must be called when the application loads.

modeler-validate

This event is fired during validation, and can be used to add custom validation rules. See Adding validation rules during runtime.

modeler-change

This event is fired anytime a change is made to the modeler that causes the underlying XML to change. This event is fired immediately after new state is pushed to the undo/redo stack.

window.ProcessMaker.EventBus.$on('modeler-change',()=>{console.log('The diagram has changed');});

Undo/redo store

The undo/redo feature is implemented using Vuex, with the undo/redo Vuex store initialized in src/undoRedoStore.js. The undo/redo store keeps track of every change in the underlying BPMN XML, recording a copy of the XML string in a stack. Traversing the undo/redo stack simply uses the loadXML function to load the XML string from the current position in the stack.

Validation

Adding a new lint rule

By default, the modeler automatically validates your diagram as you create it. This validation can be toggled on and off using the switch in the status bar. Validation is handled using https://github.com/bpmn-io/bpmnlint.

To add a new validation rule, create a new file in processmaker-plugin/rules named after your rule, for example, node-id.js. This file should export a function that returns an object with a check method. The check method will receive two arguments—node and reporter—and must return undefined if validation passes, or reporter.report to raise an error. For exmaple:

// processmaker-plugin/rules/node-id.js/** * Rule that checks node IDs start with "node_" */module.exports=function(){functioncheck(node,reporter){if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}}return{ check };};

When you are done writing the rule, add it to processmaker-plugin/index.js:

module.exports={configs: {all: {rules: {'processmaker/custom-validation': 'error','processmaker/gateway-direction': 'error','processmaker/node-id': 'error',},},},};

For more examples, see the list of default rules at https://github.com/bpmn-io/bpmnlint/tree/master/rules.

Adding validation rules during runtime

To add custom validation when the linter runs, use the global event bus:

window.ProcessMaker.EventBus.$on('modeler-validate',(node,reporter)=>{if(typeofnode.id==='string'&&!node.id.startsWith('node_')){reporter.report(node.id,'Node ID must start with the string "node_"');}});

Examples

Adding a new component

Creating a new modeler component requires creating a Vue component, a component config, and calling the registerNode method to register your component.

First, create your component config, and save it in a .js file:

// CustomComponentConfig.jsexportdefault{// A unique ID that will be used to identify your componentid: 'unique-custom-component-id',// A reference to the Vue component representing your componentcomponent: CustomComponent,// The bpmn type for your component, which will be used to save the component under in XML// It has the form prefix:namebpmnType: 'custom-namespace:CustomComponent',// A toggle to show/hide the component in the controls panelcontrol: true,// The category to place the component under in the controls panelcategory: 'BPMN',// The icon representing the component in the controls panelicon: require('@/assets/toolpanel/scriptTask.svg'),// The label for the component in the controls panellabel: 'Script Task',// The function used to create the BPMN definition object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddledefinition(moddle){returnmoddle.create('bpmn:ScriptTask',{name: 'Script Task',});},// The function used to create the BPMN diagram object for the component in the XML// moddle is a reference to an instance of bpmn-moddle: https://github.com/bpmn-io/bpmn-moddlediagram(moddle){returnmoddle.create('bpmndi:BPMNShape',{bounds: moddle.create('dc:Bounds',{height: taskHeight,width: 116,}),});},// The configuration for the inspector panelinspectorConfig: [{name: 'CustomComponent',items: [// Each item corresponds to a form element. {// Component can be a custom Vue component or a reference to a form component from @processmaker/vue-form-elementscomponent: 'FormText',config: {label: 'Custom Component Label',fontSize: '2em',},},// ...],},// ...],};

Then, create a Vue component for your custom element:

// CustomComponent.vue
<template>
<div />
</template>
<script>importTaskfrom'@/components/nodes/task/task';exportdefault { extends: Task,mounted() {// Do things with this.shape },};</script>

Finally, register your custom component:

registerNode(CustomComponentConfig);