From 6a2a763288117a159bc86c396a3f6b916560d9ba Mon Sep 17 00:00:00 2001 From: GogoVega <92022724+GogoVega@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:15:12 +0200 Subject: [PATCH] Doc the (new) functions and add the `onNodeTypeAdded` function --- .../@node-red/editor-client/src/js/nodes.js | 283 +++++++++++------- 1 file changed, 181 insertions(+), 102 deletions(-) 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 06bbfa2c9..9508a0d28 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 @@ -564,6 +564,11 @@ RED.nodes = (function() { 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++) { @@ -1693,7 +1698,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; @@ -1795,10 +1799,11 @@ RED.nodes = (function() { * Analyzes the array of nodes passed as an argument to find unknown node types. * * Options: - * - `emitNotification` (boolean) - Emit an notification with unknown types. + * - `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 {object} options An options object + * @param {{ emitNotification?: boolean; }} options An options object * @returns {Array} An array with unknown types */ function identifyUnknowType(nodes, options = {}) { @@ -1828,6 +1833,13 @@ RED.nodes = (function() { 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} existingNodes An array containing conflicting nodes. + * @param {Array} 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 @@ -1852,6 +1864,15 @@ RED.nodes = (function() { 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, @@ -1899,6 +1920,15 @@ RED.nodes = (function() { 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, @@ -2012,18 +2042,30 @@ RED.nodes = (function() { } /** + * Handles the import of nodes - these nodes can be copied, replaced or just imported. + * * 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 + * - `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} ImportMap + * + * @param {string | object | Array} 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) { + function importNodes(originalNodes, options = {}) { const defaultOptions = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} }; options = Object.assign({}, defaultOptions, options); @@ -2075,6 +2117,11 @@ RED.nodes = (function() { } } + // Ensure ignored nodes are not imported + if (options.importMap[id] === "skip") { + return false; + } + return true; }); @@ -2653,30 +2700,40 @@ RED.nodes = (function() { return result; } - // Update any config nodes referenced by the provided node to ensure their 'users' list is correct - function updateConfigNodeUsers(n, options) { + /** + * 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 n._def.defaults) { - if (n._def.defaults.hasOwnProperty(d)) { - var property = n._def.defaults[d]; + 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[n[d]]; + var configNode = configNodes[node[d]]; if (configNode) { if (options.action === "add") { - if (configNode.users.indexOf(n) === -1) { - configNode.users.push(n); + 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(n) !== -1) { + if (configNode.users.indexOf(node) !== -1) { const users = configNode.users; - users.splice(users.indexOf(n), 1); + users.splice(users.indexOf(node), 1); if (options.emitEvent) { RED.events.emit('nodes:change', configNode); } @@ -2952,88 +3009,110 @@ RED.nodes = (function() { } } - return { - init: function() { - RED.events.on("registry:node-type-added",function(type) { - var def = registry.getNodeType(type); - var replaced = false; - var replaceNodes = {}; - RED.nodes.eachNode(function(n) { - if (n.type === "unknown" && n.name === type) { - replaceNodes[n.id] = n; + /** + * 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 + 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; } - }); - RED.nodes.eachConfig(function(n) { - if (n.type === "unknown" && n.name === type) { - replaceNodes[n.id] = n; - } - }); - - const nodeGroupMap = {} - var replaceNodeIds = Object.keys(replaceNodes); - if (replaceNodeIds.length > 0) { - var reimportList = []; - replaceNodeIds.forEach(function(id) { - var n = replaceNodes[id]; - if (configNodes.hasOwnProperty(n.id)) { - delete configNodes[n.id]; - } else { - allNodes.removeNode(n); - } - if (n.g) { - // reimporting a node *without* including its group object - // will cause the g property to be cleared. Cache it - // here so we can restore it - nodeGroupMap[n.id] = n.g - } - reimportList.push(convertNode(n)); - RED.events.emit('nodes:remove',n); - }); - - // Remove any links between nodes that are going to be reimported. - // This prevents a duplicate link from being added. - var removeLinks = []; - RED.nodes.eachLink(function(l) { - if (replaceNodes.hasOwnProperty(l.source.id) && replaceNodes.hasOwnProperty(l.target.id)) { - removeLinks.push(l); - } - }); - removeLinks.forEach(removeLink); - - // Force the redraw to be synchronous so the view updates - // *now* and removes the unknown node - RED.view.redraw(true, true); - var result = importNodes(reimportList,{generateIds:false, reimport: true}); - var newNodeMap = {}; - result.nodes.forEach(function(n) { - newNodeMap[n.id] = n; - if (nodeGroupMap[n.id]) { - // This node is in a group - need to substitute the - // node reference inside the group - n.g = nodeGroupMap[n.id] - const group = RED.nodes.group(n.g) - if (group) { - var index = group.nodes.findIndex(gn => gn.id === n.id) - if (index > -1) { - group.nodes[index] = n - } - } - } - }); - RED.nodes.eachLink(function(l) { - if (newNodeMap.hasOwnProperty(l.source.id)) { - l.source = newNodeMap[l.source.id]; - } - if (newNodeMap.hasOwnProperty(l.target.id)) { - l.target = newNodeMap[l.target.id]; - } - }); - RED.view.redraw(true); } + } + }); + + // 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(); }); - RED.events.on('deploy', function () { - allNodes.clearState() - }) }, registry:registry, setNodeList: registry.setNodeList,