new: [workflow:editor] Added support of frame nodes in editor and drawflow lib

pull/9193/head
Sami Mokaddem 2023-07-28 08:39:10 +02:00
parent 68ca753593
commit 9ddc335c07
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
6 changed files with 348 additions and 13 deletions

View File

@ -6,16 +6,18 @@ class GraphUtil
{
public function __construct($graphData)
{
$this->graph = $graphData;
$this->graph = array_filter($graphData, function($i) {
return $i != '_frames';
}, ARRAY_FILTER_USE_KEY);
$this->numberNodes = count($this->graph);
$this->edgeList = $this->_buildEdgeList($graphData);
$this->edgeList = $this->_buildEdgeList($this->graph);
$this->properties = [];
}
private function _buildEdgeList($graphData): array
{
$list = [];
foreach ($graphData as $node) {
foreach ($graphData as $i => $node) {
$list[(int)$node['id']] = [];
foreach (($node['outputs'] ?? []) as $output_id => $outputs) {
foreach ($outputs as $connections) {
@ -356,6 +358,20 @@ class WorkflowRoamingData
class WorkflowGraphTool
{
/**
* cleanGraphData Remove frame nodes from the graph data
*
* @param array $graphData
* @return array
*/
public static function cleanGraphData(array $graphData): array
{
return array_filter($graphData, function($i) {
return $i != '_frames';
}, ARRAY_FILTER_USE_KEY);
}
/**
* extractTriggerFromWorkflow Return the trigger id (or full module) that are specified in the workflow
*
@ -382,8 +398,9 @@ class WorkflowGraphTool
*/
public static function extractTriggersFromWorkflow(array $graphData, bool $fullNode = false): array
{
$graphData = self::cleanGraphData($graphData);
$triggers = [];
foreach ($graphData as $node) {
foreach ($graphData as $i => $node) {
if ($node['data']['module_type'] == 'trigger') {
if (!empty($fullNode)) {
$triggers[] = $node;
@ -404,8 +421,9 @@ class WorkflowGraphTool
*/
public static function extractConcurrentTasksFromWorkflow(array $graphData, bool $fullNode = false): array
{
$graphData = self::cleanGraphData($graphData);
$nodes = [];
foreach ($graphData as $node) {
foreach ($graphData as $i => $node) {
if ($node['data']['module_type'] == 'logic' && $node['data']['id'] == 'concurrent-task') {
if (!empty($fullNode)) {
$nodes[] = $node;
@ -426,8 +444,9 @@ class WorkflowGraphTool
*/
public static function extractFilterNodesFromWorkflow(array $graphData, bool $fullNode = false): array
{
$graphData = self::cleanGraphData($graphData);
$nodes = [];
foreach ($graphData as $node) {
foreach ($graphData as $i => $node) {
if ($node['data']['module_type'] == 'logic' && $node['data']['id'] == 'generic-filter-data') {
if (!empty($fullNode)) {
$nodes[] = $node;
@ -448,8 +467,9 @@ class WorkflowGraphTool
*/
public static function extractResetFilterFromWorkflow(array $graphData, bool $fullNode = false): array
{
$graphData = self::cleanGraphData($graphData);
$nodes = [];
foreach ($graphData as $node) {
foreach ($graphData as $i => $node) {
if ($node['data']['module_type'] == 'logic' && $node['data']['id'] == 'generic-filter-reset') {
if (!empty($fullNode)) {
$nodes[] = $node;

View File

@ -1373,6 +1373,9 @@ class Workflow extends AppModel
}
$labelsByNodes = $this->getLabelsForConnections($workflow, $trigger_id);
foreach ($graphData as $i => $node) {
if ($i == '_frames') {
continue;
}
if (!empty($labelsByNodes[$node['id']])) {
foreach ($node['inputs'] as $inputName => $inputs) {
foreach ($inputs['connections'] as $j => $connection) {

View File

@ -179,6 +179,9 @@ $debugEnabled = !empty($selectedWorkflow['Workflow']['debug_enabled']);
<button id="control-duplicate" class="btn btn-small btn-primary disabled" type="button" title="<?= __('Duplicate') ?>">
<i class="fa-fw <?= $this->FontAwesome->getClass('clone') ?>"></i> <?= __('Duplicate') ?>
</button>
<button id="control-frame-node" class="btn btn-small btn-primary disabled" type="button" title="<?= __('Create frame node') ?>">
<i class="fa-fw <?= $this->FontAwesome->getClass('object-group') ?>"></i> <?= __('Frame') ?>
</button>
<button id="control-delete" class="btn btn-small btn-danger disabled" type="button" title="<?= __('Delete') ?>">
<i class="fa-fw <?= $this->FontAwesome->getClass('trash') ?>"></i> <?= __('Delete') ?>
</button>
@ -309,6 +312,7 @@ echo $this->element('genericElements/assetLoader', [
var $blockNotificationModal = $('#block-notifications-modal')
var $blockFilteringModal = $('#block-filtering-modal')
var $controlDuplicateButton = $('.control-buttons #control-duplicate')
var $controlFrameNodeButton = $('.control-buttons #control-frame-node')
var $controlDeleteButton = $('.control-buttons #control-delete')
var $controlExportBlocksLi = $('.control-buttons #control-export-blocks')
var $controlSaveBlocksLi = $('.control-buttons #control-save-blocks')

View File

@ -425,6 +425,13 @@
background-color: #2fa1db;
} */
.drawflow .drawflow-framenode.selected {
box-shadow: 0 0 5px 2px #33333366;
}
.drawflow .drawflow-framenode .drawflow-framenode-text {
cursor: text;
}
.drawflow .drawflow-node > .inputs > .input::before {
line-height: var(--dfInputHeight);
vertical-align: top;

View File

@ -6,6 +6,7 @@ class Drawflow {
this.nodeId = 1;
this.ele_selected = null;
this.node_selected = null;
this.framenode_selected = null;
this.drag = false;
this.reroute = false;
this.reroute_fix_curvature = false;
@ -32,10 +33,12 @@ class Drawflow {
this.draggable_inputs = true;
this.useuuid = false;
this.parent = parent;
this.frame_to_nodes_registry = {};
this.nodes_to_frames_registry = {};
this.noderegister = {};
this.render = render;
this.drawflow = { "drawflow": { "Home": { "data": {} } } };
this.drawflow = { "drawflow": { "Home": { "data": { _frames: {}} } } };
// Configurable options
this.module = 'Home';
this.editor_mode = 'edit';
@ -139,7 +142,9 @@ class Drawflow {
/* End Mobile Zoom */
load() {
for (var key in this.drawflow.drawflow[this.module].data) {
this.addNodeImport(this.drawflow.drawflow[this.module].data[key], this.precanvas);
if (!key.startsWith('_')) {
this.addNodeImport(this.drawflow.drawflow[this.module].data[key], this.precanvas);
}
}
if (this.reroute) {
@ -212,6 +217,10 @@ class Drawflow {
this.removeReouteConnectionSelected();
this.connection_selected = null;
}
if (this.framenode_selected != null) {
this.framenode_selected.classList.remove("selected");
this.framenode_selected = null
}
if (this.node_selected != this.ele_selected) {
this.dispatch('nodeSelected', this.ele_selected.id.slice(5));
}
@ -239,8 +248,30 @@ class Drawflow {
this.removeReouteConnectionSelected();
this.connection_selected = null;
}
if (this.framenode_selected != null) {
this.framenode_selected.classList.remove("selected");
this.framenode_selected = null
}
this.drawConnection(e.target);
break;
case 'drawflow-framenode':
if (this.node_selected != null) {
this.node_selected.classList.remove("selected");
this.node_selected = null
this.dispatch('nodeUnselected', true);
}
if (this.connection_selected != null) {
this.connection_selected.classList.remove("selected");
this.removeReouteConnectionSelected();
this.connection_selected = null;
}
if (this.framenode_selected != null) {
this.framenode_selected.classList.remove("selected");
this.framenode_selected = null
}
this.framenode_selected = this.ele_selected;
this.framenode_selected.classList.add("selected");
break;
case 'parent-drawflow':
if (this.node_selected != null) {
this.node_selected.classList.remove("selected");
@ -252,6 +283,10 @@ class Drawflow {
this.removeReouteConnectionSelected();
this.connection_selected = null;
}
if (this.framenode_selected != null) {
this.framenode_selected.classList.remove("selected");
this.framenode_selected = null
}
this.editor_selected = true;
break;
case 'drawflow':
@ -265,6 +300,10 @@ class Drawflow {
this.removeReouteConnectionSelected();
this.connection_selected = null;
}
if (this.framenode_selected != null) {
this.framenode_selected.classList.remove("selected");
this.framenode_selected = null
}
this.editor_selected = true;
break;
case 'main-path':
@ -273,6 +312,10 @@ class Drawflow {
this.node_selected = null;
this.dispatch('nodeUnselected', true);
}
if (this.framenode_selected != null) {
this.framenode_selected.classList.remove("selected");
this.framenode_selected = null
}
if (this.connection_selected != null) {
this.connection_selected.classList.remove("selected");
this.removeReouteConnectionSelected();
@ -303,6 +346,10 @@ class Drawflow {
this.removeConnection();
}
if (this.framenode_selected) {
this.removeFrameNodeId(this.framenode_selected.id);
}
if (this.node_selected != null) {
this.node_selected.classList.remove("selected");
this.node_selected = null;
@ -313,6 +360,10 @@ class Drawflow {
this.removeReouteConnectionSelected();
this.connection_selected = null;
}
if (this.framenode_selected != null) {
this.framenode_selected.classList.remove("selected");
this.framenode_selected = null
}
break;
default:
@ -543,6 +594,21 @@ class Drawflow {
}
} else {
if (e.target.classList.contains('drawflow-framenode') || e.target.classList.contains('drawflow-framenode-text')) {
const frameNode = e.target.classList.contains('drawflow-framenode') ? e.target : e.target.parentElement
const deletebox = document.createElement('div');
deletebox.classList.add("drawflow-delete");
deletebox.innerHTML = "x";
const frameNodeBCR = frameNode.getBoundingClientRect()
const pos_y = frameNodeBCR.top - 30 // roughly include margin and node size
const pos_x = frameNodeBCR.left + frameNodeBCR.width
deletebox.style.top = pos_y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)) - (this.precanvas.getBoundingClientRect().y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom))) + "px";
deletebox.style.left = pos_x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)) - (this.precanvas.getBoundingClientRect().x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom))) + "px";
this.precanvas.appendChild(deletebox);
}
}
}
@ -1092,6 +1158,16 @@ class Drawflow {
}
})
var nodeIdInt = parseInt(id.slice(5));
if (this.nodes_to_frames_registry[nodeIdInt] !== undefined && this.nodes_to_frames_registry[nodeIdInt].length > 0) {
var _this = this;
this.nodes_to_frames_registry[nodeIdInt].forEach((frameNodeUuid) => {
const containedNodeIDs = _this.frame_to_nodes_registry[frameNodeUuid]
const frameNode = _this.container.querySelector('#framenode-' + frameNodeUuid)
_this.updateFrameNodePosition(frameNode, containedNodeIDs);
})
}
}
dblclick(e) {
@ -1516,6 +1592,133 @@ class Drawflow {
});
}
addFrameNode(frameNodeConfig) {
frameNodeConfig.nodes = [...new Set(frameNodeConfig.nodes)]
if (frameNodeConfig.nodes.length == 0) {
return
}
var newNodeId = this.getUuid();
this.frame_to_nodes_registry[newNodeId] = frameNodeConfig.nodes
frameNodeConfig.nodes.forEach(nodeId => {
if (this.nodes_to_frames_registry[nodeId] === undefined) {
this.nodes_to_frames_registry[nodeId] = []
}
this.nodes_to_frames_registry[nodeId].push(newNodeId)
});
const parent = document.createElement('div');
parent.classList.add(...["parent-framenode"]);
const frameNode = document.createElement('div');
frameNode.innerHTML = '';
frameNode.setAttribute('id', 'framenode-' + newNodeId);
frameNode.classList.add(...['drawflow-framenode']);
if (frameNodeConfig.class !== undefined && frameNodeConfig.class != '') {
frameNode.classList.add(...frameNodeConfig.class.split(' '));
}
frameNode.style.backgroundColor = '#b5b5b570';
frameNode.style.border = '1px solid #333333aa';
frameNode.style.position = 'absolute'
frameNode.style['text-align'] = 'center'
frameNode.style['z-index'] = -1
frameNode.style['border-radius'] = '5px'
this.updateFrameNodePosition(frameNode, frameNodeConfig.nodes)
const frameTextNode = document.createElement('span');
frameTextNode.innerText = frameNodeConfig.text;
frameTextNode.style['line-height'] = '1.5em'
frameTextNode.style.color = '#505050'
frameTextNode.style['font-size'] = '1.5em'
frameTextNode.style['font-weight'] = 'bold'
frameTextNode.classList.add('drawflow-framenode-text')
frameNode.appendChild(frameTextNode);
parent.appendChild(frameNode);
this.precanvas.appendChild(parent);
var json = {
id: newNodeId,
text: frameNodeConfig.text,
nodes: frameNodeConfig.nodes,
class: frameNodeConfig.class,
}
this.drawflow.drawflow[this.module].data._frames[newNodeId] = json;
this.dispatch('frameNodeCreated', newNodeId);
}
updateFrameNodePosition(frameNode, nodeIDs) {
if (!frameNode) {
return
}
var offsetPadding = {
top: 100,
left: 30,
right: 30,
bottom: 20,
}
var _this = this
var nodesHtml = nodeIDs.map(function (id) {
return _this.container.querySelector('#node-' + id)
})
function getFrameCoordinatesFromNodes(nodesHtml) {
var topLeft = { x: Number.MAX_SAFE_INTEGER, y: Number.MAX_SAFE_INTEGER }
var bottomRight = { x: Number.MIN_SAFE_INTEGER, y: Number.MIN_SAFE_INTEGER }
nodesHtml.forEach(function (nodeHtml) {
var bcr = nodeHtml.getBoundingClientRect()
topLeft.x = Math.min(topLeft.x, bcr.left)
topLeft.y = Math.min(topLeft.y, bcr.top)
bottomRight.x = Math.max(bottomRight.x, bcr.left + bcr.width)
bottomRight.y = Math.max(bottomRight.y, bcr.top + bcr.height)
})
return { topLeft: topLeft, bottomRight: bottomRight }
}
var frameCoordinates = getFrameCoordinatesFromNodes(nodesHtml)
var pos_x = frameCoordinates.topLeft.x - offsetPadding.left / 2
var pos_y = frameCoordinates.topLeft.y - offsetPadding.top / 2
var frameNodeHeight = (frameCoordinates.bottomRight.y - frameCoordinates.topLeft.y) + (offsetPadding.top / 2 + offsetPadding.bottom)
var frameNodeWidth = (frameCoordinates.bottomRight.x - frameCoordinates.topLeft.x) + (offsetPadding.left / 2 + offsetPadding.right)
var transpositionFactorX = (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom))
var transpositionOffsetX = -(this.precanvas.getBoundingClientRect().x * (this.precanvas.clientWidth / (this.precanvas.clientWidth * this.zoom)))
var transpositionFactorY = (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom))
var transpositionOffsetY = -(this.precanvas.getBoundingClientRect().y * (this.precanvas.clientHeight / (this.precanvas.clientHeight * this.zoom)))
pos_x = pos_x * transpositionFactorX + transpositionOffsetX
pos_y = pos_y * transpositionFactorY + transpositionOffsetY
frameNodeWidth = frameNodeWidth * transpositionFactorX
frameNodeHeight = frameNodeHeight * transpositionFactorY
frameNode.style.top = pos_y + 'px';
frameNode.style.left = pos_x + 'px';
frameNode.style.height = frameNodeHeight + 'px';
frameNode.style.width = frameNodeWidth + 'px';
}
updateFramenode(frameNodeUuid, frameNodeConfig) {
const frameNode = document.getElementById(frameNodeUuid);
frameNode.classList.remove(...frameNode.classList)
frameNode.classList.add(...['drawflow-framenode']);
if (frameNodeConfig.class !== undefined && frameNodeConfig.class != '') {
frameNode.classList.add(...frameNodeConfig.class.split(' '));
}
const frameTextNode = frameNode.querySelector('.drawflow-framenode-text')
frameTextNode.innerText = frameNodeConfig.text
var oldJson = this.drawflow.drawflow[this.module].data._frames[frameNodeUuid.slice(10)]
oldJson.text = frameNodeConfig.text
oldJson.class = frameNodeConfig.class
this.drawflow.drawflow[this.module].data._frames[frameNodeUuid.slice(10)] = oldJson;
this.dispatch('frameNodeUpdated', frameNodeUuid.slice(10));
}
updateNodeValue(event) {
var attr = event.target.attributes
for (var i = 0; i < attr.length; i++) {
@ -1769,6 +1972,25 @@ class Drawflow {
if (this.module === moduleName) {
this.container.querySelector(`#${id}`).remove();
}
var _this = this
var new_frame_to_nodes_registry = []
if (this.nodes_to_frames_registry[id.slice(5)] !== undefined) {
this.nodes_to_frames_registry[id.slice(5)].forEach((frameUuid) => {
new_frame_to_nodes_registry = _this.frame_to_nodes_registry[frameUuid].filter((nodeId) => {
return id.slice(5) != nodeId;
})
if (new_frame_to_nodes_registry.length == 0) {
_this.removeFrameNodeId('framenode-' + frameUuid)
} else {
const frameNode = _this.container.querySelector('#framenode-' + frameUuid)
_this.frame_to_nodes_registry[frameUuid] = new_frame_to_nodes_registry
_this.updateFrameNodePosition(frameNode, _this.frame_to_nodes_registry[frameUuid]);
}
})
delete this.nodes_to_frames_registry[id.slice(5)]
}
delete this.drawflow.drawflow[moduleName].data[id.slice(5)];
this.dispatch('nodeRemoved', id.slice(5));
}
@ -1874,6 +2096,21 @@ class Drawflow {
}
}
removeFrameNodeId(fullID) {
const frameUuid = fullID.slice(10);
const moduleName = this.getModuleFromNodeId(this.frame_to_nodes_registry[frameUuid][0])
this.container.querySelector(`#${fullID}`).remove();
var _this = this;
this.frame_to_nodes_registry[frameUuid].forEach((nodeID) => {
_this.nodes_to_frames_registry[nodeID] = _this.nodes_to_frames_registry[nodeID].filter((frameNodeUuid) => {
return frameNodeUuid != frameUuid;
})
})
delete this.frame_to_nodes_registry[frameUuid];
delete this.drawflow.drawflow[moduleName].data._frames[frameUuid];
this.dispatch('frameNodeRemoved', frameUuid);
}
getModuleFromNodeId(id) {
var nameModule;
const editor = this.drawflow.drawflow
@ -1888,7 +2125,7 @@ class Drawflow {
}
addModule(name) {
this.drawflow.drawflow[name] = { "data": {} };
this.drawflow.drawflow[name] = { "data": { _frames: {} } };
this.dispatch('moduleCreated', name);
}
changeModule(name) {
@ -1917,12 +2154,16 @@ class Drawflow {
clearModuleSelected() {
this.precanvas.innerHTML = "";
this.drawflow.drawflow[this.module] = { "data": {} };
this.drawflow.drawflow[this.module] = { "data": { _frames: {}} };
this.frame_to_nodes_registry = {};
this.nodes_to_frames_registry = {};
}
clear() {
this.precanvas.innerHTML = "";
this.drawflow = { "drawflow": { "Home": { "data": {} } } };
this.drawflow = { "drawflow": { "Home": { "data": { _frames: {} } } } };
this.frame_to_nodes_registry = {};
this.nodes_to_frames_registry = {};
}
export() {
const dataExport = JSON.parse(JSON.stringify(this.drawflow));

View File

@ -175,6 +175,21 @@ function initDrawflow() {
editor.on('nodeRemoved', function () {
invalidateContentCache()
})
editor.on('frameNodeCreated', function (framenodeUuid) {
invalidateContentCache()
var frameNodeFullUuid = 'framenode-' + framenodeUuid
$('#' + frameNodeFullUuid).find('.drawflow-framenode-text').dblclick(function() {
var existingText = $(this).text()
var newText = prompt('Edit frame node text', existingText)
editor.updateFramenode(frameNodeFullUuid, {text: newText})
})
})
editor.on('frameNodeUpdated', function () {
invalidateContentCache()
})
editor.on('frameNodeRemoved', function () {
invalidateContentCache()
})
editor.on('nodeDataChanged', invalidateContentCache)
editor.on('nodeMoved', invalidateContentCache)
editor.on('connectionCreated', function() {
@ -204,6 +219,9 @@ function initDrawflow() {
duplicateSelection()
evt.preventDefault()
}
if (evt.keyCode == 70 && $drawflow.is(evt.target)) {
createFrameNodeForSelected()
}
})
editor.translate_to = function (x, y) {
this.canvas_x = x;
@ -415,6 +433,7 @@ function initDrawflow() {
})
editor.on('nodeSelected', function(node_id) {
$controlDuplicateButton.removeClass('disabled')
$controlFrameNodeButton.removeClass('disabled')
$controlDeleteButton.removeClass('disabled')
$controlSaveBlocksLi.removeClass('disabled')
$controlEditBlocksLiContainer.removeClass('disabled').find('.dropdown-menu')
@ -426,6 +445,7 @@ function initDrawflow() {
})
selection.clearSelection()
$controlDuplicateButton.addClass('disabled')
$controlFrameNodeButton.addClass('disabled')
$controlDeleteButton.addClass('disabled')
$controlSaveBlocksLi.addClass('disabled')
$controlEditBlocksLiContainer.addClass('disabled').find('.dropdown-menu')
@ -473,6 +493,9 @@ function initDrawflow() {
$controlDuplicateButton.click(function() {
duplicateSelection()
})
$controlFrameNodeButton.click(function() {
createFrameNodeForSelected()
})
$controlDeleteButton.click(function() {
deleteSelectedNodes(false)
})
@ -574,6 +597,26 @@ function duplicateSelection() {
selection.select(newNodes)
}
function createFrameNodeForSelected() {
var text = prompt('Enter text for the frame node')
var selectedNodesHtml = selection.getSelection()
var selectedIDs = selectedNodesHtml.map(function(nodeHtml) {
return parseInt(nodeHtml.id.slice(5))
})
createFrameForNodes(selectedIDs, text)
selection.clearSelection()
invalidateContentCache()
}
function createFrameForNodes(nodesIDs, text) {
const frameNode = {
nodes: nodesIDs,
text: text,
class: "",
}
editor.addFrameNode(frameNode)
}
function buildModalForBlock(node_id, node) {
var html = genNodeParamHtml(node, false)
$blockModal
@ -701,6 +744,8 @@ function addNode(block, position, additionalData={}) {
function getEditorData(cleanNodes) {
var data = {} // Make sure nodes are index by their internal IDs
var editorExport = editor.export().drawflow.Home.data
var frameNodes = editorExport._frames !== undefined ? editorExport._frames : {}
delete editorExport._frames
editorExport = Array.isArray(editorExport) ? editorExport : Object.values(editorExport)
editorExport.forEach(function(node) {
if (node !== null) { // for some reason, the editor create null nodes
@ -725,6 +770,7 @@ function getEditorData(cleanNodes) {
data[node.id] = node
}
})
data._frames = frameNodes
return data
}
@ -756,7 +802,12 @@ function loadWorkflow(workflow) {
}
// We cannot rely on the editor's import function as it recreates the nodes with the saved HTML instead of rebuilding them
// We have to manually add the nodes and their connections
Object.values(workflow.data).forEach(function (node) {
Object.entries(workflow.data).forEach(function (entry) {
var i = entry[0]
var node = entry[1]
if (i == '_frames') {
return
}
var module = all_modules_by_id[node.data.id] || all_triggers_by_id[node.data.id]
if (!module) {
console.error('Tried to add node for unknown module ' + node.data.id + ' (' + node.id + ')')
@ -818,6 +869,10 @@ function loadWorkflow(workflow) {
})
}
})
var frameNodes = workflow.data._frames || {}
Object.values(frameNodes).forEach(function (frameNode) {
editor.addFrameNode(frameNode)
})
}
function filterModules(clicked) {
@ -1294,6 +1349,11 @@ function addWorkflowBlueprint(blueprintId, cursorPosition) {
if (newNodes.length > 0) {
selection.clearSelection()
selection.select(newNodes)
var newNodeIDs = newNodes.map(function(node) {
return node.id.slice(5)
})
createFrameForNodes(newNodeIDs, workflowBlueprint.WorkflowBlueprint.name)
editor.dispatch('nodeSelected', newNodes[0].id);
}
}