diff --git a/package.json b/package.json index de431e01d..3ec82a3a3 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ ], "dependencies": { "ajv": "6.12.3", + "async-mutex": "0.2.4", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "body-parser": "1.19.0", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/searchBox.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/searchBox.js index bea8705bb..173d34034 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/searchBox.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/searchBox.js @@ -119,6 +119,9 @@ if (evt.keyCode === 27) { that.element.val(""); } + if (evt.keyCode === 13) { + evt.preventDefault(); + } }) this.element.on("keyup",function(evt) { that._change($(this).val()); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js index e7220b817..fdfca23b4 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js @@ -380,11 +380,16 @@ RED.group = (function() { return; } } + var existingGroup; + // Second pass, ungroup any groups in the selection and add their contents // to the selection for (var i=0; i 0) { + if (ns.length > 0 && mouse_mode == RED.state.MOVING_ACTIVE) { historyEvent = {t:"move",nodes:ns,dirty:RED.nodes.dirty()}; if (activeSpliceLink) { // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp @@ -2354,6 +2354,10 @@ RED.view = (function() { mousedown_port_type = null; activeSpliceLink = null; spliceActive = false; + if (activeHoverGroup) { + activeHoverGroup.hovered = false; + activeHoverGroup = null; + } d3.select(".red-ui-flow-link-splice").classed("red-ui-flow-link-splice",false); if (spliceTimer) { clearTimeout(spliceTimer); @@ -2869,7 +2873,7 @@ RED.view = (function() { //RED.touch.radialMenu.show(d3.select(this),pos); if (mouse_mode == RED.state.IMPORT_DRAGGING) { RED.keyboard.remove("escape"); - + var historyEvent = RED.history.peek(); if (activeSpliceLink) { // TODO: DRY - droppable/nodeMouseDown/canvasMouseUp var spliceLink = d3.select(activeSpliceLink).data()[0]; @@ -2886,12 +2890,27 @@ RED.view = (function() { }; RED.nodes.addLink(link1); RED.nodes.addLink(link2); - var historyEvent = RED.history.peek(); + historyEvent.links = [link1,link2]; historyEvent.removedLinks = [spliceLink]; updateActiveNodes(); } + if (activeHoverGroup) { + for (var j=0;j 0) { // If the group was just moved, all of its contents was // also moved - so no need to recalculate its bounding box @@ -4446,6 +4466,7 @@ RED.view = (function() { d.y = minY; d.w = maxX - minX; d.h = maxY - minY; + recalculateLabelOffsets = true; // if set explicitly to false, this group has just been // imported so needed this initial resize calculation. // Now that's done, delete the flag so the normal @@ -4459,28 +4480,31 @@ RED.view = (function() { } else { d.w = 40; d.h = 40; + recalculateLabelOffsets = true; } - if (!d.minWidth) { - if (d.style.label && d.name) { - var labelParts = getLabelParts(d.name||"","red-ui-flow-group-label"); - d.minWidth = labelParts.width + 8; - d.labels = labelParts.lines; - } else { - d.minWidth = 40; - d.labels = []; + if (recalculateLabelOffsets) { + if (!d.minWidth) { + if (d.style.label && d.name) { + var labelParts = getLabelParts(d.name||"","red-ui-flow-group-label"); + d.minWidth = labelParts.width + 8; + d.labels = labelParts.lines; + } else { + d.minWidth = 40; + d.labels = []; + } } - } - d.w = Math.max(d.minWidth,d.w); - if (d.style.label && d.labels.length > 0) { - var labelPos = d.style["label-position"] || "nw"; - var h = (d.labels.length-1) * 16; - if (labelPos[0] === "s") { - h += 8; - } - d.h += h; - if (labelPos[0] === "n") { - if (d.nodes.length > 0) { - d.y -= h; + d.w = Math.max(d.minWidth,d.w); + if (d.style.label && d.labels.length > 0) { + var labelPos = d.style["label-position"] || "nw"; + var h = (d.labels.length-1) * 16; + if (labelPos[0] === "s") { + h += 8; + } + d.h += h; + if (labelPos[0] === "n") { + if (d.nodes.length > 0) { + d.y -= h; + } } } } diff --git a/packages/node_modules/@node-red/runtime/lib/api/flows.js b/packages/node_modules/@node-red/runtime/lib/api/flows.js index 277099ecc..6cbda197a 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/flows.js +++ b/packages/node_modules/@node-red/runtime/lib/api/flows.js @@ -34,6 +34,8 @@ */ var runtime; +var Mutex = require('async-mutex').Mutex; +const mutex = new Mutex(); var api = module.exports = { init: function(_runtime) { @@ -64,37 +66,39 @@ var api = module.exports = { * @memberof @node-red/runtime_flows */ setFlows: function(opts) { - return new Promise(function(resolve,reject) { + return mutex.runExclusive(function() { + return new Promise(function(resolve,reject) { - var flows = opts.flows; - var deploymentType = opts.deploymentType||"full"; - runtime.log.audit({event: "flows.set",type:deploymentType}, opts.req); + var flows = opts.flows; + var deploymentType = opts.deploymentType||"full"; + runtime.log.audit({event: "flows.set",type:deploymentType}, opts.req); - var apiPromise; - if (deploymentType === 'reload') { - apiPromise = runtime.nodes.loadFlows(true); - } else { - if (flows.hasOwnProperty('rev')) { - var currentVersion = runtime.nodes.getFlows().rev; - if (currentVersion !== flows.rev) { - var err; - err = new Error(); - err.code = "version_mismatch"; - err.status = 409; - //TODO: log warning - return reject(err); + var apiPromise; + if (deploymentType === 'reload') { + apiPromise = runtime.nodes.loadFlows(true); + } else { + if (flows.hasOwnProperty('rev')) { + var currentVersion = runtime.nodes.getFlows().rev; + if (currentVersion !== flows.rev) { + var err; + err = new Error(); + err.code = "version_mismatch"; + err.status = 409; + //TODO: log warning + return reject(err); + } } + apiPromise = runtime.nodes.setFlows(flows.flows,flows.credentials,deploymentType); } - apiPromise = runtime.nodes.setFlows(flows.flows,flows.credentials,deploymentType); - } - apiPromise.then(function(flowId) { - return resolve({rev:flowId}); - }).catch(function(err) { - runtime.log.warn(runtime.log._("api.flows.error-"+(deploymentType === 'reload'?'reload':'save'),{message:err.message})); - runtime.log.warn(err.stack); - return reject(err); + apiPromise.then(function(flowId) { + return resolve({rev:flowId}); + }).catch(function(err) { + runtime.log.warn(runtime.log._("api.flows.error-"+(deploymentType === 'reload'?'reload':'save'),{message:err.message})); + runtime.log.warn(err.stack); + return reject(err); + }); }); - }); + }); }, /** @@ -107,19 +111,23 @@ var api = module.exports = { * @memberof @node-red/runtime_flows */ addFlow: function(opts) { - return new Promise(function(resolve,reject) { - var flow = opts.flow; - runtime.nodes.addFlow(flow).then(function(id) { - runtime.log.audit({event: "flow.add",id:id}, opts.req); - return resolve(id); - }).catch(function(err) { - runtime.log.audit({event: "flow.add",error:err.code||"unexpected_error",message:err.toString()}, opts.req); - err.status = 400; - return reject(err); + return mutex.runExclusive(function() { + return new Promise(function (resolve, reject) { + var flow = opts.flow; + runtime.nodes.addFlow(flow).then(function (id) { + runtime.log.audit({event: "flow.add", id: id}, opts.req); + return resolve(id); + }).catch(function (err) { + runtime.log.audit({ + event: "flow.add", + error: err.code || "unexpected_error", + message: err.toString() + }, opts.req); + err.status = 400; + return reject(err); + }) }) - }) - - + }); }, /** @@ -145,7 +153,6 @@ var api = module.exports = { return reject(err); } }) - }, /** * Updates an existing flow configuration @@ -158,33 +165,42 @@ var api = module.exports = { * @memberof @node-red/runtime_flows */ updateFlow: function(opts) { - return new Promise(function (resolve,reject) { - var flow = opts.flow; - var id = opts.id; - try { - runtime.nodes.updateFlow(id,flow).then(function() { - runtime.log.audit({event: "flow.update",id:id}, opts.req); - return resolve(id); - }).catch(function(err) { - runtime.log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()}, opts.req); - err.status = 400; - return reject(err); - }) - } catch(err) { - if (err.code === 404) { - runtime.log.audit({event: "flow.update",id:id,error:"not_found"}, opts.req); - // TODO: this swap around of .code and .status isn't ideal - err.status = 404; - err.code = "not_found"; - return reject(err); - } else { - runtime.log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()}, opts.req); - err.status = 400; - return reject(err); + return mutex.runExclusive(function() { + return new Promise(function (resolve, reject) { + var flow = opts.flow; + var id = opts.id; + try { + runtime.nodes.updateFlow(id, flow).then(function () { + runtime.log.audit({event: "flow.update", id: id}, opts.req); + return resolve(id); + }).catch(function (err) { + runtime.log.audit({ + event: "flow.update", + error: err.code || "unexpected_error", + message: err.toString() + }, opts.req); + err.status = 400; + return reject(err); + }) + } catch (err) { + if (err.code === 404) { + runtime.log.audit({event: "flow.update", id: id, error: "not_found"}, opts.req); + // TODO: this swap around of .code and .status isn't ideal + err.status = 404; + err.code = "not_found"; + return reject(err); + } else { + runtime.log.audit({ + event: "flow.update", + error: err.code || "unexpected_error", + message: err.toString() + }, opts.req); + err.status = 400; + return reject(err); + } } - } + }); }); - }, /** * Deletes a flow @@ -196,30 +212,42 @@ var api = module.exports = { * @memberof @node-red/runtime_flows */ deleteFlow: function(opts) { - return new Promise(function (resolve,reject) { - var id = opts.id; - try { - runtime.nodes.removeFlow(id).then(function() { - runtime.log.audit({event: "flow.remove",id:id}, opts.req); - return resolve(); - }).catch(function(err) { - runtime.log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()}, opts.req); - err.status = 400; - return reject(err); - }); - } catch(err) { - if (err.code === 404) { - runtime.log.audit({event: "flow.remove",id:id,error:"not_found"}, opts.req); - // TODO: this swap around of .code and .status isn't ideal - err.status = 404; - err.code = "not_found"; - return reject(err); - } else { - runtime.log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()}, opts.req); - err.status = 400; - return reject(err); + return mutex.runExclusive(function() { + return new Promise(function (resolve, reject) { + var id = opts.id; + try { + runtime.nodes.removeFlow(id).then(function () { + runtime.log.audit({event: "flow.remove", id: id}, opts.req); + return resolve(); + }).catch(function (err) { + runtime.log.audit({ + event: "flow.remove", + id: id, + error: err.code || "unexpected_error", + message: err.toString() + }, opts.req); + err.status = 400; + return reject(err); + }); + } catch (err) { + if (err.code === 404) { + runtime.log.audit({event: "flow.remove", id: id, error: "not_found"}, opts.req); + // TODO: this swap around of .code and .status isn't ideal + err.status = 404; + err.code = "not_found"; + return reject(err); + } else { + runtime.log.audit({ + event: "flow.remove", + id: id, + error: err.code || "unexpected_error", + message: err.toString() + }, opts.req); + err.status = 400; + return reject(err); + } } - } + }); }); }, @@ -264,5 +292,4 @@ var api = module.exports = { resolve(sendCredentials); }) } - } diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index 45c7f69b8..7a7c66c14 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -18,6 +18,7 @@ "dependencies": { "@node-red/registry": "1.2.0-alpha.1", "@node-red/util": "1.2.0-alpha.1", + "async-mutex": "0.2.4", "clone": "2.1.2", "express": "4.17.1", "fs-extra": "8.1.0",