/** * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ /** * An Interface to nodes and utility functions for creating/adding/deleting nodes and links * @namespace RED.nodes */ RED.nodes = (function() { var PORT_TYPE_INPUT = 1; var PORT_TYPE_OUTPUT = 0; var node_defs = {}; var linkTabMap = {}; var configNodes = {}; var links = []; var nodeLinks = {}; var defaultWorkspace; var workspaces = {}; var workspacesOrder =[]; var subflows = {}; var loadedFlowVersion = null; var groups = {}; var groupsByZ = {}; var junctions = {}; var junctionsByZ = {}; var initialLoad; var dirty = false; function setDirty(d) { dirty = d; RED.events.emit("workspace:dirty",{dirty:dirty}); } // The registry holds information about all node types. var registry = (function() { var moduleList = {}; var nodeList = []; var nodeSets = {}; var typeToId = {}; var nodeDefinitions = {}; var iconSets = {}; nodeDefinitions['tab'] = { defaults: { label: {value:""}, disabled: {value: false}, info: {value: ""}, env: {value: []} } }; var exports = { setModulePendingUpdated: function(module,version) { moduleList[module].pending_version = version; RED.events.emit("registry:module-updated",{module:module,version:version}); }, getModule: function(module) { return moduleList[module]; }, getNodeSetForType: function(nodeType) { return exports.getNodeSet(typeToId[nodeType]); }, getModuleList: function() { return moduleList; }, getNodeList: function() { return nodeList; }, getNodeTypes: function() { return Object.keys(nodeDefinitions); }, setNodeList: function(list) { nodeList = []; for(var i=0;i -1) { tabMap[n.z].splice(i,1); } } }, hasNode: function(id) { return nodes.hasOwnProperty(id); }, getNode: function(id) { return nodes[id] }, moveNode: function(n, newZ) { api.removeNode(n); n.z = newZ; api.addNode(n) }, moveNodesForwards: function(nodes) { var result = []; if (!Array.isArray(nodes)) { nodes = [nodes] } // Can only do this for nodes on the same tab. // Use nodes[0] to get the z var tabNodes = tabMap[nodes[0].z]; var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); var moved = new Set(); for (var i = tabNodes.length-1; i >= 0; i--) { if (toMove.size === 0) { break; } var n = tabNodes[i]; if (toMove.has(n)) { // This is a node to move. if (i < tabNodes.length-1 && !moved.has(tabNodes[i+1])) { // Remove from current position tabNodes.splice(i,1); // Add it back one position higher tabNodes.splice(i+1,0,n); n._reordered = true; result.push(n); } toMove.delete(n); moved.add(n); } } if (result.length > 0) { RED.events.emit('nodes:reorder',{ z: nodes[0].z, nodes: result }); } return result; }, moveNodesBackwards: function(nodes) { var result = []; if (!Array.isArray(nodes)) { nodes = [nodes] } // Can only do this for nodes on the same tab. // Use nodes[0] to get the z var tabNodes = tabMap[nodes[0].z]; var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); var moved = new Set(); for (var i = 0; i < tabNodes.length; i++) { if (toMove.size === 0) { break; } var n = tabNodes[i]; if (toMove.has(n)) { // This is a node to move. if (i > 0 && !moved.has(tabNodes[i-1])) { // Remove from current position tabNodes.splice(i,1); // Add it back one position lower tabNodes.splice(i-1,0,n); n._reordered = true; result.push(n); } toMove.delete(n); moved.add(n); } } if (result.length > 0) { RED.events.emit('nodes:reorder',{ z: nodes[0].z, nodes: result }); } return result; }, moveNodesToFront: function(nodes) { var result = []; if (!Array.isArray(nodes)) { nodes = [nodes] } // Can only do this for nodes on the same tab. // Use nodes[0] to get the z var tabNodes = tabMap[nodes[0].z]; var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); var target = tabNodes.length-1; for (var i = tabNodes.length-1; i >= 0; i--) { if (toMove.size === 0) { break; } var n = tabNodes[i]; if (toMove.has(n)) { // This is a node to move. if (i < target) { // Remove from current position tabNodes.splice(i,1); tabNodes.splice(target,0,n); n._reordered = true; result.push(n); } target--; toMove.delete(n); } } if (result.length > 0) { RED.events.emit('nodes:reorder',{ z: nodes[0].z, nodes: result }); } return result; }, moveNodesToBack: function(nodes) { var result = []; if (!Array.isArray(nodes)) { nodes = [nodes] } // Can only do this for nodes on the same tab. // Use nodes[0] to get the z var tabNodes = tabMap[nodes[0].z]; var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); var target = 0; for (var i = 0; i < tabNodes.length; i++) { if (toMove.size === 0) { break; } var n = tabNodes[i]; if (toMove.has(n)) { // This is a node to move. if (i > target) { // Remove from current position tabNodes.splice(i,1); // Add it back one position lower tabNodes.splice(target,0,n); n._reordered = true; result.push(n); } target++; toMove.delete(n); } } if (result.length > 0) { RED.events.emit('nodes:reorder',{ z: nodes[0].z, nodes: result }); } return result; }, getNodes: function(z) { return tabMap[z]; }, clear: function() { nodes = {}; tabMap = {}; }, eachNode: function(cb) { var nodeList,i,j; for (i in subflows) { if (subflows.hasOwnProperty(i)) { nodeList = tabMap[i]; for (j = 0; j < nodeList.length; j++) { if (cb(nodeList[j]) === false) { return; } } } } for (i = 0; i < workspacesOrder.length; i++) { nodeList = tabMap[workspacesOrder[i]]; for (j = 0; j < nodeList.length; j++) { if (cb(nodeList[j]) === false) { return; } } } // Flow nodes that do not have a valid tab/subflow if (tabMap["_"]) { nodeList = tabMap["_"]; for (j = 0; j < nodeList.length; j++) { if (cb(nodeList[j]) === false) { return; } } } }, filterNodes: function(filter) { var result = []; var searchSet = null; var doZFilter = false; if (filter.hasOwnProperty("z")) { if (tabMap.hasOwnProperty(filter.z)) { searchSet = tabMap[filter.z]; } else { doZFilter = true; } } var objectLookup = false; if (searchSet === null) { searchSet = Object.keys(nodes); objectLookup = true; } for (var n=0;n 0) { types.push(currentToken) } return { types: types, array: isArray } } function addNode(n) { if (n.type.indexOf("subflow") !== 0) { n["_"] = n._def._; } else { var subflowId = n.type.substring(8); var sf = RED.nodes.subflow(subflowId); if (sf) { sf.instances.push(sf); } n["_"] = RED._; } if (n._def.category == "config") { configNodes[n.id] = n; } else { if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; } n.dirty = true; updateConfigNodeUsers(n); if (n._def.category == "subflows" && typeof n.i === "undefined") { var nextId = 0; RED.nodes.eachNode(function(node) { nextId = Math.max(nextId,node.i||0); }); n.i = nextId+1; } allNodes.addNode(n); if (!nodeLinks[n.id]) { nodeLinks[n.id] = {in:[],out:[]}; } } RED.events.emit('nodes:add',n); } function addLink(l) { if (nodeLinks[l.source.id]) { const isUnique = nodeLinks[l.source.id].out.every(function(link) { return link.sourcePort !== l.sourcePort || link.target.id !== l.target.id }) if (!isUnique) { return } } links.push(l); if (l.source) { // Possible the node hasn't been added yet if (!nodeLinks[l.source.id]) { nodeLinks[l.source.id] = {in:[],out:[]}; } nodeLinks[l.source.id].out.push(l); } if (l.target) { if (!nodeLinks[l.target.id]) { nodeLinks[l.target.id] = {in:[],out:[]}; } nodeLinks[l.target.id].in.push(l); } if (l.source.z === l.target.z && linkTabMap[l.source.z]) { linkTabMap[l.source.z].push(l); } RED.events.emit("links:add",l); } function getNode(id) { if (id in configNodes) { return configNodes[id]; } return allNodes.getNode(id); } function removeNode(id) { var removedLinks = []; var removedNodes = []; var node; if (id in configNodes) { node = configNodes[id]; delete configNodes[id]; RED.events.emit('nodes:remove',node); RED.workspaces.refresh(); } else if (allNodes.hasNode(id)) { node = allNodes.getNode(id); allNodes.removeNode(node); delete nodeLinks[id]; removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); removedLinks.forEach(removeLink); var updatedConfigNode = false; for (var d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d)) { var property = node._def.defaults[d]; if (property.type) { var type = registry.getNodeType(property.type); if (type && type.category == "config") { var configNode = configNodes[node[d]]; if (configNode) { updatedConfigNode = true; if (configNode._def.exclusive) { removeNode(node[d]); removedNodes.push(configNode); } else { var users = configNode.users; users.splice(users.indexOf(node),1); RED.events.emit('nodes:change',configNode) } } } } } } if (node.type.indexOf("subflow:") === 0) { var subflowId = node.type.substring(8); var sf = RED.nodes.subflow(subflowId); if (sf) { sf.instances.splice(sf.instances.indexOf(node),1); } } if (updatedConfigNode) { RED.workspaces.refresh(); } try { if (node._def.oneditdelete) { node._def.oneditdelete.call(node); } } catch(err) { console.log("oneditdelete",node.id,node.type,err.toString()); } RED.events.emit('nodes:remove',node); } if (node && node._def.onremove) { // Deprecated: never documented but used by some early nodes console.log("Deprecated API warning: node type ",node.type," has an onremove function - should be oneditremove - please report"); node._def.onremove.call(n); } return {links:removedLinks,nodes:removedNodes}; } function moveNodesForwards(nodes) { return allNodes.moveNodesForwards(nodes); } function moveNodesBackwards(nodes) { return allNodes.moveNodesBackwards(nodes); } function moveNodesToFront(nodes) { return allNodes.moveNodesToFront(nodes); } function moveNodesToBack(nodes) { return allNodes.moveNodesToBack(nodes); } function getNodeOrder(z) { return allNodes.getNodeOrder(z); } function setNodeOrder(z, order) { allNodes.setNodeOrder(z,order); } function moveNodeToTab(node, z) { if (node.type === "group") { moveGroupToTab(node,z); return; } if (node.type === "junction") { moveJunctionToTab(node,z); return; } var oldZ = node.z; allNodes.moveNode(node,z); var nl = nodeLinks[node.id]; if (nl) { nl.in.forEach(function(l) { var idx = linkTabMap[oldZ].indexOf(l); if (idx != -1) { linkTabMap[oldZ].splice(idx, 1); } if ((l.source.z === z) && linkTabMap[z]) { linkTabMap[z].push(l); } }); nl.out.forEach(function(l) { var idx = linkTabMap[oldZ].indexOf(l); if (idx != -1) { linkTabMap[oldZ].splice(idx, 1); } if ((l.target.z === z) && linkTabMap[z]) { linkTabMap[z].push(l); } }); } RED.events.emit("nodes:change",node); } function moveGroupToTab(group, z) { var index = groupsByZ[group.z].indexOf(group); groupsByZ[group.z].splice(index,1); groupsByZ[z] = groupsByZ[z] || []; groupsByZ[z].push(group); group.z = z; RED.events.emit("groups:change",group); } function moveJunctionToTab(junction, z) { var index = junctionsByZ[junction.z].indexOf(junction); junctionsByZ[junction.z].splice(index,1); junctionsByZ[z] = junctionsByZ[z] || []; junctionsByZ[z].push(junction); var oldZ = junction.z; junction.z = z; var nl = nodeLinks[junction.id]; if (nl) { nl.in.forEach(function(l) { var idx = linkTabMap[oldZ].indexOf(l); if (idx != -1) { linkTabMap[oldZ].splice(idx, 1); } if ((l.source.z === z) && linkTabMap[z]) { linkTabMap[z].push(l); } }); nl.out.forEach(function(l) { var idx = linkTabMap[oldZ].indexOf(l); if (idx != -1) { linkTabMap[oldZ].splice(idx, 1); } if ((l.target.z === z) && linkTabMap[z]) { linkTabMap[z].push(l); } }); } RED.events.emit("junctions:change",junction); } function removeLink(l) { var index = links.indexOf(l); if (index != -1) { links.splice(index,1); if (l.source && nodeLinks[l.source.id]) { var sIndex = nodeLinks[l.source.id].out.indexOf(l) if (sIndex !== -1) { nodeLinks[l.source.id].out.splice(sIndex,1) } } if (l.target && nodeLinks[l.target.id]) { var tIndex = nodeLinks[l.target.id].in.indexOf(l) if (tIndex !== -1) { nodeLinks[l.target.id].in.splice(tIndex,1) } } if (l.source.z === l.target.z && linkTabMap[l.source.z]) { index = linkTabMap[l.source.z].indexOf(l); if (index !== -1) { linkTabMap[l.source.z].splice(index,1) } } } RED.events.emit("links:remove",l); } function addWorkspace(ws,targetIndex) { workspaces[ws.id] = ws; allNodes.addTab(ws.id); linkTabMap[ws.id] = []; ws._def = RED.nodes.getType('tab'); if (targetIndex === undefined) { workspacesOrder.push(ws.id); } else { workspacesOrder.splice(targetIndex,0,ws.id); } RED.events.emit('flows:add',ws); if (targetIndex !== undefined) { RED.events.emit('flows:reorder',workspacesOrder) } } function getWorkspace(id) { return workspaces[id]; } function removeWorkspace(id) { var ws = workspaces[id]; var removedNodes = []; var removedLinks = []; var removedGroups = []; var removedJunctions = []; if (ws) { delete workspaces[id]; delete linkTabMap[id]; workspacesOrder.splice(workspacesOrder.indexOf(id),1); var i; var node; if (allNodes.hasTab(id)) { removedNodes = allNodes.getNodes(id).slice() } for (i in configNodes) { if (configNodes.hasOwnProperty(i)) { node = configNodes[i]; if (node.z == id) { removedNodes.push(node); } } } removedJunctions = RED.nodes.junctions(id) for (i=0;i=0; i--) { removeGroup(removedGroups[i]); } allNodes.removeTab(id); RED.events.emit('flows:remove',ws); } return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions}; } function addSubflow(sf, createNewIds) { if (createNewIds) { var subflowNames = Object.keys(subflows).map(function(sfid) { return subflows[sfid].name; }); subflowNames.sort(); var copyNumber = 1; var subflowName = sf.name; subflowNames.forEach(function(name) { if (subflowName == name) { copyNumber++; subflowName = sf.name+" ("+copyNumber+")"; } }); sf.name = subflowName; } subflows[sf.id] = sf; allNodes.addTab(sf.id); linkTabMap[sf.id] = []; RED.nodes.registerType("subflow:"+sf.id, { defaults:{ name:{value:""}, env:{value:[]} }, icon: function() { return sf.icon||"subflow.svg" }, category: sf.category || "subflows", inputs: sf.in.length, outputs: sf.out.length, color: sf.color || "#DDAA99", label: function() { return this.name||RED.nodes.subflow(sf.id).name }, labelStyle: function() { return this.name?"red-ui-flow-node-label-italic":""; }, paletteLabel: function() { return RED.nodes.subflow(sf.id).name }, inputLabels: function(i) { return sf.inputLabels?sf.inputLabels[i]:null }, outputLabels: function(i) { return sf.outputLabels?sf.outputLabels[i]:null }, oneditprepare: function() { if (this.type !== 'subflow') { // A subflow instance node RED.subflow.buildEditForm("subflow",this); } else { // A subflow template node RED.subflow.buildEditForm("subflow-template", this); } }, oneditresize: function(size) { if (this.type === 'subflow') { $("#node-input-env-container").editableList('height',size.height - 80); } }, set:{ module: "node-red" } }); sf.instances = []; sf._def = RED.nodes.getType("subflow:"+sf.id); RED.events.emit("subflows:add",sf); } function getSubflow(id) { return subflows[id]; } function removeSubflow(sf) { if (subflows[sf.id]) { delete subflows[sf.id]; allNodes.removeTab(sf.id); registry.removeNodeType("subflow:"+sf.id); RED.events.emit("subflows:remove",sf); } } function subflowContains(sfid,nodeid) { var sfNodes = allNodes.getNodes(sfid); for (var i = 0; i 0) { var n = nodes.shift(); visited.add(n); var links = []; if (!initialNode || !direction || (initialNode && direction === 'up')) { links = links.concat(nodeLinks[n.id].in); } if (!initialNode || !direction || (initialNode && direction === 'down')) { links = links.concat(nodeLinks[n.id].out); } initialNode = false; links.forEach(function(l) { if (!visited.has(l.source)) { nodes.push(l.source); } if (!visited.has(l.target)) { nodes.push(l.target); } }) } return Array.from(visited); } function convertWorkspace(n,opts) { var exportCreds = true; if (opts) { if (opts.hasOwnProperty("credentials")) { exportCreds = opts.credentials; } } var node = {}; node.id = n.id; node.type = n.type; for (var d in n._def.defaults) { if (n._def.defaults.hasOwnProperty(d)) { node[d] = n[d]; } } if (exportCreds) { var credentialSet = {}; if (n.credentials) { for (var tabCred in n.credentials) { if (n.credentials.hasOwnProperty(tabCred)) { if (!n.credentials._ || n.credentials["has_"+tabCred] != n.credentials._["has_"+tabCred] || (n.credentials["has_"+tabCred] && n.credentials[tabCred])) { credentialSet[tabCred] = n.credentials[tabCred]; } } } if (Object.keys(credentialSet).length > 0) { node.credentials = credentialSet; } } } return node; } /** * Converts a node to an exportable JSON Object **/ function convertNode(n, opts) { var exportCreds = true; var exportDimensions = false; if (opts === false) { exportCreds = false; } else if (typeof opts === "object") { if (opts.hasOwnProperty("credentials")) { exportCreds = opts.credentials; } if (opts.hasOwnProperty("dimensions")) { exportDimensions = opts.dimensions; } } if (n.type === 'tab') { return convertWorkspace(n, { credentials: exportCreds }); } var node = {}; node.id = n.id; node.type = n.type; node.z = n.z; if (node.z === 0 || node.z === "") { delete node.z; } if (n.d === true) { node.d = true; } if (n.g) { node.g = n.g; } if (node.type == "unknown") { for (var p in n._orig) { if (n._orig.hasOwnProperty(p)) { node[p] = n._orig[p]; } } } else { for (var d in n._def.defaults) { if (n._def.defaults.hasOwnProperty(d)) { node[d] = n[d]; } } if (exportCreds) { var credentialSet = {}; if ((/^subflow:/.test(node.type) || (node.type === "group")) && n.credentials) { // A subflow instance/group node can have arbitrary creds for (var sfCred in n.credentials) { if (n.credentials.hasOwnProperty(sfCred)) { if (!n.credentials._ || n.credentials["has_"+sfCred] != n.credentials._["has_"+sfCred] || (n.credentials["has_"+sfCred] && n.credentials[sfCred])) { credentialSet[sfCred] = n.credentials[sfCred]; } } } } else if (n.credentials) { node.credentials = {}; // All other nodes have a well-defined list of possible credentials for (var cred in n._def.credentials) { if (n._def.credentials.hasOwnProperty(cred)) { if (n._def.credentials[cred].type == 'password') { if (!n.credentials._ || n.credentials["has_"+cred] != n.credentials._["has_"+cred] || (n.credentials["has_"+cred] && n.credentials[cred])) { credentialSet[cred] = n.credentials[cred]; } } else if (n.credentials[cred] != null && (!n.credentials._ || n.credentials[cred] != n.credentials._[cred])) { credentialSet[cred] = n.credentials[cred]; } } } } if (Object.keys(credentialSet).length > 0) { node.credentials = credentialSet; } } } if (n.type === "group") { node.x = n.x; node.y = n.y; node.w = n.w; node.h = n.h; // In 1.1.0, we have seen an instance of this array containing `undefined` // Until we know how that can happen, add a filter here to remove them node.nodes = node.nodes.filter(function(n) { return !!n }).map(function(n) { return n.id }); } if (n.type === "tab" || n.type === "group") { if (node.env && node.env.length === 0) { delete node.env; } } if (n._def.category != "config" || n.type === 'junction') { node.x = n.x; node.y = n.y; if (exportDimensions) { if (!n.hasOwnProperty('w')) { // This node has not yet been drawn in the view. So we need // to explicitly calculate its dimensions. Store the result // on the node as if it had been drawn will save us doing // it again var dimensions = RED.view.calculateNodeDimensions(n); n.w = dimensions[0]; n.h = dimensions[1]; } node.w = n.w; node.h = n.h; } node.wires = []; for(var i=0;i 0 && n.inputLabels && !/^\s*$/.test(n.inputLabels.join(""))) { node.inputLabels = n.inputLabels.slice(); } if (n.outputs > 0 && n.outputLabels && !/^\s*$/.test(n.outputLabels.join(""))) { node.outputLabels = n.outputLabels.slice(); } if ((!n._def.defaults || !n._def.defaults.hasOwnProperty("icon")) && n.icon) { var defIcon = RED.utils.getDefaultNodeIcon(n._def, n); if (n.icon !== defIcon.module+"/"+defIcon.file) { node.icon = n.icon; } } if ((!n._def.defaults || !n._def.defaults.hasOwnProperty("l")) && n.hasOwnProperty('l')) { var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true; if (showLabel != n.l) { node.l = n.l; } } } if (n.info) { node.info = n.info; } return node; } function convertSubflow(n, opts) { var exportCreds = true; var exportDimensions = false; if (opts === false) { exportCreds = false; } else if (typeof opts === "object") { if (opts.hasOwnProperty("credentials")) { exportCreds = opts.credentials; } if (opts.hasOwnProperty("dimensions")) { exportDimensions = opts.dimensions; } } var node = {}; node.id = n.id; node.type = n.type; node.name = n.name; node.info = n.info; node.category = n.category; node.in = []; node.out = []; node.env = n.env; node.meta = n.meta; if (exportCreds) { var credentialSet = {}; // A subflow node can have arbitrary creds for (var sfCred in n.credentials) { if (n.credentials.hasOwnProperty(sfCred)) { if (!n.credentials._ || n.credentials["has_"+sfCred] != n.credentials._["has_"+sfCred] || (n.credentials["has_"+sfCred] && n.credentials[sfCred])) { credentialSet[sfCred] = n.credentials[sfCred]; } } } if (Object.keys(credentialSet).length > 0) { node.credentials = credentialSet; } } node.color = n.color; n.in.forEach(function(p) { var nIn = {x:p.x,y:p.y,wires:[]}; var wires = links.filter(function(d) { return d.source === p }); for (var i=0;i 0 && n.inputLabels && !/^\s*$/.test(n.inputLabels.join(""))) { node.inputLabels = n.inputLabels.slice(); } if (node.out.length > 0 && n.outputLabels && !/^\s*$/.test(n.outputLabels.join(""))) { node.outputLabels = n.outputLabels.slice(); } if (n.icon) { if (n.icon !== "node-red/subflow.svg") { node.icon = n.icon; } } if (n.status) { node.status = {x: n.status.x, y: n.status.y, wires:[]}; links.forEach(function(d) { if (d.target === n.status) { if (d.source.type != "subflow") { node.status.wires.push({id:d.source.id, port:d.sourcePort}) } else { node.status.wires.push({id:n.id, port:0}) } } }); } return node; } function createExportableSubflow(id) { var sf = getSubflow(id); var nodeSet; var sfNodes = allNodes.getNodes(sf.id); if (sfNodes) { nodeSet = sfNodes.slice(); nodeSet.unshift(sf); } else { nodeSet = [sf]; } return createExportableNodeSet(nodeSet); } /** * Converts the current node selection to an exportable JSON Object **/ function createExportableNodeSet(set, exportedIds, exportedSubflows, exportedConfigNodes) { var nns = []; exportedIds = exportedIds || {}; set = set.filter(function(n) { if (exportedIds[n.id]) { return false; } exportedIds[n.id] = true; return true; }) exportedConfigNodes = exportedConfigNodes || {}; exportedSubflows = exportedSubflows || {}; for (var n=0;n 0) { var errorMessage = RED._("clipboard.importDuplicate",{count:existingNodes.length}); var nodeList = $("
    "); var existingNodesCount = Math.min(5,existingNodes.length); for (var i=0;i").text( conflict.existing.id+ " [ "+conflict.existing.type+ ((conflict.imported.type !== conflict.existing.type)?" | "+conflict.imported.type:"")+" ]").appendTo(nodeList) } if (existingNodesCount !== existingNodes.length) { $("
  • ").text(RED._("deploy.confirm.plusNMore",{count:existingNodes.length-existingNodesCount})).appendTo(nodeList) } var wrapper = $("

    ").append(nodeList); var existingNodesError = new Error(errorMessage+wrapper.html()); existingNodesError.code = "import_conflict"; existingNodesError.importConfig = identifyImportConflicts(newNodes); throw existingNodesError; } var removedNodes; if (nodesToReplace.length > 0) { var replaceResult = replaceNodes(nodesToReplace); removedNodes = replaceResult.removedNodes; } var isInitialLoad = false; if (!initialLoad) { isInitialLoad = true; initialLoad = JSON.parse(JSON.stringify(newNodes)); } var unknownTypes = []; for (i=0;i 0) { var typeList = $("

      "); unknownTypes.forEach(function(t) { $("
    • ").text(t).appendTo(typeList); }) typeList = typeList[0].outerHTML; RED.notify("

      "+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+"

      "+typeList,"error",false,10000); } var activeWorkspace = RED.workspaces.active(); //TODO: check the z of the subflow instance and check _that_ if it exists var activeSubflow = getSubflow(activeWorkspace); for (i=0;i node.outputs) { if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) { // If 'wires' is longer than outputs, clip wires console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs); node.wires = node.wires.slice(0,node.outputs); } else { // The node declares outputs in its defaults, but has not got a valid value // Defer to the length of the wires array node.outputs = node.wires.length; } } for (d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { node[d] = n[d]; node._config[d] = JSON.stringify(n[d]); } } node._config.x = node.x; node._config.y = node.y; if (node._def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) { node.credentials = {}; for (d in node._def.credentials) { if (node._def.credentials.hasOwnProperty(d) && n.credentials.hasOwnProperty(d)) { node.credentials[d] = n.credentials[d]; } } } } } node_map[n.id] = node; // If an 'unknown' config node, it will not have been caught by the // proper config node handling, so needs adding to new_nodes here if (node.type === 'junction') { new_junctions.push(node) } else if (node.type === "unknown" || node._def.category !== "config") { new_nodes.push(node); } else if (node.type === "group") { new_groups.push(node); new_group_set.add(node.id); } } } } // Remap all wires and config node references for (i=0;i",node_map[wires[w2]].id); } } } } delete n.wires; } if (n.g && node_map[n.g]) { n.g = node_map[n.g].id; } else { delete n.g } for (var d3 in n._def.defaults) { if (n._def.defaults.hasOwnProperty(d3)) { if (n._def.defaults[d3].type) { var nodeList = n[d3]; if (!Array.isArray(nodeList)) { nodeList = [nodeList]; } nodeList = nodeList.map(function(id) { var node = node_map[id]; if (node) { if (node._def.category === 'config') { if (node.users.indexOf(n) === -1) { node.users.push(n); } } return node.id; } return id; }) n[d3] = Array.isArray(n[d3])?nodeList:nodeList[0]; } } } // If importing into a subflow, ensure an outbound-link doesn't // get added if (activeSubflow && /^link /.test(n.type) && n.links) { n.links = n.links.filter(function(id) { var otherNode = RED.nodes.node(id); return (otherNode && otherNode.z === activeWorkspace) }); } } for (i=0;i island index var nodeToIslandIndex = new Map(); // Maps island index => [nodes in island] var islandIndexToNodes = new Map(); var internalLinks = new Set(); nodes.forEach((node, index) => { nodeToIslandIndex.set(node,index); islandIndexToNodes.set(index, [node]); var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); inboundLinks.forEach(l => { if (selectedNodes.has(l.source)) { internalLinks.add(l) } }) outboundLinks.forEach(l => { if (selectedNodes.has(l.target)) { internalLinks.add(l) } }) }) internalLinks.forEach(l => { let source = l.source; let target = l.target; if (nodeToIslandIndex.get(source) !== nodeToIslandIndex.get(target)) { let sourceIsland = nodeToIslandIndex.get(source); let islandToMove = nodeToIslandIndex.get(target); let nodesToMove = islandIndexToNodes.get(islandToMove); nodesToMove.forEach(n => { nodeToIslandIndex.set(n,sourceIsland); islandIndexToNodes.get(sourceIsland).push(n); }) islandIndexToNodes.delete(islandToMove); } }) const result = []; islandIndexToNodes.forEach((nodes,index) => { result.push(nodes); }) return result; } function detachNodes(nodes) { let allSelectedNodes = []; nodes.forEach(node => { if (node.type === 'group') { let groupNodes = RED.group.getNodes(node,true,true); allSelectedNodes = allSelectedNodes.concat(groupNodes); } else { allSelectedNodes.push(node); } }) if (allSelectedNodes.length > 0 ) { const nodeIslands = RED.nodes.getNodeIslands(allSelectedNodes); let removedLinks = []; let newLinks = []; let createdLinkIds = new Set(); nodeIslands.forEach(nodes => { let selectedNodes = new Set(nodes); let allInboundLinks = []; let allOutboundLinks = []; // Identify links that enter or exit this island of nodes nodes.forEach(node => { var inboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_INPUT); var outboundLinks = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); inboundLinks.forEach(l => { if (!selectedNodes.has(l.source)) { allInboundLinks.push(l) } }) outboundLinks.forEach(l => { if (!selectedNodes.has(l.target)) { allOutboundLinks.push(l) } }) }); // Identify the links to restore allInboundLinks.forEach(inLink => { // For Each inbound link, // - get source node. // - trace through to all outbound links let sourceNode = inLink.source; let targetNodes = new Set(); let visited = new Set(); let stack = [inLink.target]; while (stack.length > 0) { let node = stack.pop(stack); visited.add(node) let links = RED.nodes.getNodeLinks(node, PORT_TYPE_OUTPUT); links.forEach(l => { if (visited.has(l.target)) { return } visited.add(l.target); if (selectedNodes.has(l.target)) { // internal link stack.push(l.target) } else { targetNodes.add(l.target) } }) } targetNodes.forEach(target => { let linkId = `${sourceNode.id}[${inLink.sourcePort}] -> ${target.id}` if (!createdLinkIds.has(linkId)) { createdLinkIds.add(linkId); let link = { source: sourceNode, sourcePort: inLink.sourcePort, target: target } let existingLinks = RED.nodes.filterLinks(link) if (existingLinks.length === 0) { newLinks.push(link); } } }) }) // 2. delete all those links allInboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)}) allOutboundLinks.forEach(l => { RED.nodes.removeLink(l); removedLinks.push(l)}) }) newLinks.forEach(l => RED.nodes.addLink(l)); return { newLinks, removedLinks } } } return { init: function() { RED.events.on("registry:node-type-added",function(type) { var def = registry.getNodeType(type); var replaced = false; var replaceNodes = {}; RED.nodes.eachNode(function(n) { if (n.type === "unknown" && n.name === type) { replaceNodes[n.id] = n; } }); RED.nodes.eachConfig(function(n) { if (n.type === "unknown" && n.name === type) { replaceNodes[n.id] = n; } }); const nodeGroupMap = {} var replaceNodeIds = Object.keys(replaceNodes); if (replaceNodeIds.length > 0) { var reimportList = []; replaceNodeIds.forEach(function(id) { var n = replaceNodes[id]; if (configNodes.hasOwnProperty(n.id)) { delete configNodes[n.id]; } else { allNodes.removeNode(n); } if (n.g) { // reimporting a node *without* including its group object // will cause the g property to be cleared. Cache it // here so we can restore it nodeGroupMap[n.id] = n.g } reimportList.push(convertNode(n)); RED.events.emit('nodes:remove',n); }); // Remove any links between nodes that are going to be reimported. // This prevents a duplicate link from being added. var removeLinks = []; RED.nodes.eachLink(function(l) { if (replaceNodes.hasOwnProperty(l.source.id) && replaceNodes.hasOwnProperty(l.target.id)) { removeLinks.push(l); } }); removeLinks.forEach(removeLink); // Force the redraw to be synchronous so the view updates // *now* and removes the unknown node RED.view.redraw(true, true); var result = importNodes(reimportList,{generateIds:false, reimport: true}); var newNodeMap = {}; result.nodes.forEach(function(n) { newNodeMap[n.id] = n; if (nodeGroupMap[n.id]) { // This node is in a group - need to substitute the // node reference inside the group n.g = nodeGroupMap[n.id] const group = RED.nodes.group(n.g) if (group) { var index = group.nodes.findIndex(gn => gn.id === n.id) if (index > -1) { group.nodes[index] = n } } } }); RED.nodes.eachLink(function(l) { if (newNodeMap.hasOwnProperty(l.source.id)) { l.source = newNodeMap[l.source.id]; } if (newNodeMap.hasOwnProperty(l.target.id)) { l.target = newNodeMap[l.target.id]; } }); RED.view.redraw(true); } }); }, registry:registry, setNodeList: registry.setNodeList, getNodeSet: registry.getNodeSet, addNodeSet: registry.addNodeSet, removeNodeSet: registry.removeNodeSet, enableNodeSet: registry.enableNodeSet, disableNodeSet: registry.disableNodeSet, setIconSets: registry.setIconSets, getIconSets: registry.getIconSets, registerType: registry.registerNodeType, getType: registry.getNodeType, getNodeHelp: getNodeHelp, convertNode: convertNode, add: addNode, remove: removeNode, clear: clear, detachNodes: detachNodes, moveNodesForwards: moveNodesForwards, moveNodesBackwards: moveNodesBackwards, moveNodesToFront: moveNodesToFront, moveNodesToBack: moveNodesToBack, getNodeOrder: getNodeOrder, setNodeOrder: setNodeOrder, moveNodeToTab: moveNodeToTab, addLink: addLink, removeLink: removeLink, getNodeLinks: function(id, portType) { if (typeof id !== 'string') { id = id.id; } if (nodeLinks[id]) { if (portType === 1) { // Return cloned arrays so they can be safely modified by caller return [].concat(nodeLinks[id].in) } else { return [].concat(nodeLinks[id].out) } } return []; }, addWorkspace: addWorkspace, removeWorkspace: removeWorkspace, getWorkspaceOrder: function() { return [...workspacesOrder] }, setWorkspaceOrder: function(order) { workspacesOrder = order; }, workspace: getWorkspace, addSubflow: addSubflow, removeSubflow: removeSubflow, subflow: getSubflow, subflowContains: subflowContains, addGroup: addGroup, removeGroup: removeGroup, group: function(id) { return groups[id] }, groups: function(z) { return groupsByZ[z]?groupsByZ[z].slice():[] }, addJunction: addJunction, removeJunction: removeJunction, junction: function(id) { return junctions[id] }, junctions: function(z) { return junctionsByZ[z]?junctionsByZ[z].slice():[] }, eachNode: function(cb) { allNodes.eachNode(cb); }, eachLink: function(cb) { for (var l=0;l