diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index 60e71c391..2b5b259e6 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -574,6 +574,24 @@ RED.history = (function() { RED.editor.updateNodeProperties(ev.node,outputMap); RED.editor.validateNode(ev.node); } + // If it's a Config Node, validate user nodes too. + // NOTE: The Config Node must be validated before validating users. + if (ev.node.users) { + const validatedNodes = new Set(); + const userStack = ev.node.users.slice(); + + validatedNodes.add(ev.node.id); + while (userStack.length) { + const node = userStack.pop(); + if (!validatedNodes.has(node.id)) { + validatedNodes.add(node.id); + if (node.users) { + userStack.push(...node.users); + } + RED.editor.validateNode(node); + } + } + } if (ev.links) { inverseEv.createdLinks = []; for (i=0;i n.outputs)) { n.outputs = n.wires.length; } n.dirty = true; - updateConfigNodeUsers(newNode); + updateConfigNodeUsers(newNode, { action: "add" }); if (n._def.category == "subflows" && typeof n.i === "undefined") { var nextId = 0; RED.nodes.eachNode(function(node) { @@ -779,6 +785,7 @@ RED.nodes = (function() { delete nodeLinks[id]; removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); }); removedLinks.forEach(removeLink); + updateConfigNodeUsers(node, { action: "remove" }); var updatedConfigNode = false; for (var d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d)) { @@ -792,10 +799,6 @@ RED.nodes = (function() { 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) } } } @@ -1046,23 +1049,34 @@ 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 update the name. + */ function addSubflow(sf, createNewIds) { if (createNewIds) { - var subflowNames = Object.keys(subflows).map(function(sfid) { - return subflows[sfid].name; - }); + // Update the Subflow name to highlight that this is a copy + const subflowNames = Object.keys(subflows).map(function (sfid) { + return subflows[sfid].name || ""; + }) + subflowNames.sort() - subflowNames.sort(); - var copyNumber = 1; - var subflowName = sf.name; + 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] = []; @@ -1115,7 +1129,7 @@ RED.nodes = (function() { module: "node-red" } }); - sf.instances = []; + sf._def = RED.nodes.getType("subflow:"+sf.id); RED.events.emit("subflows:add",sf); } @@ -1757,7 +1771,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); @@ -1794,9 +1809,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 { @@ -2037,6 +2063,8 @@ RED.nodes = (function() { if (matchingSubflow) { subflow_denylist[n.id] = matchingSubflow; } else { + const oldId = n.id; + subflow_map[n.id] = n; if (createNewIds || options.importMap[n.id] === "copy") { nid = getID(); @@ -2064,7 +2092,7 @@ RED.nodes = (function() { n.status.id = getID(); } new_subflows.push(n); - addSubflow(n,createNewIds || options.importMap[n.id] === "copy"); + addSubflow(n,createNewIds || options.importMap[oldId] === "copy"); } } } @@ -2184,7 +2212,7 @@ RED.nodes = (function() { x:parseFloat(n.x || 0), y:parseFloat(n.y || 0), z:n.z, - type:0, + type: n.type, info: n.info, changed:false, _config:{} @@ -2245,7 +2273,6 @@ RED.nodes = (function() { } } } - node.type = n.type; node._def = def; if (node.type === "group") { node._def = RED.group.def; @@ -2275,6 +2302,15 @@ RED.nodes = (function() { outputs: n.outputs|| (n.wires && n.wires.length) || 0, set: registry.getNodeSet("node-red/unknown") } + var orig = {}; + for (var p in n) { + if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") { + orig[p] = n[p]; + } + } + node._orig = orig; + node.name = n.type; + node.type = "unknown"; } else { if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") { parentId = subflow.id; @@ -2335,29 +2371,31 @@ RED.nodes = (function() { node.type = "unknown"; } if (node._def.category != "config") { - if (n.hasOwnProperty('inputs')) { - node.inputs = n.inputs; + if (n.hasOwnProperty('inputs') && def.defaults.hasOwnProperty("inputs")) { + node.inputs = parseInt(n.inputs, 10); node._config.inputs = JSON.stringify(n.inputs); } else { node.inputs = node._def.inputs; } - if (n.hasOwnProperty('outputs')) { - node.outputs = n.outputs; + if (n.hasOwnProperty('outputs') && def.defaults.hasOwnProperty("outputs")) { + node.outputs = parseInt(n.outputs, 10); node._config.outputs = JSON.stringify(n.outputs); } else { node.outputs = node._def.outputs; } - if (node.hasOwnProperty('wires') && node.wires.length > node.outputs) { - if (!node._def.defaults.hasOwnProperty("outputs") || !isNaN(parseInt(n.outputs))) { - // If 'wires' is longer than outputs, clip wires - console.log("Warning: node.wires longer than node.outputs - trimming wires:",node.id," wires:",node.wires.length," outputs:",node.outputs); - node.wires = node.wires.slice(0,node.outputs); - } else { - // The node declares outputs in its defaults, but has not got a valid value - // Defer to the length of the wires array + + // The node declares outputs in its defaults, but has not got a valid value + // Defer to the length of the wires array + if (node.hasOwnProperty('wires')) { + if (isNaN(node.outputs)) { node.outputs = node.wires.length; + } else if (node.wires.length > node.outputs) { + // If 'wires' is longer than outputs, clip wires + console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs); + node.wires = node.wires.slice(0, node.outputs); } } + for (d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { node[d] = n[d]; @@ -2454,11 +2492,6 @@ RED.nodes = (function() { nodeList = nodeList.map(function(id) { var node = node_map[id]; if (node) { - if (node._def.category === 'config') { - if (node.users.indexOf(n) === -1) { - node.users.push(n); - } - } return node.id; } return id; @@ -2472,9 +2505,11 @@ RED.nodes = (function() { n = new_subflows[i]; n.in.forEach(function(input) { input.wires.forEach(function(wire) { - var link = {source:input, sourcePort:0, target:node_map[wire.id]}; - addLink(link); - new_links.push(link); + if (node_map.hasOwnProperty(wire.id)) { + var link = {source:input, sourcePort:0, target:node_map[wire.id]}; + addLink(link); + new_links.push(link); + } }); delete input.wires; }); @@ -2483,11 +2518,13 @@ RED.nodes = (function() { var link; if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { link = {source:n.in[wire.port], sourcePort:wire.port,target:output}; - } else { + } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) { link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:output}; } - addLink(link); - new_links.push(link); + if (link) { + addLink(link); + new_links.push(link); + } }); delete output.wires; }); @@ -2496,11 +2533,13 @@ RED.nodes = (function() { var link; if (subflow_map[wire.id] && subflow_map[wire.id].id == n.id) { link = {source:n.in[wire.port], sourcePort:wire.port,target:n.status}; - } else { + } else if (node_map.hasOwnProperty(wire.id) || subflow_map.hasOwnProperty(wire.id)) { link = {source:node_map[wire.id]||subflow_map[wire.id], sourcePort:wire.port,target:n.status}; } - addLink(link); - new_links.push(link); + if (link) { + addLink(link); + new_links.push(link); + } }); delete n.status.wires; } @@ -2679,19 +2718,44 @@ RED.nodes = (function() { return result; } - // Update any config nodes referenced by the provided node to ensure their 'users' list is correct - function updateConfigNodeUsers(n) { - for (var d in n._def.defaults) { - if (n._def.defaults.hasOwnProperty(d)) { - var property = n._def.defaults[d]; + /** + * 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[n[d]]; + var configNode = configNodes[node[d]]; if (configNode) { - if (configNode.users.indexOf(n) === -1) { - configNode.users.push(n); - RED.events.emit('nodes:change',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); + } + } } } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 8d515e7c8..635bc8ca2 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -808,6 +808,17 @@ RED.editor = (function() { } } + const oldCreds = {}; + if (editing_node._def.credentials) { + for (const prop in editing_node._def.credentials) { + if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { + if (prop in editing_node.credentials) { + oldCreds[prop] = editing_node.credentials[prop]; + } + } + } + } + try { const rc = editing_node._def.oneditsave.call(editing_node); if (rc === true) { @@ -839,6 +850,21 @@ RED.editor = (function() { } } } + + if (editing_node._def.credentials) { + for (const prop in editing_node._def.credentials) { + if (Object.prototype.hasOwnProperty.call(editing_node._def.credentials, prop)) { + if (oldCreds[prop] !== editing_node.credentials[prop]) { + if (editing_node.credentials[prop] === '__PWRD__') { + continue; + } + editState.changes.credentials = editState.changes.credentials || {}; + editState.changes.credentials[prop] = oldCreds[prop]; + editState.changed = true; + } + } + } + } } } @@ -1481,134 +1507,181 @@ RED.editor = (function() { }, { id: "node-config-dialog-ok", - text: adding?RED._("editor.configAdd"):RED._("editor.configUpdate"), + text: adding ? RED._("editor.configAdd") : RED._("editor.configUpdate"), class: "primary", click: function() { - var editState = { + // TODO: Already defined + const configProperty = name; + const configType = type; + const configTypeDef = RED.nodes.getType(configType); + + const wasChanged = editing_config_node.changed; + const editState = { changes: {}, changed: false, outputMap: null }; - var configProperty = name; - var configId = editing_config_node.id; - var configType = type; - var configAdding = adding; - var configTypeDef = RED.nodes.getType(configType); - var d; - var input; + + // Call `oneditsave` and search for changes + handleEditSave(editing_config_node, editState); - if (configTypeDef.oneditsave) { - try { - configTypeDef.oneditsave.call(editing_config_node); - } catch(err) { - console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString()); - } - } - - for (d in configTypeDef.defaults) { - if (configTypeDef.defaults.hasOwnProperty(d)) { - var newValue; - input = $("#node-config-input-"+d); - if (input.attr('type') === "checkbox") { - newValue = input.prop('checked'); - } else if ("format" in configTypeDef.defaults[d] && configTypeDef.defaults[d].format !== "" && input[0].nodeName === "DIV") { - newValue = input.text(); - } else { - newValue = input.val(); - } - if (newValue != null && newValue !== editing_config_node[d]) { - if (editing_config_node._def.defaults[d].type) { - if (newValue == "_ADD_") { - newValue = ""; - } - // Change to a related config node - var configNode = RED.nodes.node(editing_config_node[d]); - if (configNode) { - var users = configNode.users; - users.splice(users.indexOf(editing_config_node),1); - RED.events.emit("nodes:change",configNode); - } - configNode = RED.nodes.node(newValue); - if (configNode) { - configNode.users.push(editing_config_node); - RED.events.emit("nodes:change",configNode); - } - } - editing_config_node[d] = newValue; - } - } - } - - activeEditPanes.forEach(function(pane) { + // Search for changes in the edit box (panes) + activeEditPanes.forEach(function (pane) { if (pane.apply) { pane.apply.call(pane, editState); } - }) + }); - editing_config_node.label = configTypeDef.label; - - var scope = $("#red-ui-editor-config-scope").val(); - editing_config_node.z = scope; + // TODO: Why? + editing_config_node.label = configTypeDef.label + // Check if disabled has changed if ($("#node-config-input-node-disabled").prop('checked')) { if (editing_config_node.d !== true) { + editState.changes.d = editing_config_node.d; + editState.changed = true; editing_config_node.d = true; } } else { if (editing_config_node.d === true) { + editState.changes.d = editing_config_node.d; + editState.changed = true; delete editing_config_node.d; } } + // NOTE: must be undefined if no scope used + const scope = $("#red-ui-editor-config-scope").val() || undefined; + + // Check if the scope has changed + if (editing_config_node.z !== scope) { + editState.changes.z = editing_config_node.z; + editState.changed = true; + editing_config_node.z = scope; + } + + // Search for nodes that use this config node that are no longer + // in scope, so must be removed + const historyEvents = []; if (scope) { - // Search for nodes that use this one that are no longer - // in scope, so must be removed - editing_config_node.users = editing_config_node.users.filter(function(n) { - var keep = true; - for (var d in n._def.defaults) { - if (n._def.defaults.hasOwnProperty(d)) { - if (n._def.defaults[d].type === editing_config_node.type && - n[d] === editing_config_node.id && - n.z !== scope) { - keep = false; - // Remove the reference to this node - // and revalidate - n[d] = null; - n.dirty = true; - n.changed = true; - validateNode(n); + const newUsers = editing_config_node.users.filter(function (node) { + let keepNode = false; + let nodeModified = null; + + for (const d in node._def.defaults) { + if (node._def.defaults.hasOwnProperty(d)) { + if (node._def.defaults[d].type === editing_config_node.type) { + if (node[d] === editing_config_node.id) { + if (node.z === editing_config_node.z) { + // The node is kept only if at least one property uses + // this config node in the correct scope. + keepNode = true; + } else { + if (!nodeModified) { + nodeModified = { + t: "edit", + node: node, + changes: { [d]: node[d] }, + changed: node.changed, + dirty: node.dirty + }; + } else { + nodeModified.changes[d] = node[d]; + } + + // Remove the reference to the config node + node[d] = ""; + } + } } } } - return keep; - }); - } - if (configAdding) { - RED.nodes.add(editing_config_node); - } - - validateNode(editing_config_node); - var validatedNodes = {}; - validatedNodes[editing_config_node.id] = true; - - var userStack = editing_config_node.users.slice(); - while(userStack.length > 0) { - var user = userStack.pop(); - if (!validatedNodes[user.id]) { - validatedNodes[user.id] = true; - if (user.users) { - userStack = userStack.concat(user.users); + // Add the node modified to the history + if (nodeModified) { + historyEvents.push(nodeModified); } - validateNode(user); + + // Mark as changed and revalidate this node + if (!keepNode) { + node.changed = true; + node.dirty = true; + validateNode(node); + RED.events.emit("nodes:change", node); + } + + return keepNode; + }); + + // Check if users are changed + if (editing_config_node.users.length !== newUsers.length) { + editState.changes.users = editing_config_node.users; + editState.changed = true; + editing_config_node.users = newUsers; } } - RED.nodes.dirty(true); - RED.view.redraw(true); - if (!configAdding) { - RED.events.emit("editor:save",editing_config_node); - RED.events.emit("nodes:change",editing_config_node); + + if (editState.changed) { + // Set the congig node as changed + editing_config_node.changed = true; } + + // Now, validate the config node + validateNode(editing_config_node); + + // And validate nodes using this config node too + const validatedNodes = new Set(); + const userStack = editing_config_node.users.slice(); + + validatedNodes.add(editing_config_node.id); + while (userStack.length) { + const node = userStack.pop(); + if (!validatedNodes.has(node.id)) { + validatedNodes.add(node.id); + if (node.users) { + userStack.push(...node.users); + } + validateNode(node); + } + } + + let historyEvent = { + t: "edit", + node: editing_config_node, + changes: editState.changes, + changed: wasChanged, + dirty: RED.nodes.dirty() + }; + + if (historyEvents.length) { + // Need a multi events + historyEvent = { + t: "multi", + events: [historyEvent].concat(historyEvents), + dirty: historyEvent.dirty + }; + } + + if (!adding) { + // This event is triggered when the edit box is saved, + // regardless of whether there are any modifications. + RED.events.emit("editor:save", editing_config_node); + } + + if (editState.changed) { + if (adding) { + RED.history.push({ t: "add", nodes: [editing_config_node.id], dirty: RED.nodes.dirty() }); + // Add the new config node and trigger the `nodes:add` event + RED.nodes.add(editing_config_node); + } else { + RED.history.push(historyEvent); + RED.events.emit("nodes:change", editing_config_node); + } + + RED.nodes.dirty(true); + RED.view.redraw(true); + } + RED.tray.close(function() { var filter = null; // when editing a config via subflow edit panel, the `configProperty` will not diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js index 3f35d98a8..396317052 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/properties.js @@ -132,9 +132,16 @@ } } if (node._def.credentials) { - var credDefinition = node._def.credentials; - var credsChanged = updateNodeCredentials(node,credDefinition,this.inputClass); - editState.changed = editState.changed || credsChanged; + const credDefinition = node._def.credentials; + const credChanges = updateNodeCredentials(node, credDefinition, this.inputClass); + + if (Object.keys(credChanges).length) { + editState.changed = true; + editState.changes.credentials = { + ...(editState.changes.credentials || {}), + ...credChanges + }; + } } } } @@ -162,10 +169,11 @@ * @param node - the node containing the credentials * @param credDefinition - definition of the credentials * @param prefix - prefix of the input fields - * @return {boolean} whether anything has changed + * @return {object} an object containing the modified properties */ function updateNodeCredentials(node, credDefinition, prefix) { - var changed = false; + const changes = {}; + if (!node.credentials) { node.credentials = {_:{}}; } else if (!node.credentials._) { @@ -178,22 +186,32 @@ if (input.length > 0) { var value = input.val(); if (credDefinition[cred].type == 'password') { - node.credentials['has_' + cred] = (value !== ""); - if (value == '__PWRD__') { - continue; + if (value === '__PWRD__') { + // A cred value exists - no changes + } else if (value === '' && node.credentials['has_' + cred] === false) { + // Empty cred value exists - no changes + } else if (value === node.credentials[cred]) { + // A cred value exists locally in the editor - no changes + // Like the user sets a value, saves the config, + // reopens the config and save the config again + } else { + changes[cred] = node.credentials[cred]; + node.credentials[cred] = value; } - changed = true; - } - node.credentials[cred] = value; - if (value != node.credentials._[cred]) { - changed = true; + node.credentials['has_' + cred] = (value !== ''); + } else { + // Since these creds are loaded by the editor, + // values can be directly compared + if (value !== node.credentials[cred]) { + changes[cred] = node.credentials[cred]; + node.credentials[cred] = value; + } } } } } - return changed; + + return changes; } - - })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index 3e1b9a410..603dc8d30 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -512,6 +512,7 @@ RED.subflow = (function() { removedNodes.push(n); } if (n.z == id) { + RED.nodes.updateConfigNodeUsers(n, { action: "remove" }); removedNodes.push(n); } }); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js b/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js index 216cc3e40..f284f2464 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/typeSearch.js @@ -382,7 +382,7 @@ RED.typeSearch = (function() { var items = []; RED.nodes.registry.getNodeTypes().forEach(function(t) { var def = RED.nodes.getType(t); - if (def.category !== 'config' && t !== 'unknown' && t !== 'tab') { + if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') { items.push({type:t,def: def, label:getTypeLabel(t,def)}); } }); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index ff0091e65..245b6db75 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -288,7 +288,7 @@ RED.view = (function() { } selectedLinks.clearUnselected() }, - length: () => groups.length, + length: () => groups.size, forEach: (func) => { groups.forEach(func) }, toArray: () => [...groups], clear: function () { @@ -2689,22 +2689,21 @@ RED.view = (function() { addToRemovedLinks(reconnectResult.removedLinks) } - var startDirty = RED.nodes.dirty(); - var startChanged = false; - var selectedGroups = []; + const startDirty = RED.nodes.dirty(); + let movingSelectedGroups = []; if (movingSet.length() > 0) { for (var i=0;i=0; i--) { - var g = selectedGroups[i]; + for (i = movingSelectedGroups.length-1; i>=0; i--) { + var g = movingSelectedGroups[i]; removedGroups.push(g); RED.nodes.removeGroup(g); } diff --git a/packages/node_modules/@node-red/nodes/core/function/89-delay.js b/packages/node_modules/@node-red/nodes/core/function/89-delay.js index b0fc2ed86..8f9964115 100644 --- a/packages/node_modules/@node-red/nodes/core/function/89-delay.js +++ b/packages/node_modules/@node-red/nodes/core/function/89-delay.js @@ -253,7 +253,13 @@ module.exports = function(RED) { if (node.allowrate && m.hasOwnProperty("rate") && !isNaN(parseFloat(m.rate))) { node.rate = m.rate; } - send(m); + if (msg.hasOwnProperty("reset")) { + if (msg.hasOwnProperty("flush")) { + node.buffer.push({msg: m, send: send, done: done}); + } + } + else { send(m); } + node.reportDepth(); node.intervalID = setInterval(sendMsgFromBuffer, node.rate); done(); @@ -303,7 +309,8 @@ module.exports = function(RED) { node.droppedMsgs++; } } - } else { + } + else { if (node.allowrate && msg.hasOwnProperty("rate") && !isNaN(parseFloat(msg.rate))) { node.rate = msg.rate; } diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index fa25a26d0..6b7f659b9 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -113,6 +113,10 @@ async function evaluateEnvProperties(flow, env, credentials) { resolve() }); })) + } else if (type === "conf-type" && /^\${[^}]+}$/.test(value)) { + // Get the config node from the parent subflow + const name = value.substring(2, value.length - 1); + value = flow.getSetting(name); } else { try { value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); diff --git a/test/nodes/core/function/89-delay_spec.js b/test/nodes/core/function/89-delay_spec.js index 46b0037bc..4fbf3df54 100644 --- a/test/nodes/core/function/89-delay_spec.js +++ b/test/nodes/core/function/89-delay_spec.js @@ -1009,6 +1009,29 @@ describe('delay Node', function() { }); }); + it('sending a msg with reset to empty queue doesnt send anything', function(done) { + this.timeout(2000); + var flow = [{"id":"delayNode1","type":"delay","name":"delayNode","pauseType":"rate","timeout":1,"timeoutUnits":"seconds","rate":2,"rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(delayNode, flow, function() { + var delayNode1 = helper.getNode("delayNode1"); + var helperNode1 = helper.getNode("helperNode1"); + var t = Date.now(); + var c = 0; + helperNode1.on("input", function(msg) { + console.log("Shold not get here") + done(e); + }); + + setTimeout( function() { + if (c === 0) { done(); } + }, 250); + + // send test messages + delayNode1.receive({payload:1,topic:"foo",reset:true}); // send something with blank topic + }); + }); + /* Messaging API support */ function mapiDoneTestHelper(done, pauseType, drop, msgAndTimings) { const completeNode = require("nr-test-utils").require("@node-red/nodes/core/common/24-complete.js");