diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 2a7b440f2..23ea99230 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -591,7 +591,12 @@ RED.nodes = (function() { return api; })() - function getID() { + /** + * 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')); @@ -706,7 +711,9 @@ RED.nodes = (function() { } else { if (n.wires && (n.wires.length > n.outputs)) { n.outputs = n.wires.length; } n.dirty = true; - updateConfigNodeUsers(n); + // 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) { @@ -779,6 +786,9 @@ RED.nodes = (function() { 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)) { @@ -789,13 +799,10 @@ RED.nodes = (function() { 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); - } else { - var users = configNode.users; - users.splice(users.indexOf(node),1); - RED.events.emit('nodes:change',configNode) } } } @@ -1032,23 +1039,36 @@ RED.nodes = (function() { 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) { - var subflowNames = Object.keys(subflows).map(function(sfid) { - return subflows[sfid].name; - }); + // Update the Subflow Id + sf.id = generateId(); - subflowNames.sort(); - var copyNumber = 1; - var subflowName = sf.name; + // 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++; - subflowName = sf.name+" ("+copyNumber+")"; } }); + sf.name = subflowName; } + + sf.instances = []; + subflows[sf.id] = sf; allNodes.addTab(sf.id); linkTabMap[sf.id] = []; @@ -1101,7 +1121,7 @@ RED.nodes = (function() { module: "node-red" } }); - sf.instances = []; + sf._def = RED.nodes.getType("subflow:"+sf.id); RED.events.emit("subflows:add",sf); } @@ -1705,7 +1725,6 @@ RED.nodes = (function() { var newConfigNodes = {}; var removedNodes = []; // Figure out what we're being asked to replace - subflows/configNodes - // TODO: config nodes newNodes.forEach(function(n) { if (n.type === "subflow") { newSubflows[n.id] = n; @@ -1743,7 +1762,8 @@ RED.nodes = (function() { // 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 - var subflowNodes = [n].concat(zMap[n.id]); + // 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); @@ -1780,9 +1800,20 @@ RED.nodes = (function() { // Replace config nodes // configNodeIds.forEach(function(id) { - removedNodes = removedNodes.concat(convertNode(getNode(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); - importNodes([newConfigNodes[id]]) + + // Import the new one + importNodes([newConfigNodes[id]]); + + // Re-attributes the user count + getNode(id).users = currentUserCount; }); return { @@ -1792,798 +1823,846 @@ RED.nodes = (function() { } /** + * Analyzes the array of nodes passed as an argument to find unknown node types. + * * Options: - * - generateIds - whether to replace all node ids - * - addFlow - whether to import nodes to a new tab - * - markChanged - whether to set changed=true on all newly imported objects - * - reimport - if node has a .z property, dont overwrite it - * Only applicible when `generateIds` is false - * - importMap - how to resolve any conflicts. - * - id:import - import as-is - * - id:copy - import with new id - * - id:replace - import over the top of existing + * - `emitNotification` - Emit an notification with unknown types. Default false. + * + * @remarks Throws an error to be handled. + * @param {Array} nodes An array of nodes to analyse + * @param {{ emitNotification?: boolean; }} options An options object + * @returns {Array} An array with unknown types */ - function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) { - const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} } - options = Object.assign({}, defOpts, options) - options.importMap = options.importMap || {} - const createNewIds = options.generateIds; - const reimport = (!createNewIds && !!options.reimport) - const createMissingWorkspace = options.addFlow; - var i; - var n; - var newNodes; - var nodeZmap = {}; - var recoveryWorkspace; - if (typeof newNodesObj === "string") { - if (newNodesObj === "") { - return; + 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); } - try { - newNodes = JSON.parse(newNodesObj); - } catch(err) { - var e = new Error(RED._("clipboard.invalidFlow",{message:err.message})); - e.code = "NODE_RED"; - throw e; + } + + if (options.emitNotification && unknownTypes.length) { + const typeList = $("