diff --git a/packages/node_modules/@node-red/editor-client/src/js/comms.js b/packages/node_modules/@node-red/editor-client/src/js/comms.js index 2af8b69a6..a58ce8f7c 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/comms.js +++ b/packages/node_modules/@node-red/editor-client/src/js/comms.js @@ -120,8 +120,8 @@ RED.comms = (function() { subscribers[i](msg.topic,msg.data); } catch (error) { // need to decide what to do with this uncaught error - console.warn('Uncaught error from RED.comms.subscribe: ' + err.toString()) - console.warn(err) + console.warn('Uncaught error from RED.comms.subscribe: ' + error.toString()) + console.warn(error) } } } 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 142606822..08e83ee87 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 @@ -514,7 +514,15 @@ RED.history = (function() { } } } - + if (ev.node.type === 'subflow') { + // Ensure ports get a refresh in case of a label change + if (ev.changes.inputLabels) { + ev.node.in.forEach(function(input) { input.dirty = true; }); + } + if (ev.changes.outputLabels) { + ev.node.out.forEach(function(output) { output.dirty = true; }); + } + } ev.node.dirty = true; ev.node.changed = ev.changed; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js index c70757dfa..b528579ef 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/contextMenu.js @@ -223,6 +223,11 @@ RED.contextMenu = (function () { { onselect: 'core:show-export-dialog', label: RED._("menu.label.export") } ) } + if (hasSelection && canEdit) { + menuItems.push( + { onselect: 'core:convert-to-subflow', label: RED._("menu.label.selectionToSubflow") } + ) + } menuItems.push( { onselect: 'core:select-all-nodes', label: RED._("keyboard.selectAll") } ) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index d318f476c..d929e2560 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -180,7 +180,7 @@ RED.deploy = (function() { } function updateLockedState() { - if (RED.settings.user?.permissions === 'read') { + if (!RED.user.hasPermission('flows.write')) { $(".red-ui-deploy-button-group").addClass("readOnly"); $("#red-ui-header-button-deploy").addClass("disabled"); } else { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js index d6dd5112d..2d225a277 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/appearance.js @@ -489,6 +489,9 @@ changes.inputLabels = node.inputLabels; node.inputLabels = newValue; changed = true; + if (node.type === "subflow") { + node.in[0].dirty = true + } } hasNonBlankLabel = false; newValue = new Array(node.outputs); 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 453a545ad..4aa9bc128 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 @@ -123,6 +123,7 @@ RED.view = (function() { let suggestedLinks = []; let suggestedJunctions = []; + let forceFullRedraw = false // Note: these are the permitted status colour aliases. The actual RGB values // are set in the CSS - flow.scss/colors.scss const status_colours = { @@ -4018,6 +4019,7 @@ RED.view = (function() { if (spacebarPressed) { return } + clearSuggestedFlow(); RED.contextMenu.hide(); evt = evt || d3.event; if (evt === 1) { @@ -4441,9 +4443,9 @@ RED.view = (function() { return tooltip; } - function portMouseOver(port,d,portType,portIndex) { + function portMouseOver(port,d,portType,portIndex, event) { if (mouse_mode === RED.state.SELECTING_NODE) { - d3.event.stopPropagation(); + (d3.event || event).stopPropagation(); return; } if (spacebarPressed) { @@ -4485,9 +4487,9 @@ RED.view = (function() { } port.classed("red-ui-flow-port-hovered",active); } - function portMouseOut(port,d,portType,portIndex) { + function portMouseOut(port,d,portType,portIndex, event) { if (mouse_mode === RED.state.SELECTING_NODE) { - d3.event.stopPropagation(); + (d3.event || event).stopPropagation(); return; } clearTimeout(portLabelHoverTimeout); @@ -4608,6 +4610,7 @@ RED.view = (function() { if (spacebarPressed) { return } + clearSuggestedFlow() focusView(); RED.contextMenu.hide(); if (d3.event.button === 1) { @@ -5239,6 +5242,186 @@ RED.view = (function() { } } + function buildSubflowPort (d) { + const NODE_TYPE = d.direction === "in" ? PORT_TYPE_INPUT : PORT_TYPE_OUTPUT; + // PORT_TYPE is the 'opposite' of NODE_TYPE + const PORT_TYPE = NODE_TYPE === PORT_TYPE_INPUT ? PORT_TYPE_OUTPUT : PORT_TYPE_INPUT; + var node = d3.select(this); + var nodeContents = document.createDocumentFragment(); + + d.h = 40; + d.resize = true; + d.dirty = true; + + var mainRect = document.createElementNS("http://www.w3.org/2000/svg","rect"); + mainRect.__data__ = d; + mainRect.setAttribute("class", "red-ui-flow-subflow-port"); + mainRect.setAttribute("rx", 8); + mainRect.setAttribute("ry", 8); + mainRect.setAttribute("width", 40); + mainRect.setAttribute("height", 40); + node[0][0].__mainRect__ = mainRect; + d3.select(mainRect) + .on("mouseup",nodeMouseUp) + .on("mousedown",nodeMouseDown) + .on("touchstart",nodeTouchStart) + .on("touchend",nodeTouchEnd) + nodeContents.appendChild(mainRect); + + const port_label_group = document.createElementNS("http://www.w3.org/2000/svg","g"); + port_label_group.setAttribute("x",0); + port_label_group.setAttribute("y",0); + node[0][0].__portLabelGroup__ = port_label_group; + + const port_label = document.createElementNS("http://www.w3.org/2000/svg","text"); + port_label.setAttribute("class","red-ui-flow-port-label"); + port_label.style["font-size"] = "10px"; + port_label.textContent = NODE_TYPE === PORT_TYPE_INPUT? "input" : "output"; + port_label_group.appendChild(port_label); + node[0][0].__portLabel__ = port_label; + + if (NODE_TYPE === PORT_TYPE_OUTPUT) { + const port_number = document.createElementNS("http://www.w3.org/2000/svg","text"); + port_number.setAttribute("class","red-ui-flow-port-label red-ui-flow-port-index"); + port_number.setAttribute("x",0); + port_number.setAttribute("y",0); + port_number.textContent = d.i+1; + port_label_group.appendChild(port_number); + node[0][0].__portNumber__ = port_number; + } + + const port_border = document.createElementNS("http://www.w3.org/2000/svg","path"); + port_border.setAttribute("d","M 40 1 l 0 38") + port_border.setAttribute("class", "red-ui-flow-node-icon-shade-border") + port_label_group.appendChild(port_border); + node[0][0].__portBorder__ = port_border; + + nodeContents.appendChild(port_label_group); + + var text = document.createElementNS("http://www.w3.org/2000/svg","g"); + text.setAttribute("class","red-ui-flow-port-label"); + text.setAttribute("transform","translate(38,0)"); + text.setAttribute('style', 'fill : #888'); // hard coded here! + node[0][0].__textGroup__ = text; + nodeContents.append(text); + + var portEl = document.createElementNS("http://www.w3.org/2000/svg","g"); + portEl.setAttribute('transform','translate(-5,15)') + + var port = document.createElementNS("http://www.w3.org/2000/svg","rect"); + port.setAttribute("class","red-ui-flow-port"); + port.setAttribute("rx",3); + port.setAttribute("ry",3); + port.setAttribute("width",10); + port.setAttribute("height",10); + portEl.appendChild(port); + port.__data__ = d; + + d3.select(port) + .on("mousedown", function(d,i){portMouseDown(d,PORT_TYPE,0);} ) + .on("touchstart", function(d,i){portMouseDown(d,PORT_TYPE,0);d3.event.preventDefault();} ) + .on("mouseup", function(d,i){portMouseUp(d,PORT_TYPE,0);}) + .on("touchend",function(d,i){portMouseUp(d,PORT_TYPE,0);d3.event.preventDefault();} ) + .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE,0);}) + .on("mouseout",function(d){portMouseOut(d3.select(this),d,PORT_TYPE,0);}); + + node[0][0].__port__ = portEl + nodeContents.appendChild(portEl); + node[0][0].appendChild(nodeContents); + } + function updateSubflowPort (d) { + if (d.dirty) { + const port_height = 40; + const NODE_TYPE = d.direction === "in" ? PORT_TYPE_INPUT : PORT_TYPE_OUTPUT; + // PORT_TYPE is the 'opposite' of NODE_TYPE + const PORT_TYPE = NODE_TYPE === PORT_TYPE_INPUT ? PORT_TYPE_OUTPUT : PORT_TYPE_INPUT; + + var label = getPortLabel(activeSubflow, NODE_TYPE, d.i) || ""; + var hideLabel = (label.length < 1) + var labelParts; + if (d.resize || this.__hideLabel__ !== hideLabel || this.__label__ !== label) { + labelParts = getLabelParts(label, "red-ui-flow-node-label"); + if (labelParts.lines.length !== this.__labelLineCount__ || this.__label__ !== label) { + d.resize = true; + } + this.__label__ = label; + this.__labelLineCount__ = labelParts.lines.length; + + if (hideLabel) { + d.h = Math.max(port_height,(d.outputs || 0) * 15); + } else { + d.h = Math.max(6+24*labelParts.lines.length,(d.outputs || 0) * 15, port_height); + } + this.__hideLabel__ = hideLabel; + } + + if (d.resize) { + var ow = d.w; + if (hideLabel) { + d.w = port_height; + } else { + d.w = Math.max(port_height,20*(Math.ceil((labelParts.width+50+7)/20)) ); + } + if (ow !== undefined) { + d.x += (d.w-ow)/2; + } + d.resize = false; + } + + this.setAttribute("transform", "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")"); + // This might be the first redraw after a node has been click-dragged to start a move. + // So its selected state might have changed since the last redraw. + this.classList.toggle("red-ui-flow-node-selected", !!d.selected ) + if (mouse_mode != RED.state.MOVING_ACTIVE) { + this.classList.toggle("red-ui-flow-node-disabled", d.d === true); + this.__mainRect__.setAttribute("width", d.w) + this.__mainRect__.setAttribute("height", d.h) + this.__mainRect__.classList.toggle("red-ui-flow-node-highlighted",!!d.highlighted ); + + if (labelParts) { + // The label has changed + var sa = labelParts.lines; + var sn = labelParts.lines.length; + var textLines = this.__textGroup__.childNodes; + while(textLines.length > sn) { + textLines[textLines.length-1].remove(); + } + for (var i=0; i sn) { - textLines[textLines.length-1].remove(); - } - for (var i=0; i