mirror of
https://github.com/node-red/node-red.git
synced 2025-03-01 10:36:34 +00:00
3302 lines
122 KiB
JavaScript
3302 lines
122 KiB
JavaScript
/**
|
|
* 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;
|
|
if (!d) {
|
|
allNodes.clearState()
|
|
}
|
|
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},
|
|
locked: {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);
|
|
},
|
|
/**
|
|
* Get an array of node definitions
|
|
* @param {Object} options - options object
|
|
* @param {boolean} [options.configOnly] - if true, only return config nodes
|
|
* @param {function} [options.filter] - a filter function to apply to the list of nodes
|
|
* @returns array of node definitions
|
|
*/
|
|
getNodeDefinitions: function(options) {
|
|
const result = []
|
|
const configOnly = (options && options.configOnly)
|
|
const filter = (options && options.filter)
|
|
const keys = Object.keys(nodeDefinitions)
|
|
for (const key of keys) {
|
|
const def = nodeDefinitions[key]
|
|
if(!def) { continue }
|
|
if (configOnly && def.category !== "config") {
|
|
continue
|
|
}
|
|
if (filter && !filter(nodeDefinitions[key])) {
|
|
continue
|
|
}
|
|
result.push(nodeDefinitions[key])
|
|
}
|
|
return result
|
|
},
|
|
setNodeList: function(list) {
|
|
nodeList = [];
|
|
for(var i=0;i<list.length;i++) {
|
|
var ns = list[i];
|
|
exports.addNodeSet(ns);
|
|
}
|
|
},
|
|
addNodeSet: function(ns) {
|
|
if (!ns.types) {
|
|
// A node has been loaded without any types. Ignore it.
|
|
return;
|
|
}
|
|
ns.added = false;
|
|
nodeSets[ns.id] = ns;
|
|
for (var j=0;j<ns.types.length;j++) {
|
|
typeToId[ns.types[j]] = ns.id;
|
|
}
|
|
nodeList.push(ns);
|
|
|
|
moduleList[ns.module] = moduleList[ns.module] || {
|
|
name:ns.module,
|
|
version:ns.version,
|
|
local:ns.local,
|
|
sets:{}
|
|
};
|
|
if (ns.pending_version) {
|
|
moduleList[ns.module].pending_version = ns.pending_version;
|
|
}
|
|
moduleList[ns.module].sets[ns.name] = ns;
|
|
RED.events.emit("registry:node-set-added",ns);
|
|
},
|
|
removeNodeSet: function(id) {
|
|
var ns = nodeSets[id];
|
|
if (!ns) { return {} }
|
|
|
|
for (var j=0;j<ns.types.length;j++) {
|
|
delete typeToId[ns.types[j]];
|
|
}
|
|
delete nodeSets[id];
|
|
for (var i=0;i<nodeList.length;i++) {
|
|
if (nodeList[i].id === id) {
|
|
nodeList.splice(i,1);
|
|
break;
|
|
}
|
|
}
|
|
delete moduleList[ns.module].sets[ns.name];
|
|
if (Object.keys(moduleList[ns.module].sets).length === 0) {
|
|
delete moduleList[ns.module];
|
|
}
|
|
RED.events.emit("registry:node-set-removed",ns);
|
|
return ns;
|
|
},
|
|
getNodeSet: function(id) {
|
|
return nodeSets[id];
|
|
},
|
|
enableNodeSet: function(id) {
|
|
var ns = nodeSets[id];
|
|
ns.enabled = true;
|
|
RED.events.emit("registry:node-set-enabled",ns);
|
|
},
|
|
disableNodeSet: function(id) {
|
|
var ns = nodeSets[id];
|
|
ns.enabled = false;
|
|
RED.events.emit("registry:node-set-disabled",ns);
|
|
},
|
|
registerNodeType: function(nt,def) {
|
|
if (nt.substring(0,8) != "subflow:") {
|
|
if (!nodeSets[typeToId[nt]]) {
|
|
var error = "";
|
|
var fullType = nt;
|
|
if (RED._loadingModule) {
|
|
fullType = "["+RED._loadingModule+"] "+nt;
|
|
if (nodeSets[RED._loadingModule]) {
|
|
error = nodeSets[RED._loadingModule].err || "";
|
|
} else {
|
|
error = "Unknown error";
|
|
}
|
|
}
|
|
RED.notify(RED._("palette.event.unknownNodeRegistered",{type:fullType, error:error}), "error");
|
|
return;
|
|
}
|
|
def.set = nodeSets[typeToId[nt]];
|
|
nodeSets[typeToId[nt]].added = true;
|
|
nodeSets[typeToId[nt]].enabled = true;
|
|
|
|
var ns;
|
|
if (def.set.module === "node-red") {
|
|
ns = "node-red";
|
|
} else {
|
|
ns = def.set.id;
|
|
}
|
|
def["_"] = function() {
|
|
var args = Array.prototype.slice.call(arguments, 0);
|
|
var original = args[0];
|
|
if (args[0].indexOf(":") === -1) {
|
|
args[0] = ns+":"+args[0];
|
|
}
|
|
var result = RED._.apply(null,args);
|
|
if (result === args[0]) {
|
|
result = original;
|
|
}
|
|
return result;
|
|
}
|
|
// TODO: too tightly coupled into palette UI
|
|
}
|
|
|
|
def.type = nt;
|
|
nodeDefinitions[nt] = def;
|
|
|
|
|
|
if (def.defaults) {
|
|
for (var d in def.defaults) {
|
|
if (def.defaults.hasOwnProperty(d)) {
|
|
if (def.defaults[d].type) {
|
|
try {
|
|
def.defaults[d]._type = parseNodePropertyTypeString(def.defaults[d].type)
|
|
} catch(err) {
|
|
console.warn(err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
RED.events.emit("registry:node-type-added",nt);
|
|
},
|
|
removeNodeType: function(nt) {
|
|
if (nt.substring(0,8) != "subflow:") {
|
|
// NON-NLS - internal debug message
|
|
throw new Error("this api is subflow only. called with:",nt);
|
|
}
|
|
delete nodeDefinitions[nt];
|
|
RED.events.emit("registry:node-type-removed",nt);
|
|
},
|
|
getNodeType: function(nt) {
|
|
return nodeDefinitions[nt];
|
|
},
|
|
setIconSets: function(sets) {
|
|
iconSets = sets;
|
|
iconSets["font-awesome"] = RED.nodes.fontAwesome.getIconList();
|
|
},
|
|
getIconSets: function() {
|
|
return iconSets;
|
|
}
|
|
};
|
|
return exports;
|
|
})();
|
|
|
|
// allNodes holds information about the Flow nodes.
|
|
var allNodes = (function() {
|
|
// Map node.id -> node
|
|
var nodes = {};
|
|
// Map tab.id -> Array of nodes on that tab
|
|
var tabMap = {};
|
|
// Map tab.id -> Set of dirty object ids on that tab
|
|
var tabDirtyMap = {};
|
|
// Map tab.id -> Set of object ids of things deleted from the tab that weren't otherwise dirty
|
|
var tabDeletedNodesMap = {};
|
|
// Set of object ids of things added to a tab after initial import
|
|
var addedDirtyObjects = new Set()
|
|
|
|
function changeCollectionDepth(tabNodes, toMove, direction, singleStep) {
|
|
const result = []
|
|
const moved = new Set();
|
|
const startIndex = direction ? tabNodes.length - 1 : 0
|
|
const endIndex = direction ? -1 : tabNodes.length
|
|
const step = direction ? -1 : 1
|
|
let target = startIndex // Only used for all-the-way moves
|
|
for (let i = startIndex; i != endIndex; i += step) {
|
|
if (toMove.size === 0) {
|
|
break;
|
|
}
|
|
const n = tabNodes[i]
|
|
if (toMove.has(n)) {
|
|
if (singleStep) {
|
|
if (i !== startIndex && !moved.has(tabNodes[i - step])) {
|
|
tabNodes.splice(i, 1)
|
|
tabNodes.splice(i - step, 0, n)
|
|
n._reordered = true
|
|
result.push(n)
|
|
}
|
|
} else {
|
|
if (i !== target) {
|
|
tabNodes.splice(i, 1)
|
|
tabNodes.splice(target, 0, n)
|
|
n._reordered = true
|
|
result.push(n)
|
|
}
|
|
target += step
|
|
}
|
|
toMove.delete(n);
|
|
moved.add(n);
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
var api = {
|
|
addTab: function(id) {
|
|
tabMap[id] = [];
|
|
tabDirtyMap[id] = new Set();
|
|
tabDeletedNodesMap[id] = new Set();
|
|
},
|
|
hasTab: function(z) {
|
|
return tabMap.hasOwnProperty(z)
|
|
},
|
|
removeTab: function(id) {
|
|
delete tabMap[id];
|
|
delete tabDirtyMap[id];
|
|
delete tabDeletedNodesMap[id];
|
|
},
|
|
addNode: function(n) {
|
|
nodes[n.id] = n;
|
|
if (tabMap.hasOwnProperty(n.z)) {
|
|
tabMap[n.z].push(n);
|
|
api.addObjectToWorkspace(n.z, n.id, n.changed || n.moved)
|
|
} else {
|
|
console.warn("Node added to unknown tab/subflow:",n);
|
|
tabMap["_"] = tabMap["_"] || [];
|
|
tabMap["_"].push(n);
|
|
}
|
|
},
|
|
removeNode: function(n) {
|
|
delete nodes[n.id]
|
|
if (tabMap.hasOwnProperty(n.z)) {
|
|
var i = tabMap[n.z].indexOf(n);
|
|
if (i > -1) {
|
|
tabMap[n.z].splice(i,1);
|
|
}
|
|
api.removeObjectFromWorkspace(n.z, n.id)
|
|
}
|
|
},
|
|
/**
|
|
* Add an object to our dirty/clean tracking state
|
|
* @param {String} z
|
|
* @param {String} id
|
|
* @param {Boolean} isDirty
|
|
*/
|
|
addObjectToWorkspace: function (z, id, isDirty) {
|
|
if (isDirty) {
|
|
addedDirtyObjects.add(id)
|
|
}
|
|
if (tabDeletedNodesMap[z].has(id)) {
|
|
tabDeletedNodesMap[z].delete(id)
|
|
}
|
|
api.markNodeDirty(z, id, isDirty)
|
|
},
|
|
/**
|
|
* Remove an object from our dirty/clean tracking state
|
|
* @param {String} z
|
|
* @param {String} id
|
|
*/
|
|
removeObjectFromWorkspace: function (z, id) {
|
|
if (!addedDirtyObjects.has(id)) {
|
|
tabDeletedNodesMap[z].add(id)
|
|
} else {
|
|
addedDirtyObjects.delete(id)
|
|
}
|
|
api.markNodeDirty(z, id, false)
|
|
},
|
|
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)
|
|
},
|
|
/**
|
|
* @param {array} nodes
|
|
* @param {boolean} direction true:forwards false:back
|
|
* @param {boolean} singleStep true:single-step false:all-the-way
|
|
*/
|
|
changeDepth: function(nodes, direction, singleStep) {
|
|
if (!Array.isArray(nodes)) {
|
|
nodes = [nodes]
|
|
}
|
|
let result = []
|
|
const tabNodes = tabMap[nodes[0].z];
|
|
const toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" }));
|
|
if (toMove.size > 0) {
|
|
result = result.concat(changeCollectionDepth(tabNodes, toMove, direction, singleStep))
|
|
if (result.length > 0) {
|
|
RED.events.emit('nodes:reorder',{
|
|
z: nodes[0].z,
|
|
nodes: result
|
|
});
|
|
}
|
|
}
|
|
|
|
const groupNodes = groupsByZ[nodes[0].z] || []
|
|
const groupsToMove = new Set(nodes.filter(function(n) { return n.type === 'group'}))
|
|
if (groupsToMove.size > 0) {
|
|
const groupResult = changeCollectionDepth(groupNodes, groupsToMove, direction, singleStep)
|
|
if (groupResult.length > 0) {
|
|
result = result.concat(groupResult)
|
|
RED.events.emit('groups:reorder',{
|
|
z: nodes[0].z,
|
|
nodes: groupResult
|
|
});
|
|
}
|
|
}
|
|
RED.view.redraw(true)
|
|
return result
|
|
},
|
|
moveNodesForwards: function(nodes) {
|
|
return api.changeDepth(nodes, true, true)
|
|
},
|
|
moveNodesBackwards: function(nodes) {
|
|
return api.changeDepth(nodes, false, true)
|
|
},
|
|
moveNodesToFront: function(nodes) {
|
|
return api.changeDepth(nodes, true, false)
|
|
},
|
|
moveNodesToBack: function(nodes) {
|
|
return api.changeDepth(nodes, false, false)
|
|
},
|
|
getNodes: function(z) {
|
|
return tabMap[z];
|
|
},
|
|
clear: function() {
|
|
nodes = {};
|
|
tabMap = {};
|
|
tabDirtyMap = {};
|
|
tabDeletedNodesMap = {};
|
|
addedDirtyObjects = new Set();
|
|
},
|
|
/**
|
|
* Clear all internal state on what is dirty.
|
|
*/
|
|
clearState: function () {
|
|
// Called when a deploy happens, we can forget about added/remove
|
|
// items as they have now been deployed.
|
|
addedDirtyObjects = new Set()
|
|
const flowsToCheck = new Set()
|
|
for (const [z, set] of Object.entries(tabDeletedNodesMap)) {
|
|
if (set.size > 0) {
|
|
set.clear()
|
|
flowsToCheck.add(z)
|
|
}
|
|
}
|
|
for (const [z, set] of Object.entries(tabDirtyMap)) {
|
|
if (set.size > 0) {
|
|
set.clear()
|
|
flowsToCheck.add(z)
|
|
}
|
|
}
|
|
for (const z of flowsToCheck) {
|
|
api.checkTabState(z)
|
|
}
|
|
},
|
|
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<searchSet.length;n++) {
|
|
var node = searchSet[n];
|
|
if (objectLookup) {
|
|
node = nodes[node];
|
|
}
|
|
if (filter.hasOwnProperty("type") && node.type !== filter.type) {
|
|
continue;
|
|
}
|
|
if (doZFilter && node.z !== filter.z) {
|
|
continue;
|
|
}
|
|
result.push(node);
|
|
}
|
|
return result;
|
|
},
|
|
getNodeOrder: function(z) {
|
|
return (groupsByZ[z] || []).concat(tabMap[z]).map(n => n.id)
|
|
},
|
|
setNodeOrder: function(z, order) {
|
|
var orderMap = {};
|
|
order.forEach(function(id,i) {
|
|
orderMap[id] = i;
|
|
})
|
|
tabMap[z].sort(function(A,B) {
|
|
A._reordered = true;
|
|
B._reordered = true;
|
|
return orderMap[A.id] - orderMap[B.id];
|
|
})
|
|
if (groupsByZ[z]) {
|
|
groupsByZ[z].sort(function(A,B) {
|
|
return orderMap[A.id] - orderMap[B.id];
|
|
})
|
|
}
|
|
},
|
|
/**
|
|
* Update our records if an object is dirty or not
|
|
* @param {String} z tab id
|
|
* @param {String} id object id
|
|
* @param {Boolean} dirty whether the object is dirty or not
|
|
*/
|
|
markNodeDirty: function(z, id, dirty) {
|
|
if (tabDirtyMap[z]) {
|
|
if (dirty) {
|
|
tabDirtyMap[z].add(id)
|
|
} else {
|
|
tabDirtyMap[z].delete(id)
|
|
}
|
|
api.checkTabState(z)
|
|
}
|
|
},
|
|
/**
|
|
* Check if a tab should update its contentsChange flag
|
|
* @param {String} z tab id
|
|
*/
|
|
checkTabState: function (z) {
|
|
const ws = workspaces[z] || subflows[z]
|
|
if (ws) {
|
|
const contentsChanged = tabDirtyMap[z].size > 0 || tabDeletedNodesMap[z].size > 0
|
|
if (Boolean(ws.contentsChanged) !== contentsChanged) {
|
|
ws.contentsChanged = contentsChanged
|
|
if (ws.type === 'tab') {
|
|
RED.events.emit("flows:change", ws);
|
|
} else {
|
|
RED.events.emit("subflows:change", ws);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return api;
|
|
})()
|
|
|
|
/**
|
|
* Generates a random ID consisting of 8 bytes.
|
|
*
|
|
* @returns {string} The generated ID.
|
|
*/
|
|
function generateId() {
|
|
var bytes = [];
|
|
for (var i=0;i<8;i++) {
|
|
bytes.push(Math.round(0xff*Math.random()).toString(16).padStart(2,'0'));
|
|
}
|
|
return bytes.join("");
|
|
}
|
|
|
|
function parseNodePropertyTypeString(typeString) {
|
|
typeString = typeString.trim();
|
|
var c;
|
|
var pos = 0;
|
|
var isArray = /\[\]$/.test(typeString);
|
|
if (isArray) {
|
|
typeString = typeString.substring(0,typeString.length-2);
|
|
}
|
|
|
|
var l = typeString.length;
|
|
var inBrackets = false;
|
|
var inToken = false;
|
|
var currentToken = "";
|
|
var types = [];
|
|
while (pos < l) {
|
|
c = typeString[pos];
|
|
if (inToken) {
|
|
if (c === "|") {
|
|
types.push(currentToken.trim())
|
|
currentToken = "";
|
|
inToken = false;
|
|
} else if (c === ")") {
|
|
types.push(currentToken.trim())
|
|
currentToken = "";
|
|
inBrackets = false;
|
|
inToken = false;
|
|
} else {
|
|
currentToken += c;
|
|
}
|
|
} else {
|
|
if (c === "(") {
|
|
if (inBrackets) {
|
|
throw new Error("Invalid character '"+c+"' at position "+pos)
|
|
}
|
|
inBrackets = true;
|
|
} else if (c !== " ") {
|
|
inToken = true;
|
|
currentToken = c;
|
|
}
|
|
}
|
|
pos++;
|
|
}
|
|
currentToken = currentToken.trim();
|
|
if (currentToken.length > 0) {
|
|
types.push(currentToken)
|
|
}
|
|
return {
|
|
types: types,
|
|
array: isArray
|
|
}
|
|
}
|
|
|
|
const nodeProxyHandler = {
|
|
get(node, prop) {
|
|
if (prop === '__isProxy__') {
|
|
return true
|
|
} else if (prop == '__node__') {
|
|
return node
|
|
}
|
|
return node[prop]
|
|
},
|
|
set(node, prop, value) {
|
|
if (node.z && (RED.nodes.workspace(node.z)?.locked || RED.nodes.subflow(node.z)?.locked)) {
|
|
if (
|
|
node._def.defaults[prop] ||
|
|
prop === 'z' ||
|
|
prop === 'l' ||
|
|
prop === 'd' ||
|
|
(prop === 'changed' && (!!node.changed) !== (!!value)) || // jshint ignore:line
|
|
((prop === 'x' || prop === 'y') && !node.resize && node.type !== 'group')
|
|
) {
|
|
throw new Error(`Cannot modified property '${prop}' of locked object '${node.type}:${node.id}'`)
|
|
}
|
|
}
|
|
if (node.z && (prop === 'changed' || prop === 'moved')) {
|
|
setTimeout(() => {
|
|
allNodes.markNodeDirty(node.z, node.id, node.changed || node.moved)
|
|
}, 0)
|
|
}
|
|
node[prop] = value;
|
|
return true
|
|
}
|
|
}
|
|
|
|
function addNode(n) {
|
|
let newNode
|
|
if (!n.__isProxy__) {
|
|
newNode = new Proxy(n, nodeProxyHandler)
|
|
} else {
|
|
newNode = 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(newNode);
|
|
}
|
|
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;
|
|
// TODO: The event should be triggered?
|
|
updateConfigNodeUsers(newNode, { action: "add" });
|
|
// TODO: What is this property used for?
|
|
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(newNode);
|
|
if (!nodeLinks[n.id]) {
|
|
nodeLinks[n.id] = {in:[],out:[]};
|
|
}
|
|
}
|
|
RED.events.emit('nodes:add',newNode);
|
|
return newNode
|
|
}
|
|
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);
|
|
allNodes.addObjectToWorkspace(l.source.z, getLinkId(l), true)
|
|
}
|
|
RED.events.emit("links:add",l);
|
|
}
|
|
|
|
function getLinkId(link) {
|
|
return link.source.id + ':' + link.sourcePort + ':' + link.target.id
|
|
}
|
|
|
|
|
|
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);
|
|
// TODO: The event should not be triggered?
|
|
updateConfigNodeUsers(node, { action: "remove" });
|
|
|
|
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;
|
|
// TODO: Not sure if still exists
|
|
if (configNode._def.exclusive) {
|
|
removeNode(node[d]);
|
|
removedNodes.push(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 oneditdelete - please report");
|
|
node._def.onremove.call(node);
|
|
}
|
|
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)
|
|
}
|
|
allNodes.removeObjectFromWorkspace(l.source.z, getLinkId(l))
|
|
}
|
|
}
|
|
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<removedNodes.length;i++) {
|
|
var result = removeNode(removedNodes[i].id);
|
|
removedLinks = removedLinks.concat(result.links);
|
|
}
|
|
for (i=0;i<removedJunctions.length;i++) {
|
|
var result = removeJunction(removedJunctions[i])
|
|
removedLinks = removedLinks.concat(result.links)
|
|
}
|
|
|
|
// Must get 'removedGroups' in the right order.
|
|
// - start with the top-most groups
|
|
// - then recurse into them
|
|
removedGroups = (groupsByZ[id] || []).filter(function(g) { return !g.g; });
|
|
for (i=0;i<removedGroups.length;i++) {
|
|
removedGroups[i].nodes.forEach(function(n) {
|
|
if (n.type === "group") {
|
|
removedGroups.push(n);
|
|
}
|
|
});
|
|
}
|
|
// Now remove them in the reverse order
|
|
for (i=removedGroups.length-1; i>=0; i--) {
|
|
removeGroup(removedGroups[i]);
|
|
}
|
|
allNodes.removeTab(id);
|
|
RED.events.emit('flows:remove',ws);
|
|
}
|
|
return {nodes:removedNodes,links:removedLinks, groups: removedGroups, junctions: removedJunctions};
|
|
}
|
|
|
|
/**
|
|
* Add a Subflow to the Workspace
|
|
*
|
|
* @param {object} sf The Subflow to add.
|
|
* @param {boolean|undefined} createNewIds Whether to create a new ID and update the name.
|
|
*/
|
|
function addSubflow(sf, createNewIds) {
|
|
if (createNewIds) {
|
|
// Update the Subflow Id
|
|
sf.id = generateId();
|
|
|
|
// Update the Subflow name to highlight that this is a copy
|
|
const subflowNames = Object.keys(subflows).map(function (sfid) {
|
|
return subflows[sfid].name || "";
|
|
}).sort();
|
|
|
|
let copyNumber = 1;
|
|
let subflowName = sf.name;
|
|
subflowNames.forEach(function(name) {
|
|
if (subflowName == name) {
|
|
subflowName = sf.name + " (" + copyNumber + ")";
|
|
copyNumber++;
|
|
}
|
|
});
|
|
|
|
sf.name = subflowName;
|
|
}
|
|
|
|
sf.instances = [];
|
|
|
|
subflows[sf.id] = sf;
|
|
allNodes.addTab(sf.id);
|
|
linkTabMap[sf.id] = [];
|
|
|
|
RED.nodes.registerType("subflow:"+sf.id, {
|
|
defaults:{
|
|
name:{value:""},
|
|
env:{value:[], validate: function(value) {
|
|
const errors = []
|
|
if (value) {
|
|
value.forEach(env => {
|
|
const r = RED.utils.validateTypedProperty(env.value, env.type)
|
|
if (r !== true) {
|
|
errors.push(env.name+': '+r)
|
|
}
|
|
})
|
|
}
|
|
if (errors.length === 0) {
|
|
return true
|
|
} else {
|
|
return errors
|
|
}
|
|
}}
|
|
},
|
|
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._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<sfNodes.length; i++) {
|
|
var node = sfNodes[i];
|
|
var m = /^subflow:(.+)$/.exec(node.type);
|
|
if (m) {
|
|
if (m[1] === nodeid) {
|
|
return true;
|
|
} else {
|
|
var result = subflowContains(m[1],nodeid);
|
|
if (result) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getDownstreamNodes(node) {
|
|
const downstreamLinks = nodeLinks[node.id].out
|
|
const downstreamNodes = new Set(downstreamLinks.map(l => l.target))
|
|
return Array.from(downstreamNodes)
|
|
}
|
|
function getAllDownstreamNodes(node) {
|
|
return getAllFlowNodes(node,'down').filter(function(n) { return n !== node });
|
|
}
|
|
function getAllUpstreamNodes(node) {
|
|
return getAllFlowNodes(node,'up').filter(function(n) { return n !== node });
|
|
}
|
|
function getAllFlowNodes(node, direction) {
|
|
var selection = RED.view.selection();
|
|
var visited = new Set();
|
|
var nodes = [node];
|
|
var initialNode = true;
|
|
while(nodes.length > 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)) {
|
|
if (d === 'locked' && !n.locked) {
|
|
continue
|
|
}
|
|
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) {
|
|
// 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<n.outputs;i++) {
|
|
node.wires.push([]);
|
|
}
|
|
var wires = links.filter(function(d){return d.source === n;});
|
|
for (var j=0;j<wires.length;j++) {
|
|
var w = wires[j];
|
|
if (w.target.type != "subflow") {
|
|
if (w.sourcePort < node.wires.length) {
|
|
node.wires[w.sourcePort].push(w.target.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (n.inputs > 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<wires.length;i++) {
|
|
var w = wires[i];
|
|
if (w.target.type != "subflow") {
|
|
nIn.wires.push({id:w.target.id})
|
|
}
|
|
}
|
|
node.in.push(nIn);
|
|
});
|
|
n.out.forEach(function(p,c) {
|
|
var nOut = {x:p.x,y:p.y,wires:[]};
|
|
var wires = links.filter(function(d) { return d.target === p });
|
|
for (i=0;i<wires.length;i++) {
|
|
if (wires[i].source.type != "subflow") {
|
|
nOut.wires.push({id:wires[i].source.id,port:wires[i].sourcePort})
|
|
} else {
|
|
nOut.wires.push({id:n.id,port:0})
|
|
}
|
|
}
|
|
node.out.push(nOut);
|
|
});
|
|
|
|
if (node.in.length > 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<set.length;n++) {
|
|
var node = set[n];
|
|
if (node.type.substring(0,8) == "subflow:") {
|
|
var subflowId = node.type.substring(8);
|
|
if (!exportedSubflows[subflowId]) {
|
|
exportedSubflows[subflowId] = true;
|
|
var subflow = getSubflow(subflowId);
|
|
var subflowSet = allNodes.getNodes(subflowId).slice();
|
|
subflowSet.unshift(subflow);
|
|
|
|
RED.nodes.eachConfig(function(n) {
|
|
if (n.z == subflowId) {
|
|
subflowSet.push(n);
|
|
exportedConfigNodes[n.id] = true;
|
|
}
|
|
});
|
|
|
|
subflowSet = subflowSet.concat(RED.nodes.junctions(subflowId))
|
|
subflowSet = subflowSet.concat(RED.nodes.groups(subflowId))
|
|
|
|
var exportableSubflow = createExportableNodeSet(subflowSet, exportedIds, exportedSubflows, exportedConfigNodes);
|
|
nns = exportableSubflow.concat(nns);
|
|
}
|
|
}
|
|
if (node.type !== "subflow") {
|
|
var convertedNode = RED.nodes.convertNode(node, { credentials: false });
|
|
for (var d in node._def.defaults) {
|
|
if (node._def.defaults[d].type) {
|
|
var nodeList = node[d];
|
|
if (!Array.isArray(nodeList)) {
|
|
nodeList = [nodeList];
|
|
}
|
|
nodeList = nodeList.filter(function(id) {
|
|
if (id in configNodes) {
|
|
var confNode = configNodes[id];
|
|
if (confNode._def.exportable !== false) {
|
|
if (!(id in exportedConfigNodes)) {
|
|
exportedConfigNodes[id] = true;
|
|
set.push(confNode);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
if (nodeList.length === 0) {
|
|
convertedNode[d] = Array.isArray(node[d])?[]:""
|
|
} else {
|
|
convertedNode[d] = Array.isArray(node[d])?nodeList:nodeList[0]
|
|
}
|
|
}
|
|
}
|
|
nns.push(convertedNode);
|
|
if (node.type === "group") {
|
|
nns = nns.concat(createExportableNodeSet(node.nodes, exportedIds, exportedSubflows, exportedConfigNodes));
|
|
}
|
|
} else {
|
|
var convertedSubflow = convertSubflow(node, { credentials: false });
|
|
nns.push(convertedSubflow);
|
|
}
|
|
}
|
|
return nns;
|
|
}
|
|
|
|
// Create the Flow JSON for the current configuration
|
|
// opts.credentials (whether to include (known) credentials) - default: true
|
|
// opts.dimensions (whether to include node dimensions) - default: false
|
|
function createCompleteNodeSet(opts) {
|
|
var nns = [];
|
|
var i;
|
|
for (i=0;i<workspacesOrder.length;i++) {
|
|
if (workspaces[workspacesOrder[i]].type == "tab") {
|
|
nns.push(convertWorkspace(workspaces[workspacesOrder[i]], opts));
|
|
}
|
|
}
|
|
for (i in subflows) {
|
|
if (subflows.hasOwnProperty(i)) {
|
|
nns.push(convertSubflow(subflows[i], opts));
|
|
}
|
|
}
|
|
for (i in groups) {
|
|
if (groups.hasOwnProperty(i)) {
|
|
nns.push(convertNode(groups[i], opts));
|
|
}
|
|
}
|
|
for (i in junctions) {
|
|
if (junctions.hasOwnProperty(i)) {
|
|
nns.push(convertNode(junctions[i], opts));
|
|
}
|
|
}
|
|
for (i in configNodes) {
|
|
if (configNodes.hasOwnProperty(i)) {
|
|
nns.push(convertNode(configNodes[i], opts));
|
|
}
|
|
}
|
|
RED.nodes.eachNode(function(n) {
|
|
nns.push(convertNode(n, opts));
|
|
})
|
|
return nns;
|
|
}
|
|
|
|
function checkForMatchingSubflow(subflow,subflowNodes) {
|
|
subflowNodes = subflowNodes || [];
|
|
var i;
|
|
var match = null;
|
|
RED.nodes.eachSubflow(function(sf) {
|
|
if (sf.name != subflow.name ||
|
|
sf.info != subflow.info ||
|
|
sf.in.length != subflow.in.length ||
|
|
sf.out.length != subflow.out.length) {
|
|
return;
|
|
}
|
|
var sfNodes = RED.nodes.filterNodes({z:sf.id});
|
|
if (sfNodes.length != subflowNodes.length) {
|
|
return;
|
|
}
|
|
|
|
var subflowNodeSet = [subflow].concat(subflowNodes);
|
|
var sfNodeSet = [sf].concat(sfNodes);
|
|
|
|
var exportableSubflowNodes = JSON.stringify(subflowNodeSet);
|
|
var exportableSFNodes = JSON.stringify(createExportableNodeSet(sfNodeSet));
|
|
var nodeMap = {};
|
|
for (i=0;i<sfNodes.length;i++) {
|
|
exportableSubflowNodes = exportableSubflowNodes.replace(new RegExp("\""+subflowNodes[i].id+"\"","g"),'"'+sfNodes[i].id+'"');
|
|
}
|
|
exportableSubflowNodes = exportableSubflowNodes.replace(new RegExp("\""+subflow.id+"\"","g"),'"'+sf.id+'"');
|
|
|
|
if (exportableSubflowNodes !== exportableSFNodes) {
|
|
return;
|
|
}
|
|
|
|
match = sf;
|
|
return false;
|
|
});
|
|
return match;
|
|
}
|
|
function compareNodes(nodeA,nodeB,idMustMatch) {
|
|
if (idMustMatch && nodeA.id != nodeB.id) {
|
|
return false;
|
|
}
|
|
if (nodeA.type != nodeB.type) {
|
|
return false;
|
|
}
|
|
var def = nodeA._def;
|
|
for (var d in def.defaults) {
|
|
if (def.defaults.hasOwnProperty(d)) {
|
|
var vA = nodeA[d];
|
|
var vB = nodeB[d];
|
|
if (typeof vA !== typeof vB) {
|
|
return false;
|
|
}
|
|
if (vA === null || typeof vA === "string" || typeof vA === "number") {
|
|
if (vA !== vB) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (JSON.stringify(vA) !== JSON.stringify(vB)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function identifyImportConflicts(importedNodes) {
|
|
var imported = {
|
|
tabs: {},
|
|
subflows: {},
|
|
groups: {},
|
|
junctions: {},
|
|
configs: {},
|
|
nodes: {},
|
|
all: [],
|
|
conflicted: {},
|
|
zMap: {},
|
|
}
|
|
|
|
importedNodes.forEach(function(n) {
|
|
imported.all.push(n);
|
|
if (n.type === "tab") {
|
|
imported.tabs[n.id] = n;
|
|
} else if (n.type === "subflow") {
|
|
imported.subflows[n.id] = n;
|
|
} else if (n.type === "group") {
|
|
imported.groups[n.id] = n;
|
|
} else if (n.type === "junction") {
|
|
imported.junctions[n.id] = n;
|
|
} else if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
|
|
imported.nodes[n.id] = n;
|
|
} else {
|
|
imported.configs[n.id] = n;
|
|
}
|
|
var nodeZ = n.z || "__global__";
|
|
imported.zMap[nodeZ] = imported.zMap[nodeZ] || [];
|
|
imported.zMap[nodeZ].push(n)
|
|
if (allNodes.hasNode(n.id) || configNodes[n.id] || workspaces[n.id] || subflows[n.id] || groups[n.id] || junctions[n.id]) {
|
|
imported.conflicted[n.id] = n;
|
|
}
|
|
})
|
|
return imported;
|
|
|
|
}
|
|
|
|
/**
|
|
* Replace the provided nodes.
|
|
* This must contain complete Subflow defs or complete Flow Tabs.
|
|
* It does not replace an individual node in the middle of a flow.
|
|
*/
|
|
function replaceNodes(newNodes) {
|
|
var zMap = {};
|
|
var newSubflows = {};
|
|
var newConfigNodes = {};
|
|
var removedNodes = [];
|
|
// Figure out what we're being asked to replace - subflows/configNodes
|
|
newNodes.forEach(function(n) {
|
|
if (n.type === "subflow") {
|
|
newSubflows[n.id] = n;
|
|
} else if (!n.hasOwnProperty('x') && !n.hasOwnProperty('y')) {
|
|
newConfigNodes[n.id] = n;
|
|
}
|
|
if (n.z) {
|
|
zMap[n.z] = zMap[n.z] || [];
|
|
zMap[n.z].push(n);
|
|
}
|
|
})
|
|
|
|
// Filter out config nodes inside a subflow def that is being replaced
|
|
var configNodeIds = Object.keys(newConfigNodes);
|
|
configNodeIds.forEach(function(id) {
|
|
var n = newConfigNodes[id];
|
|
if (newSubflows[n.z]) {
|
|
// This config node is in a subflow to be replaced.
|
|
// - remove from the list as it'll get handled with the subflow
|
|
delete newConfigNodes[id];
|
|
}
|
|
});
|
|
// Rebuild the list of ids
|
|
configNodeIds = Object.keys(newConfigNodes);
|
|
|
|
// ------------------------------
|
|
// Replace subflow definitions
|
|
//
|
|
// For each of the subflows to be replaced:
|
|
var newSubflowIds = Object.keys(newSubflows);
|
|
newSubflowIds.forEach(function(id) {
|
|
var n = newSubflows[id];
|
|
// Get a snapshot of the existing subflow definition
|
|
removedNodes = removedNodes.concat(createExportableSubflow(id));
|
|
// Remove the old subflow definition - but leave the instances in place
|
|
var removalResult = RED.subflow.removeSubflow(n.id, true);
|
|
// Create the list of nodes for the new subflow def
|
|
// Need to sort the list in order to remove missing nodes
|
|
var subflowNodes = [n].concat(zMap[n.id]).filter((s) => !!s);
|
|
// Import the new subflow - no clashes should occur as we've removed
|
|
// the old version
|
|
var result = importNodes(subflowNodes);
|
|
newSubflows[id] = getSubflow(id);
|
|
})
|
|
|
|
// Having replaced the subflow definitions, now need to update the
|
|
// instance nodes.
|
|
RED.nodes.eachNode(function(n) {
|
|
if (/^subflow:/.test(n.type)) {
|
|
var sfId = n.type.substring(8);
|
|
if (newSubflows[sfId]) {
|
|
// This is an instance of one of the replaced subflows
|
|
// - update the new def's instances array to include this one
|
|
newSubflows[sfId].instances.push(n);
|
|
// - update the instance's _def to point to the new def
|
|
n._def = RED.nodes.getType(n.type);
|
|
// - set all the flags so the view refreshes properly
|
|
n.dirty = true;
|
|
n.changed = true;
|
|
n._colorChanged = true;
|
|
}
|
|
}
|
|
})
|
|
|
|
newSubflowIds.forEach(function(id) {
|
|
var n = newSubflows[id];
|
|
RED.events.emit("subflows:change",n);
|
|
})
|
|
// Just in case the imported subflow changed color.
|
|
RED.utils.clearNodeColorCache();
|
|
|
|
// ------------------------------
|
|
// Replace config nodes
|
|
//
|
|
configNodeIds.forEach(function(id) {
|
|
const configNode = getNode(id);
|
|
const currentUserCount = configNode.users;
|
|
|
|
// Add a snapshot of the Config Node
|
|
removedNodes = removedNodes.concat(convertNode(configNode));
|
|
|
|
// Remove the Config Node instance
|
|
removeNode(id);
|
|
|
|
// Import the new one
|
|
importNodes([newConfigNodes[id]]);
|
|
|
|
// Re-attributes the user count
|
|
getNode(id).users = currentUserCount;
|
|
});
|
|
|
|
return {
|
|
removedNodes: removedNodes
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Analyzes the array of nodes passed as an argument to find unknown node types.
|
|
*
|
|
* Options:
|
|
* - `emitNotification` - Emit an notification with unknown types. Default false.
|
|
*
|
|
* @remarks Throws an error to be handled.
|
|
* @param {Array<object>} nodes An array of nodes to analyse
|
|
* @param {{ emitNotification?: boolean; }} options An options object
|
|
* @returns {Array<string>} An array with unknown types
|
|
*/
|
|
function identifyUnknowType(nodes, options = {}) {
|
|
const unknownTypes = [];
|
|
for (let node of nodes) {
|
|
// TODO: remove workspace in next release+1
|
|
const knowTypes = ["workspace", "tab", "subflow", "group", "junction"];
|
|
|
|
if (!knowTypes.includes(node.type) &&
|
|
node.type.substring(0, 8) != "subflow:" &&
|
|
!registry.getNodeType(node.type) &&
|
|
!unknownTypes.includes(node.type)) {
|
|
unknownTypes.push(node.type);
|
|
}
|
|
}
|
|
|
|
if (options.emitNotification && unknownTypes.length) {
|
|
const typeList = $("<ul>");
|
|
unknownTypes.forEach(function(type) {
|
|
$("<li>").text(type).appendTo(typeList);
|
|
});
|
|
|
|
RED.notify(
|
|
"<p>" + RED._("clipboard.importUnrecognised", {count: unknownTypes.length }) + "</p>" + typeList[0].outerHTML,
|
|
"error", false, 10000);
|
|
}
|
|
return unknownTypes;
|
|
}
|
|
|
|
/**
|
|
* Warns the user that the import contains existing nodes that the user need to resolve.
|
|
*
|
|
* @remarks Throws an error to be handled.
|
|
* @param {Array<object>} existingNodes An array containing conflicting nodes.
|
|
* @param {Array<object>} importedNodes An array containing all imported nodes.
|
|
*/
|
|
function emitExistingNodesNotification(existingNodes, importedNodes) {
|
|
const errorMessage = RED._("clipboard.importDuplicate", { count: existingNodes.length });
|
|
const maxItemCount = 5; // Max 5 items in the list
|
|
const nodeList = $("<ul>");
|
|
|
|
let itemCount = 0;
|
|
for (const { existing, imported } of existingNodes) {
|
|
if (itemCount >= maxItemCount) { break; }
|
|
const conflictType = (imported.type !== existing.type) ? " | " + imported.type : "";
|
|
$("<li>").text(existing.id + " [ " + existing.type + conflictType + " ]").appendTo(nodeList);
|
|
itemCount++;
|
|
}
|
|
|
|
if (existingNodes.length > maxItemCount) {
|
|
$("<li>").text(RED._("deploy.confirm.plusNMore", { count: (existingNodes.length - maxItemCount) })).appendTo(nodeList);
|
|
}
|
|
|
|
const wrapper = $("<p>").append(nodeList);
|
|
const existingNodesError = new Error(errorMessage + wrapper.html());
|
|
existingNodesError.code = "import_conflict";
|
|
existingNodesError.importConfig = identifyImportConflicts(importedNodes);
|
|
throw existingNodesError;
|
|
}
|
|
|
|
/**
|
|
* Makes a copy of the Config Node received as a parameter.
|
|
*
|
|
* @remarks Don't change the ID.
|
|
* @param {object} configNode The Config Node to copy.
|
|
* @param {object} def The Config Node definition.
|
|
* @param {object} options Same options as import
|
|
* @returns The new Config Node copied.
|
|
*/
|
|
function copyConfigNode(configNode, def, options = {}) {
|
|
const newNode = {
|
|
id: configNode.id,
|
|
z: configNode.z,
|
|
type: configNode.type,
|
|
changed: false,
|
|
icon: configNode.icon,
|
|
info: configNode.info,
|
|
label: def.label,
|
|
users: [],
|
|
_config: {},
|
|
_def: def
|
|
};
|
|
|
|
if (!configNode.z) {
|
|
delete newNode.z;
|
|
}
|
|
|
|
if (options.markChanged) {
|
|
newNode.changed = true;
|
|
}
|
|
|
|
if (configNode.hasOwnProperty("d")) { // Disabled
|
|
newNode.d = configNode.d;
|
|
}
|
|
|
|
// Copy node user properties
|
|
for (const d in def.defaults) {
|
|
if (def.defaults.hasOwnProperty(d)) {
|
|
newNode._config[d] = JSON.stringify(configNode[d]);
|
|
newNode[d] = configNode[d];
|
|
}
|
|
}
|
|
|
|
// Copy credentials - ONLY if the node contains it to avoid erase it
|
|
if (def.hasOwnProperty("credentials") && configNode.hasOwnProperty("credentials")) {
|
|
newNode.credentials = {};
|
|
for (const c in def.credentials) {
|
|
if (def.credentials.hasOwnProperty(c) && configNode.credentials.hasOwnProperty(c)) {
|
|
newNode.credentials[c] = configNode.credentials[c];
|
|
}
|
|
}
|
|
}
|
|
|
|
return newNode;
|
|
}
|
|
|
|
/**
|
|
* Makes a copy of the Node received as a parameter.
|
|
*
|
|
* @remarks Don't change the ID.
|
|
* @param {object} node The Node to copy.
|
|
* @param {object} def The Node definition.
|
|
* @param {object} options Same options as import
|
|
* @returns The new Node copied.
|
|
*/
|
|
function copyNode(node, def, options = {}) {
|
|
const newNode = {
|
|
id: node.id,
|
|
x: parseFloat(node.x || 0),
|
|
y: parseFloat(node.y || 0),
|
|
z: node.z,
|
|
type: node.type,
|
|
changed: false,
|
|
info: node.info,
|
|
_config: {},
|
|
_def: def
|
|
};
|
|
|
|
if (node.type !== "group" && node.type !== "junction") {
|
|
newNode.wires = node.wires || [];
|
|
newNode.inputLabels = node.inputLabels;
|
|
newNode.outputLabels = node.outputLabels;
|
|
newNode.icon = node.icon;
|
|
}
|
|
if (node.hasOwnProperty("l")) { // Label (show/hide)
|
|
newNode.l = node.l;
|
|
}
|
|
if (node.hasOwnProperty("d")) { // Disabled
|
|
newNode.d = node.d;
|
|
}
|
|
if (node.hasOwnProperty("g")) { // Group
|
|
newNode.g = node.g;
|
|
}
|
|
if (options.markChanged) {
|
|
newNode.changed = true;
|
|
}
|
|
|
|
if (node.type === "group") {
|
|
for (const d in newNode._def.defaults) {
|
|
if (newNode._def.defaults.hasOwnProperty(d) && d !== "inputs" && d !== "outputs") {
|
|
newNode[d] = node[d];
|
|
newNode._config[d] = JSON.stringify(node[d]);
|
|
}
|
|
}
|
|
newNode._config.x = node.x;
|
|
newNode._config.y = node.y;
|
|
if (node.hasOwnProperty("w")) { // Weight
|
|
newNode.w = node.w;
|
|
}
|
|
if (node.hasOwnProperty("h")) { // Height
|
|
newNode.h = node.h;
|
|
}
|
|
} else if (node.type === "junction") {
|
|
newNode._def = { defaults: {} };
|
|
newNode._config.x = node.x;
|
|
newNode._config.y = node.y;
|
|
newNode.wires = node.wires || [];
|
|
newNode.inputs = 1;
|
|
newNode.outputs = 1;
|
|
newNode.w = 0;
|
|
newNode.h = 0;
|
|
} else if (node.type.substring(0, 7) === "subflow") {
|
|
newNode.name = node.name;
|
|
newNode.inputs = node.inputs ?? 0;
|
|
newNode.outputs = node.outputs ?? 0;
|
|
newNode.env = node.env;
|
|
} else {
|
|
newNode._config.x = node.x;
|
|
newNode._config.y = node.y;
|
|
|
|
if (node.hasOwnProperty("inputs") && def.defaults.hasOwnProperty("inputs")) {
|
|
newNode._config.inputs = JSON.stringify(node.inputs);
|
|
newNode.inputs = parseInt(node.inputs, 10);
|
|
} else {
|
|
newNode.inputs = def.inputs;
|
|
}
|
|
|
|
if (node.hasOwnProperty("outputs") && def.defaults.hasOwnProperty("outputs")) {
|
|
newNode._config.outputs = JSON.stringify(node.outputs);
|
|
newNode.outputs = parseInt(node.outputs, 10);
|
|
} else {
|
|
newNode.outputs = def.outputs;
|
|
}
|
|
|
|
// The node declares outputs in its defaults, but has not got a valid value
|
|
// Defer to the length of the wires array
|
|
if (isNaN(newNode.outputs)) {
|
|
newNode.outputs = newNode.wires.length;
|
|
} else if (newNode.wires.length > newNode.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);
|
|
// TODO: Pas dans l'autre sens ?
|
|
newNode.wires = newNode.wires.slice(0, newNode.outputs);
|
|
}
|
|
|
|
// Copy node user properties
|
|
for (const d in def.defaults) {
|
|
if (newNode._def.defaults.hasOwnProperty(d) && d !== "inputs" && d !== "outputs") {
|
|
newNode._config[d] = JSON.stringify(node[d]);
|
|
newNode[d] = node[d];
|
|
}
|
|
}
|
|
|
|
// Copy credentials - ONLY if the node contains it to avoid erase it
|
|
if (def.hasOwnProperty("credentials") && node.hasOwnProperty("credentials")) {
|
|
newNode.credentials = {};
|
|
for (const c in def.credentials) {
|
|
if (newNode._def.credentials.hasOwnProperty(c) && node.credentials.hasOwnProperty(c)) {
|
|
newNode.credentials[c] = node.credentials[c];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return newNode;
|
|
}
|
|
|
|
/**
|
|
* Handles the import of nodes - these nodes can be copied, replaced or just imported.
|
|
*
|
|
* Options:
|
|
* - `addFlow` - whether to import nodes to a new tab. Default false.
|
|
* - `generateIds` - whether to replace all node ids. Default false.
|
|
* - `markChanged` - whether to set `changed` = true on all newly imported nodes.
|
|
* - `reimport` - if node has a `z` property, dont overwrite it
|
|
* Only applicible when `generateIds` is false. Default false.
|
|
* - `importMap` - how to resolve any conflicts.
|
|
* - nodeId: `import` - import as-is
|
|
* - nodeId: `copy` - import with new id
|
|
* - nodeId: `replace` - repace the existing
|
|
*
|
|
* @remarks Only Subflow definition (tab) and Config Node are replaceable!
|
|
*
|
|
* @typedef {string} NodeId
|
|
* @typedef {Record<NodeId, "copy" | "import" | "replace" | "skip">} ImportMap
|
|
*
|
|
* @param {string | object | Array<object>} originalNodes A node or array of nodes to import
|
|
* @param {{ addFlow?: boolean; generateIds?: boolean; importMap?: ImportMap; markChanged?: boolean;
|
|
* reimport?: boolean; }} options Options to involve on import.
|
|
* @returns An object containing all the elements created.
|
|
*/
|
|
function importNodes(originalNodes, options = {}) {
|
|
const defaultOptions = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} };
|
|
options = Object.assign({}, defaultOptions, options);
|
|
|
|
const createNewIds = options.generateIds;
|
|
const createNewWorkspace = options.addFlow;
|
|
const reimport = (!createNewIds && !!options.reimport);
|
|
|
|
// Checks and converts nodes into an Array if necessary
|
|
if (typeof originalNodes === "string") {
|
|
if (originalNodes === "") { return; }
|
|
try {
|
|
originalNodes = JSON.parse(originalNodes);
|
|
} catch(err) {
|
|
const error = new Error(RED._("clipboard.invalidFlow", { message: err.message }));
|
|
error.code = "NODE_RED";
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(originalNodes)) {
|
|
originalNodes = [originalNodes];
|
|
}
|
|
|
|
const seenIds = new Set();
|
|
const existingNodes = [];
|
|
const nodesToReplace = [];
|
|
// Checks if the imported nodes contains duplicates or existing nodes.
|
|
originalNodes = originalNodes.filter(function (node) {
|
|
const id = node.id;
|
|
|
|
// This is a temporary fix to help resolve corrupted flows caused by 0.20.0 where multiple
|
|
// copies of the flow would get loaded at the same time.
|
|
// NOTE: Generally it is the last occurrence which is kept but not here.
|
|
if (seenIds.has(id)) { return false; }
|
|
seenIds.add(id);
|
|
|
|
// Checks if the node already exists - the user will choose between copying the node, replacing it or not import it
|
|
if (!createNewIds) {
|
|
if (!options.importMap[id]) {
|
|
// No conflict resolution for this node
|
|
// TODO: Sure?
|
|
const existingNode = allNodes.getNode(id) || configNodes[id] || workspaces[id] || subflows[id] || groups[id] || junctions[id];
|
|
if (existingNode) {
|
|
existingNodes.push({ existing: existingNode, imported: node });
|
|
}
|
|
} else if (options.importMap[id] === "replace") {
|
|
nodesToReplace.push(node);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Ensure ignored nodes are not imported
|
|
if (options.importMap[id] === "skip") {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// If some nodes already exists, ask the user for each node to choose
|
|
// between copying it, replacing it or not importing it.
|
|
// NOTE: Stops the import here - throws an error.
|
|
if (existingNodes.length) {
|
|
emitExistingNodesNotification(existingNodes, originalNodes);
|
|
}
|
|
|
|
// NOTE: activeWorkspace can be equal to 0 if it's the initial load
|
|
let activeWorkspace = RED.workspaces.active();
|
|
const activeSubflow = getSubflow(activeWorkspace);
|
|
if (activeSubflow) {
|
|
for (const node of originalNodes) {
|
|
const group = /^subflow:(.+)$/.exec(node.type);
|
|
if (group) {
|
|
const subflowId = group[1];
|
|
let error;
|
|
|
|
if (subflowId === activeSubflow.id) {
|
|
error = new Error(RED._("notification.errors.cannotAddSubflowToItself"));
|
|
} else if (subflowContains(subflowId, activeSubflow.id)) {
|
|
error = new Error(RED._("notification.errors.cannotAddCircularReference"));
|
|
}
|
|
|
|
if (error) {
|
|
// TODO: standardise error codes
|
|
error.code = "NODE_RED";
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const removedNodes = [];
|
|
// Now that the user has made his choice, replace the nodes that need to be replaced.
|
|
if (nodesToReplace.length > 0) {
|
|
const result = replaceNodes(nodesToReplace);
|
|
removedNodes.concat(result.removedNodes);
|
|
}
|
|
|
|
let isInitialLoad = false;
|
|
if (!initialLoad) {
|
|
isInitialLoad = true;
|
|
initialLoad = JSON.parse(JSON.stringify(originalNodes));
|
|
}
|
|
|
|
// Identifies unknown nodes and can emit a notification to warn the user.
|
|
const unknownTypes = identifyUnknowType(originalNodes, { emitNotification: !isInitialLoad });
|
|
|
|
const subflowMap = {};
|
|
const workspaceMap = {};
|
|
const newSubflows = [];
|
|
const newWorkspaces = [];
|
|
// Find all tabs and subflow tabs and add it to workspace
|
|
// NOTE: Subflow tab not the instance
|
|
for (const node of originalNodes) {
|
|
const oldId = node.id;
|
|
|
|
// TODO: remove workspace in next release+1
|
|
if (node.type === "workspace" || node.type === "tab") {
|
|
// Legacy type
|
|
if (node.type === "workspace") {
|
|
node.type = "tab";
|
|
}
|
|
// The flow being sorted, the first workspace is supposed to be the default
|
|
if (defaultWorkspace == null) {
|
|
defaultWorkspace = node;
|
|
}
|
|
// If it is the initial load, the value is equal to 0
|
|
if (activeWorkspace === 0) {
|
|
activeWorkspace = defaultWorkspace.id;
|
|
}
|
|
if (createNewIds || options.importMap[node.id] === "copy") {
|
|
node.id = generateId();
|
|
}
|
|
addWorkspace(node);
|
|
workspaceMap[oldId] = node.id;
|
|
newWorkspaces.push(node);
|
|
RED.workspaces.add(node);
|
|
} else if (node.type === "subflow") {
|
|
node.in.forEach(function(input, i) {
|
|
input.type = "subflow";
|
|
input.direction = "in";
|
|
input.z = node.id;
|
|
input.i = i;
|
|
input.id = generateId();
|
|
});
|
|
node.out.forEach(function(output, i) {
|
|
output.type = "subflow";
|
|
output.direction = "out";
|
|
output.z = node.id;
|
|
output.i = i;
|
|
output.id = generateId();
|
|
});
|
|
if (node.status) {
|
|
node.status.type = "subflow";
|
|
node.status.direction = "status";
|
|
node.status.z = node.id;
|
|
node.status.id = generateId();
|
|
}
|
|
|
|
subflowMap[oldId] = node;
|
|
newSubflows.push(node);
|
|
// NOTE: Update the id in the `addSubflow` function
|
|
addSubflow(node, (createNewIds || options.importMap[node.id] === "copy"));
|
|
}
|
|
}
|
|
|
|
// Add a tab if there isn't one there already - Like first install
|
|
if (!defaultWorkspace) {
|
|
defaultWorkspace = { type: "tab", id: generateId(), disabled: false, info: "", label: RED._("workspace.defaultName", { number: 1 }), env: [] };
|
|
addWorkspace(defaultWorkspace);
|
|
RED.workspaces.add(defaultWorkspace);
|
|
newWorkspaces.push(defaultWorkspace);
|
|
activeWorkspace = defaultWorkspace.id;
|
|
}
|
|
|
|
let newWorkspace = null;
|
|
let recoveryWorkspace = null;
|
|
// Correct or update the z property of each node
|
|
for (const node of originalNodes) {
|
|
const isConfigNode = !node.hasOwnProperty("x") && !node.hasOwnProperty("y");
|
|
|
|
// If it's the initial load, create a recovery workspace if any nodes don't have `node.z` and assign to it.
|
|
if (!node.z && isInitialLoad && node.hasOwnProperty("x") && node.hasOwnProperty("y")) {
|
|
// Hit the rare issue where node z values get set to 0.
|
|
// Repair the flow - but we really need to track that down.
|
|
if (!recoveryWorkspace) {
|
|
recoveryWorkspace = {
|
|
id: generateId(),
|
|
type: "tab",
|
|
disabled: false,
|
|
label: RED._("clipboard.recoveredNodes"),
|
|
info: RED._("clipboard.recoveredNodesInfo"),
|
|
env: []
|
|
};
|
|
}
|
|
node.z = recoveryWorkspace.id;
|
|
continue;
|
|
}
|
|
|
|
// TODO: remove workspace in next release+1
|
|
if (node.type === "workspace" || node.type === "tab" || node.type === "subflow") { continue; }
|
|
|
|
// Fix `node.z` for not found/new one case
|
|
if (createNewIds || options.importMap[node.id] === "copy") {
|
|
// Config Node can have an undefined `node.z`
|
|
if (!isConfigNode || (isConfigNode && node.z)) {
|
|
if (subflowMap[node.z]) {
|
|
node.z = subflowMap[node.z].id;
|
|
} else {
|
|
node.z = workspaceMap[node.z];
|
|
if (!workspaces[node.z]) {
|
|
node.z = activeWorkspace;
|
|
if (createNewWorkspace) {
|
|
if (newWorkspace === null) {
|
|
newWorkspace = RED.workspaces.add(null, true);
|
|
newWorkspaces.push(newWorkspace);
|
|
}
|
|
node.z = newWorkspace.id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const keepNodesCurrentZ = reimport && node.z && (RED.workspaces.contains(node.z) || RED.nodes.subflow(node.z));
|
|
|
|
if (isConfigNode && !keepNodesCurrentZ && node.z && !workspaceMap[node.z] && !subflowMap[node.z]) {
|
|
node.z = activeWorkspace;
|
|
} else if (!isConfigNode && !keepNodesCurrentZ && (node.z == null || (!workspaceMap[node.z] && !subflowMap[node.z]))) {
|
|
node.z = activeWorkspace;
|
|
if (createNewWorkspace) {
|
|
if (!newWorkspace) {
|
|
newWorkspace = RED.workspaces.add(null,true);
|
|
newWorkspaces.push(newWorkspace);
|
|
}
|
|
node.z = newWorkspace.id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the recovery tab if used and warn the user
|
|
if (recoveryWorkspace) {
|
|
addWorkspace(recoveryWorkspace);
|
|
RED.workspaces.add(recoveryWorkspace);
|
|
// Put the recovery workspace at the first position
|
|
newWorkspaces.splice(0, 1, recoveryWorkspace, newWorkspaces[0]);
|
|
const notification = RED.notify(RED._("clipboard.recoveredNodesNotification", { flowName: RED._("clipboard.recoveredNodes") }), {
|
|
type: "warning",
|
|
fixed: true,
|
|
buttons: [
|
|
{ text: RED._("common.label.close"), click: function () { notification.close(); } }
|
|
]
|
|
});
|
|
}
|
|
|
|
const nodeMap = {};
|
|
const newNodes = [];
|
|
const newGroups = [];
|
|
const newJunctions = [];
|
|
// Find all Config Nodes, Groups, Junctions, Nodes and Subflow instances and add them
|
|
// NOTE: Replaced Config Nodes and Subflow instances no longer appear below
|
|
for (const node of originalNodes) {
|
|
const oldId = node.id;
|
|
|
|
// TODO: remove workspace in next release+1
|
|
if (node.type === "workspace" || node.type === "tab" || node.type === "subflow") { continue; }
|
|
|
|
// Get the Node definition
|
|
let def = registry.getNodeType(node.type);
|
|
|
|
// Update the Node definition for Subflow instance
|
|
// TODO: A thing with `node.i`
|
|
if (node.type.substring(0, 7) === "subflow") {
|
|
const parentId = node.type.split(":")[1];
|
|
const subflow = subflowMap[parentId] || getSubflow(parentId);
|
|
|
|
// If the parent Subflow is not found, this Subflow will be marked as unknown
|
|
if (subflow) {
|
|
if (createNewIds || options.importMap[node.id] === "copy") {
|
|
node.type = "subflow:" + subflow.id;
|
|
def = registry.getNodeType(node.type);
|
|
}
|
|
|
|
node.inputs = subflow.in.length;
|
|
node.outputs = subflow.out.length;
|
|
}
|
|
}
|
|
|
|
let isUnknownNode = false;
|
|
// Try to fix the node definition
|
|
if (!def) {
|
|
// Group Node
|
|
if (node.type === "group") {
|
|
def = RED.group.def;
|
|
}
|
|
// Unknown Config Node
|
|
else if (!node.hasOwnProperty("x") && !node.hasOwnProperty("y")) {
|
|
isUnknownNode = true;
|
|
def = {
|
|
category: "config",
|
|
defaults: {},
|
|
set: registry.getNodeSet("node-red/unknown")
|
|
};
|
|
// Unknown Node
|
|
} else {
|
|
isUnknownNode = true;
|
|
def = {
|
|
color: "#fee",
|
|
defaults: {},
|
|
label: "unknown: " + node.type,
|
|
labelStyle: "red-ui-flow-node-label-italic",
|
|
inputs: node.inputs ?? 0, // TODO: Find if the node has an input
|
|
outputs: node.outputs ?? node.wires?.length ?? 0,
|
|
set: registry.getNodeSet("node-red/unknown")
|
|
};
|
|
}
|
|
}
|
|
|
|
// TODO: Group Node has config as category - why?
|
|
const isConfigNode = def?.category === "config" && node.type !== "group";
|
|
|
|
// Now the properties have been fixed, copy the node properties:
|
|
// NOTE: If the Node def is unknown, user properties will not be copied
|
|
if (isConfigNode) {
|
|
nodeMap[oldId] = copyConfigNode(node, def, options);
|
|
} else {
|
|
// Node, Group, Junction or Subflow
|
|
nodeMap[oldId] = copyNode(node, def, options);
|
|
}
|
|
|
|
// Unknown Node - Copy user properties so that the node is always exportable
|
|
if (isUnknownNode) {
|
|
const propertiesNotCopyable = ["x", "y", "z", "id", "wires"];
|
|
nodeMap[oldId]._orig = Object.entries(node).reduce(function (orig, [prop, value]) {
|
|
if (node.hasOwnProperty(prop) && !propertiesNotCopyable.includes(prop)) {
|
|
orig[prop] = value;
|
|
}
|
|
return orig;
|
|
}, {});
|
|
|
|
nodeMap[oldId].name = node.type;
|
|
nodeMap[oldId].type = "unknown";
|
|
}
|
|
|
|
// Now the node has been copied, change the `id` if it's a copy
|
|
if (createNewIds || options.importMap[oldId] === "copy") {
|
|
nodeMap[oldId].id = generateId();
|
|
}
|
|
|
|
if (node.type === "junction") {
|
|
newJunctions.push(nodeMap[oldId])
|
|
} else if (node.type === "group") {
|
|
newGroups.push(nodeMap[oldId]);
|
|
} else {
|
|
newNodes.push(nodeMap[oldId]);
|
|
}
|
|
}
|
|
|
|
const newLinks = [];
|
|
// Remap all Wires and (Config) Node references
|
|
for (const node of [...newNodes, ...newGroups]) {
|
|
if (node.wires) {
|
|
for (const wiresGroup of node.wires) {
|
|
const wires = Array.isArray(wiresGroup) ? wiresGroup : [wiresGroup];
|
|
wires.forEach(function (wire) {
|
|
// Skip if the wire is clinked to a non-existent node
|
|
if (!nodeMap.hasOwnProperty(wire)) { return; }
|
|
if (node.z === nodeMap[wire].z) {
|
|
const link = { source: node, sourcePort: node.wires.indexOf(wiresGroup), target: nodeMap[wire] };
|
|
addLink(link);
|
|
newLinks.push(link);
|
|
} else {
|
|
console.log("Warning: dropping link that crosses tabs:", node.id, "->", nodeMap[wire].id);
|
|
}
|
|
});
|
|
}
|
|
delete node.wires;
|
|
}
|
|
|
|
// Update the Group id
|
|
if (node.g && nodeMap[node.g]) {
|
|
node.g = nodeMap[node.g].id;
|
|
} else {
|
|
delete node.g;
|
|
}
|
|
|
|
// If importing into a subflow, ensure an outbound-link doesn't get added
|
|
if (activeSubflow && /^link /.test(node.type) && node.links) {
|
|
node.links = node.links.filter(function (id) {
|
|
const otherNode = nodeMap[id] || RED.nodes.node(id);
|
|
return (otherNode?.z === activeWorkspace);
|
|
});
|
|
}
|
|
|
|
// Update the node id for select inputs and Links nodes
|
|
for (const d in node._def.defaults) {
|
|
if (node._def.defaults.hasOwnProperty(d) && node._def.defaults[d].type) {
|
|
let nodeList = node[d];
|
|
|
|
if (!Array.isArray(nodeList)) {
|
|
nodeList = [nodeList];
|
|
}
|
|
|
|
nodeList = nodeList.map(function (id) {
|
|
const n = nodeMap[id];
|
|
return n ? n.id : id;
|
|
});
|
|
|
|
node[d] = Array.isArray(node[d]) ? nodeList : nodeList[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add Links to Workspace
|
|
for (const subflow of newSubflows) {
|
|
subflow.in.forEach(function (input) {
|
|
input.wires.forEach(function (wire) {
|
|
if (nodeMap.hasOwnProperty(wire.id)) {
|
|
const link = { source: input, sourcePort: 0, target: nodeMap[wire.id] };
|
|
addLink(link);
|
|
newLinks.push(link);
|
|
}
|
|
});
|
|
delete input.wires;
|
|
});
|
|
|
|
subflow.out.forEach(function (output) {
|
|
output.wires.forEach(function (wire) {
|
|
let link;
|
|
if (subflowMap[wire.id] && subflowMap[wire.id].id === subflow.id) {
|
|
link = { source: subflow.in[wire.port], sourcePort: wire.port, target: output };
|
|
} else if (nodeMap.hasOwnProperty(wire.id) || subflowMap.hasOwnProperty(wire.id)) {
|
|
link = { source: nodeMap[wire.id] || subflowMap[wire.id], sourcePort: wire.port, target: output };
|
|
}
|
|
|
|
if (link) {
|
|
addLink(link);
|
|
newLinks.push(link);
|
|
}
|
|
});
|
|
delete output.wires;
|
|
});
|
|
|
|
subflow.status?.wires.forEach(function (wire) {
|
|
let link;
|
|
if (subflowMap[wire.id] && subflowMap[wire.id].id === subflow.id) {
|
|
link = { source: subflow.in[wire.port], sourcePort: wire.port, target: subflow.status };
|
|
} else if (nodeMap.hasOwnProperty(wire.id) || subflowMap.hasOwnProperty(wire.id)) {
|
|
link = { source: nodeMap[wire.id] || subflowMap[wire.id], sourcePort: wire.port, target: subflow.status };
|
|
}
|
|
|
|
if (link) {
|
|
addLink(link);
|
|
newLinks.push(link);
|
|
}
|
|
});
|
|
|
|
if (subflow.status) {
|
|
delete subflow.status.wires;
|
|
}
|
|
}
|
|
|
|
// Order the groups to ensure they are outer-most to inner-most
|
|
|
|
const groupDepthMap = {};
|
|
const groupsId = newGroups.map(function (group) { return group.id; });
|
|
for (const group of newGroups) {
|
|
// Delete the group if it is not part of the import
|
|
if (group.g && !groupsId.includes(group.g)) {
|
|
delete group.g;
|
|
}
|
|
// If the group does not contain a group, it's the outer-most
|
|
if (!group.g) {
|
|
groupDepthMap[group.id] = 0;
|
|
}
|
|
}
|
|
|
|
let changedDepth;
|
|
do {
|
|
changedDepth = false;
|
|
for (const group of newGroups) {
|
|
if (group.g) {
|
|
if (groupDepthMap[group.id] !== groupDepthMap[group.g] + 1) {
|
|
groupDepthMap[group.id] = groupDepthMap[group.g] + 1;
|
|
changedDepth = true;
|
|
}
|
|
}
|
|
}
|
|
} while(changedDepth);
|
|
|
|
newGroups.sort(function (a, b) {
|
|
return (groupDepthMap[a.id] - groupDepthMap[b.id]);
|
|
});
|
|
|
|
// Add Groups to Workspace
|
|
for (const index in newGroups) {
|
|
if (newGroups.indexOf(newGroups[index]) !== -1) {
|
|
newGroups[index] = addGroup(newGroups[index]);
|
|
nodeMap[newGroups[index].id] = newGroups[index];
|
|
}
|
|
}
|
|
|
|
// Add Junctions to Workspace
|
|
for (const index in newJunctions) {
|
|
if (newJunctions.indexOf(newJunctions[index]) !== -1) {
|
|
newJunctions[index] = addJunction(newJunctions[index]);
|
|
nodeMap[newJunctions[index].id] = newJunctions[index];
|
|
}
|
|
}
|
|
|
|
// Now the nodes have been fully updated, add them as Proxy.
|
|
for (const index in newNodes) {
|
|
if (newNodes.indexOf(newNodes[index]) !== -1) {
|
|
newNodes[index] = addNode(newNodes[index]);
|
|
nodeMap[newNodes[index].id] = newNodes[index];
|
|
}
|
|
}
|
|
|
|
// Finally validate all Nodes.
|
|
for (const node of newNodes) {
|
|
RED.editor.validateNode(node);
|
|
}
|
|
|
|
const lookupNode = (id) => {
|
|
const mappedNode = nodeMap[id];
|
|
if (!mappedNode) {
|
|
return null;
|
|
}
|
|
if (mappedNode.__isProxy__) {
|
|
return mappedNode;
|
|
} else {
|
|
return nodeMap[mappedNode.id];
|
|
}
|
|
};
|
|
|
|
// Update groups to reference proxy node objects
|
|
for (const group of newGroups) {
|
|
// bypass the proxy in case the flow is locked
|
|
group.__node__.nodes = group.nodes.map(lookupNode);
|
|
// Just in case the group references a node that doesn't exist for some reason
|
|
group.__node__.nodes = group.nodes.filter(function (n) {
|
|
if (n) {
|
|
// Repair any nodes that have forgotten they are in this group
|
|
if (n.g !== group.id) {
|
|
n.g = group.id;
|
|
}
|
|
}
|
|
return !!n;
|
|
});
|
|
}
|
|
|
|
// Update links to use proxy node objects
|
|
for (const link of newLinks) {
|
|
link.source = lookupNode(link.source.id) || link.source;
|
|
link.target = lookupNode(link.target.id) || link.target;
|
|
}
|
|
|
|
// TODO: Right place?
|
|
RED.workspaces.refresh();
|
|
|
|
return {
|
|
nodes: newNodes,
|
|
links: newLinks,
|
|
groups: newGroups,
|
|
junctions: newJunctions,
|
|
workspaces: newWorkspaces,
|
|
subflows: newSubflows,
|
|
missingWorkspace: newWorkspace,
|
|
removedNodes: removedNodes
|
|
};
|
|
}
|
|
|
|
// TODO: supports filter.z|type
|
|
function filterNodes(filter) {
|
|
return allNodes.filterNodes(filter);
|
|
}
|
|
|
|
function filterLinks(filter) {
|
|
var result = [];
|
|
var candidateLinks = [];
|
|
var hasCandidates = false;
|
|
var filterSZ = filter.source && filter.source.z;
|
|
var filterTZ = filter.target && filter.target.z;
|
|
var filterZ;
|
|
if (filterSZ || filterTZ) {
|
|
if (filterSZ === filterTZ) {
|
|
filterZ = filterSZ;
|
|
} else {
|
|
filterZ = (filterSZ === undefined)?filterTZ:filterSZ
|
|
}
|
|
}
|
|
if (filterZ) {
|
|
candidateLinks = linkTabMap[filterZ] || [];
|
|
hasCandidates = true;
|
|
} else if (filter.source && filter.source.hasOwnProperty("id")) {
|
|
if (nodeLinks[filter.source.id]) {
|
|
hasCandidates = true;
|
|
candidateLinks = candidateLinks.concat(nodeLinks[filter.source.id].out)
|
|
}
|
|
} else if (filter.target && filter.target.hasOwnProperty("id")) {
|
|
if (nodeLinks[filter.target.id]) {
|
|
hasCandidates = true;
|
|
candidateLinks = candidateLinks.concat(nodeLinks[filter.target.id].in)
|
|
}
|
|
}
|
|
if (!hasCandidates) {
|
|
candidateLinks = links;
|
|
}
|
|
for (var n=0;n<candidateLinks.length;n++) {
|
|
var link = candidateLinks[n];
|
|
if (filter.source) {
|
|
if (filter.source.hasOwnProperty("id") && link.source.id !== filter.source.id) {
|
|
continue;
|
|
}
|
|
if (filter.source.hasOwnProperty("z") && link.source.z !== filter.source.z) {
|
|
continue;
|
|
}
|
|
}
|
|
if (filter.target) {
|
|
if (filter.target.hasOwnProperty("id") && link.target.id !== filter.target.id) {
|
|
continue;
|
|
}
|
|
if (filter.target.hasOwnProperty("z") && link.target.z !== filter.target.z) {
|
|
continue;
|
|
}
|
|
}
|
|
if (filter.hasOwnProperty("sourcePort") && link.sourcePort !== filter.sourcePort) {
|
|
continue;
|
|
}
|
|
result.push(link);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Update any config nodes referenced by the provided node to ensure
|
|
* their 'users' list is correct.
|
|
*
|
|
* Options:
|
|
* - `action` - Add or remove the node from the Config Node users list. Default `add`.
|
|
* - `emitEvent` - Emit the `nodes:changes` event. Default true.
|
|
*
|
|
* @param {object} node The node in which to check if it contains references
|
|
* @param {{ action?: "add" | "remove"; emitEvent?: boolean; }} options Options to apply.
|
|
*/
|
|
function updateConfigNodeUsers(node, options = {}) {
|
|
const defaultOptions = { action: "add", emitEvent: true };
|
|
options = Object.assign({}, defaultOptions, options);
|
|
|
|
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) {
|
|
if (options.action === "add") {
|
|
if (configNode.users.indexOf(node) === -1) {
|
|
configNode.users.push(node);
|
|
if (options.emitEvent) {
|
|
RED.events.emit('nodes:change', configNode);
|
|
}
|
|
}
|
|
} else if (options.action === "remove") {
|
|
if (configNode.users.indexOf(node) !== -1) {
|
|
const users = configNode.users;
|
|
users.splice(users.indexOf(node), 1);
|
|
if (options.emitEvent) {
|
|
RED.events.emit('nodes:change', configNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function flowVersion(version) {
|
|
if (version !== undefined) {
|
|
loadedFlowVersion = version;
|
|
} else {
|
|
return loadedFlowVersion;
|
|
}
|
|
}
|
|
|
|
function clear() {
|
|
links = [];
|
|
linkTabMap = {};
|
|
nodeLinks = {};
|
|
configNodes = {};
|
|
workspacesOrder = [];
|
|
groups = {};
|
|
groupsByZ = {};
|
|
junctions = {};
|
|
junctionsByZ = {};
|
|
|
|
var workspaceIds = Object.keys(workspaces);
|
|
// Ensure all workspaces are unlocked so we don't get any edit-protection
|
|
// preventing removal
|
|
workspaceIds.forEach(function(id) {
|
|
workspaces[id].locked = false
|
|
});
|
|
|
|
var subflowIds = Object.keys(subflows);
|
|
subflowIds.forEach(function(id) {
|
|
RED.subflow.removeSubflow(id)
|
|
});
|
|
workspaceIds.forEach(function(id) {
|
|
RED.workspaces.remove(workspaces[id]);
|
|
});
|
|
defaultWorkspace = null;
|
|
initialLoad = null;
|
|
workspaces = {};
|
|
|
|
allNodes.clear();
|
|
|
|
RED.nodes.dirty(false);
|
|
RED.view.redraw(true, true);
|
|
RED.palette.refresh();
|
|
RED.workspaces.refresh();
|
|
RED.sidebar.config.refresh();
|
|
RED.sidebar.info.refresh();
|
|
|
|
RED.events.emit("workspace:clear");
|
|
}
|
|
|
|
function addGroup(group) {
|
|
if (!group.__isProxy__) {
|
|
group = new Proxy(group, nodeProxyHandler)
|
|
}
|
|
groupsByZ[group.z] = groupsByZ[group.z] || [];
|
|
groupsByZ[group.z].push(group);
|
|
groups[group.id] = group;
|
|
allNodes.addObjectToWorkspace(group.z, group.id, group.changed || group.moved)
|
|
RED.events.emit("groups:add",group);
|
|
return group
|
|
}
|
|
function removeGroup(group) {
|
|
var i = groupsByZ[group.z].indexOf(group);
|
|
groupsByZ[group.z].splice(i,1);
|
|
if (groupsByZ[group.z].length === 0) {
|
|
delete groupsByZ[group.z];
|
|
}
|
|
if (group.g) {
|
|
if (groups[group.g]) {
|
|
var index = groups[group.g].nodes.indexOf(group);
|
|
groups[group.g].nodes.splice(index,1);
|
|
}
|
|
}
|
|
RED.group.markDirty(group);
|
|
allNodes.removeObjectFromWorkspace(group.z, group.id)
|
|
delete groups[group.id];
|
|
RED.events.emit("groups:remove",group);
|
|
}
|
|
function getGroupOrder(z) {
|
|
const groups = groupsByZ[z]
|
|
return groups.map(g => g.id)
|
|
}
|
|
|
|
function addJunction(junction) {
|
|
if (!junction.__isProxy__) {
|
|
junction = new Proxy(junction, nodeProxyHandler)
|
|
}
|
|
junctionsByZ[junction.z] = junctionsByZ[junction.z] || []
|
|
junctionsByZ[junction.z].push(junction)
|
|
junctions[junction.id] = junction;
|
|
if (!nodeLinks[junction.id]) {
|
|
nodeLinks[junction.id] = {in:[],out:[]};
|
|
}
|
|
allNodes.addObjectToWorkspace(junction.z, junction.id, junction.changed || junction.moved)
|
|
RED.events.emit("junctions:add", junction)
|
|
return junction
|
|
}
|
|
function removeJunction(junction) {
|
|
var i = junctionsByZ[junction.z].indexOf(junction)
|
|
junctionsByZ[junction.z].splice(i, 1)
|
|
if (junctionsByZ[junction.z].length === 0) {
|
|
delete junctionsByZ[junction.z]
|
|
}
|
|
delete junctions[junction.id]
|
|
delete nodeLinks[junction.id];
|
|
allNodes.removeObjectFromWorkspace(junction.z, junction.id)
|
|
RED.events.emit("junctions:remove", junction)
|
|
|
|
var removedLinks = links.filter(function(l) { return (l.source === junction) || (l.target === junction); });
|
|
removedLinks.forEach(removeLink);
|
|
return { links: removedLinks }
|
|
}
|
|
|
|
function getNodeHelp(type) {
|
|
var helpContent = "";
|
|
var helpElement = $("script[data-help-name='"+type+"']");
|
|
if (helpElement) {
|
|
helpContent = helpElement.html();
|
|
var helpType = helpElement.attr("type");
|
|
if (helpType === "text/markdown") {
|
|
helpContent = RED.utils.renderMarkdown(helpContent);
|
|
}
|
|
}
|
|
return helpContent;
|
|
}
|
|
|
|
function getNodeIslands(nodes) {
|
|
var selectedNodes = new Set(nodes);
|
|
// Maps node => 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
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds unknown nodes of the same type and replaces them to update their
|
|
* definition.
|
|
*
|
|
* An imported node that has no definition is marked as unknown.
|
|
* When adding a new node type, this function searches for existing nodes
|
|
* of that type to update their definition.
|
|
*
|
|
* @param {string} type The type of the Node added
|
|
*/
|
|
function onNodeTypeAdded(type) {
|
|
const nodesToReplace = [];
|
|
RED.nodes.eachConfig(function (node) {
|
|
if (node.type === "unknown" && node.name === type) {
|
|
nodesToReplace.push(node);
|
|
}
|
|
});
|
|
|
|
RED.nodes.eachNode(function (node) {
|
|
if (node.type === "unknown" && node.name === type) {
|
|
nodesToReplace.push(node);
|
|
}
|
|
});
|
|
|
|
// Skip if there is nothing to replace
|
|
if (!nodesToReplace.length) {
|
|
return;
|
|
}
|
|
|
|
const nodeGroupMap = {};
|
|
const removedNodes = [];
|
|
nodesToReplace.forEach(function (node) {
|
|
// Create a snapshot of the Node
|
|
removedNodes.push(convertNode(node));
|
|
|
|
// Remove the Node
|
|
// NOTE: DON'T use removeNode - no need for everything that is done there.
|
|
// Just delete the node because we re-import it as is afterwards.
|
|
if (configNodes.hasOwnProperty(node.id)) {
|
|
delete configNodes[node.id];
|
|
} else {
|
|
allNodes.removeNode(node);
|
|
}
|
|
|
|
// Reimporting a node *without* including its group object will cause
|
|
// the g property to be cleared. Cache it here so we can restore it.
|
|
if (node.g) {
|
|
nodeGroupMap[node.id] = node.g
|
|
}
|
|
});
|
|
|
|
const removedLinks = [];
|
|
const nodeMap = nodesToReplace.reduce(function (map, node) {
|
|
map[node.id] = node;
|
|
return map;
|
|
}, {});
|
|
// Remove any links between nodes that are going to be reimported.
|
|
// This prevents a duplicate link from being added.
|
|
RED.nodes.eachLink(function (link) {
|
|
if (nodeMap.hasOwnProperty(link.source.id) && nodeMap.hasOwnProperty(link.target.id)) {
|
|
removedLinks.push(link);
|
|
}
|
|
});
|
|
|
|
removedLinks.forEach(removeLink);
|
|
|
|
// Force the redraw to be synchronous so the view updates
|
|
// *now* and removes the unknown node
|
|
RED.view.redraw(true, true);
|
|
|
|
// Re-import removed nodes - now nodes have their definition
|
|
const result = importNodes(removedNodes, { generateIds: false, reimport: true });
|
|
|
|
const newNodeMap = {};
|
|
// Rattach the nodes to their group
|
|
result?.nodes.forEach(function (node) {
|
|
newNodeMap[node.id] = node;
|
|
if (nodeGroupMap[node.id]) {
|
|
// This node is in a group - need to substitute the
|
|
// node reference inside the group
|
|
node.g = nodeGroupMap[node.id];
|
|
const group = RED.nodes.group(node.g);
|
|
if (group) {
|
|
const index = group.nodes.findIndex((g) => g.id === node.id);
|
|
if (index > -1) {
|
|
group.nodes[index] = node;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Relink nodes
|
|
RED.nodes.eachLink(function (link) {
|
|
if (newNodeMap.hasOwnProperty(link.source.id)) {
|
|
link.source = newNodeMap[link.source.id];
|
|
}
|
|
if (newNodeMap.hasOwnProperty(link.target.id)) {
|
|
link.target = newNodeMap[link.target.id];
|
|
}
|
|
});
|
|
|
|
RED.view.redraw(true);
|
|
}
|
|
|
|
return {
|
|
init: function () {
|
|
RED.events.on("registry:node-type-added", onNodeTypeAdded);
|
|
RED.events.on("deploy", function () {
|
|
allNodes.clearState();
|
|
});
|
|
},
|
|
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<links.length;l++) {
|
|
if (cb(links[l]) === false) {
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
eachConfig: function(cb) {
|
|
for (var id in configNodes) {
|
|
if (configNodes.hasOwnProperty(id)) {
|
|
if (cb(configNodes[id]) === false) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
eachSubflow: function(cb) {
|
|
for (var id in subflows) {
|
|
if (subflows.hasOwnProperty(id)) {
|
|
if (cb(subflows[id]) === false) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
eachWorkspace: function(cb) {
|
|
for (var i=0;i<workspacesOrder.length;i++) {
|
|
if (cb(workspaces[workspacesOrder[i]]) === false) {
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
eachGroup: function(cb) {
|
|
for (var group of Object.values(groups)) {
|
|
if (cb(group) === false) {
|
|
break
|
|
}
|
|
}
|
|
},
|
|
eachJunction: function(cb) {
|
|
for (var junction of Object.values(junctions)) {
|
|
if (cb(junction) === false) {
|
|
break
|
|
}
|
|
}
|
|
},
|
|
|
|
node: getNode,
|
|
|
|
version: flowVersion,
|
|
originalFlow: function(flow) {
|
|
if (flow === undefined) {
|
|
return initialLoad;
|
|
} else {
|
|
initialLoad = flow;
|
|
}
|
|
},
|
|
|
|
filterNodes: filterNodes,
|
|
filterLinks: filterLinks,
|
|
|
|
import: importNodes,
|
|
|
|
identifyImportConflicts: identifyImportConflicts,
|
|
|
|
getAllFlowNodes: getAllFlowNodes,
|
|
getAllUpstreamNodes: getAllUpstreamNodes,
|
|
getAllDownstreamNodes: getAllDownstreamNodes,
|
|
getDownstreamNodes: getDownstreamNodes,
|
|
getNodeIslands: getNodeIslands,
|
|
createExportableNodeSet: createExportableNodeSet,
|
|
createCompleteNodeSet: createCompleteNodeSet,
|
|
updateConfigNodeUsers: updateConfigNodeUsers,
|
|
id: generateId,
|
|
dirty: function(d) {
|
|
if (d == null) {
|
|
return dirty;
|
|
} else {
|
|
setDirty(d);
|
|
}
|
|
}
|
|
};
|
|
})();
|