',{class:"red-ui-search-result-description"}).appendTo(div);
- var label = object.label;
- object.index += "|"+label.toLowerCase();
+ var label = nodeItem.label;
+ nodeItem.index += "|"+label.toLowerCase();
$('
',{class:"red-ui-search-result-node-label"}).text(label).appendTo(contentDiv);
+ nodeItem.element = container;
+
div.on("click", function(evt) {
evt.preventDefault();
- confirm(object);
+ confirm(nodeItem);
});
+ div.on('mouseenter', function() {
+ if (suggestCallback) {
+ if (nodeItem.nodes) {
+ // This is a multi-node suggestion
+ suggestCallback({
+ nodes: nodeItem.nodes
+ });
+ } else if (nodeItem.type) {
+ // Single node suggestion
+ suggestCallback({
+ nodes: [{
+ x: 0,
+ y: 0,
+ type: nodeItem.type
+ }]
+ });
+ }
+ }
+ })
+ div.on('mouseleave', function() {
+ if (suggestCallback) {
+ suggestCallback(null);
+ }
+ })
+
},
scrollOnAdd: false
});
@@ -221,10 +264,10 @@ RED.typeSearch = (function() {
}
function confirm(def) {
hide();
- if (!/^_action_:/.test(def.type)) {
+ if (!def.nodes && !/^_action_:/.test(def.type)) {
typesUsed[def.type] = Date.now();
}
- addCallback(def.type);
+ addCallback(def);
}
function handleMouseActivity(evt) {
@@ -274,6 +317,7 @@ RED.typeSearch = (function() {
addCallback = opts.add;
cancelCallback = opts.cancel;
moveCallback = opts.move;
+ suggestCallback = opts.suggest;
RED.events.emit("type-search:open");
//shade.show();
if ($("#red-ui-main-container").height() - opts.y - 195 < 0) {
@@ -356,11 +400,11 @@ RED.typeSearch = (function() {
(!filter.output || def.outputs > 0)
}
function refreshTypeList(opts) {
- var i;
+ let i;
searchResults.editableList('empty');
searchInput.searchBox('value','').focus();
selected = -1;
- var common = [
+ const common = [
'inject','debug','function','change','switch','junction'
].filter(function(t) { return applyFilter(opts.filter,t,RED.nodes.getType(t)); });
@@ -371,7 +415,7 @@ RED.typeSearch = (function() {
// common.push('_action_:core:split-wire-with-link-nodes')
// }
- var recentlyUsed = Object.keys(typesUsed);
+ let recentlyUsed = Object.keys(typesUsed);
recentlyUsed.sort(function(a,b) {
return typesUsed[b]-typesUsed[a];
});
@@ -379,9 +423,10 @@ RED.typeSearch = (function() {
return applyFilter(opts.filter,t,RED.nodes.getType(t)) && common.indexOf(t) === -1;
});
- var items = [];
+ const items = [];
+
RED.nodes.registry.getNodeTypes().forEach(function(t) {
- var def = RED.nodes.getType(t);
+ const def = RED.nodes.getType(t);
if (def.set?.enabled !== false && def.category !== 'config' && t !== 'unknown' && t !== 'tab') {
items.push({type:t,def: def, label:getTypeLabel(t,def)});
}
@@ -389,18 +434,46 @@ RED.typeSearch = (function() {
items.push({ type: 'junction', def: { inputs:1, outputs: 1, label: 'junction', type: 'junction'}, label: 'junction' })
items.sort(sortTypeLabels);
- var commonCount = 0;
- var item;
- var index = 0;
+ let index = 0;
+
+ // const suggestionItem = {
+ // suggestionPlaceholder: true,
+ // label: 'loading suggestions...',
+ // separator: true,
+ // i: index++
+ // }
+ // searchResults.editableList('addItem', suggestionItem);
+ // setTimeout(function() {
+ // searchResults.editableList('removeItem', suggestionItem);
+
+ // const suggestedItem = {
+ // suggestion: true,
+ // label: 'Change/Debug Combo',
+ // separator: true,
+ // i: suggestionItem.i,
+ // nodes: [
+ // { id: 'suggestion-1', type: 'change', x: 0, y: 0, wires:[['suggestion-2']] },
+ // { id: 'suggestion-2', type: 'function', outputs: 3, x: 200, y: 0, wires:[['suggestion-3'],['suggestion-4'],['suggestion-6']] },
+ // { id: 'suggestion-3', _g: 'suggestion-group-1', type: 'debug', x: 375, y: -40 },
+ // { id: 'suggestion-4', _g: 'suggestion-group-1', type: 'debug', x: 375, y: 0 },
+ // { id: 'suggestion-5', _g: 'suggestion-group-1', type: 'debug', x: 410, y: 40 },
+ // { id: 'suggestion-6', type: 'junction', wires: [['suggestion-5']], x:325, y:40 }
+ // ]
+ // }
+ // searchResults.editableList('addItem', suggestedItem);
+ // }, 1000)
+
for(i=0;i
0 || drag_line.virtualLink) ) {
+ src = drag_line.node;
+ src_port = drag_line.port;
+ dst = targetNode;
+ } else if (drag_line.portType === PORT_TYPE_INPUT && (targetNode.outputs > 0 || drag_line.virtualLink)) {
+ src = targetNode;
+ dst = drag_line.node;
+ src_port = 0;
+ }
+ if (src && dst) {
+ var link = {source: src, sourcePort:src_port, target: dst};
+ RED.nodes.addLink(link);
+ const historyEvent = RED.history.peek()
+ if (historyEvent.t === 'add') {
+ historyEvent.links = historyEvent.links || []
+ historyEvent.links.push(link)
+ } else {
+ // TODO: importNodes *can* generate a multi history event
+ // but we don't currently support that
+ }
+ }
+ if (quickAddLink.el) {
+ quickAddLink.el.remove();
+ }
+ quickAddLink = null;
+ }
+ updateActiveNodes();
+ updateSelection();
+ redraw();
+
+ return
+ } else {
+ type = type.type
+ }
+ }
var nn;
var historyEvent;
if (/^_action_:/.test(type)) {
@@ -1479,7 +1556,7 @@ RED.view = (function() {
if (nn.type === 'junction') {
nn = RED.nodes.addJunction(nn);
} else {
- nn = RED.nodes.add(nn);
+ nn = RED.nodes.add(nn, { source: 'typeSearch' });
}
if (quickAddLink) {
var drag_line = quickAddLink;
@@ -1662,6 +1739,22 @@ RED.view = (function() {
quickAddActive = false;
ghostNode.remove();
}
+ },
+ suggest: function (suggestion) {
+ if (suggestion?.nodes?.length > 0) {
+ // Reposition the suggestion relative to the existing ghost node position
+ const deltaX = suggestion.nodes[0].x - point[0]
+ const deltaY = suggestion.nodes[0].y - point[1]
+ suggestion.nodes.forEach(node => {
+ if (Object.hasOwn(node, 'x')) {
+ node.x = node.x - deltaX
+ }
+ if (Object.hasOwn(node, 'y')) {
+ node.y = node.y - deltaY
+ }
+ })
+ }
+ setSuggestedFlow(suggestion);
}
});
@@ -4576,20 +4669,28 @@ RED.view = (function() {
nodeLayer.selectAll(".red-ui-flow-subflow-port-input").remove();
nodeLayer.selectAll(".red-ui-flow-subflow-port-status").remove();
}
-
- var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(activeNodes,function(d){return d.id});
+ let nodesToDraw = activeNodes;
+ if (suggestedNodes.length > 0) {
+ nodesToDraw = [...activeNodes, ...suggestedNodes]
+ }
+ var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(nodesToDraw,function(d){return d.id});
node.exit().each(function(d,i) {
- RED.hooks.trigger("viewRemoveNode",{node:d,el:this})
+ if (!d.__ghost) {
+ RED.hooks.trigger("viewRemoveNode",{node:d,el:this})
+ }
}).remove();
var nodeEnter = node.enter().insert("svg:g")
.attr("class", "red-ui-flow-node red-ui-flow-node-group")
- .classed("red-ui-flow-subflow", activeSubflow != null);
+ .classed("red-ui-flow-subflow", activeSubflow != null)
nodeEnter.each(function(d,i) {
this.__outputs__ = [];
this.__inputs__ = [];
var node = d3.select(this);
+ if (d.__ghost) {
+ node.classed("red-ui-flow-node-ghost",true);
+ }
var nodeContents = document.createDocumentFragment();
var isLink = (d.type === "link in" || d.type === "link out")
var hideLabel = d.hasOwnProperty('l')?!d.l : isLink;
@@ -4624,19 +4725,21 @@ RED.view = (function() {
bgButton.setAttribute("width",16);
bgButton.setAttribute("height",node_height-12);
bgButton.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
- d3.select(bgButton)
- .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
- .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
- .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}})
- .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) {
- var op = 1;
- if (d._def.button.toggle) {
- op = d[d._def.button.toggle]?1:0.2;
- }
- d3.select(this).attr("fill-opacity",op);
- }})
- .on("click",nodeButtonClicked)
- .on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();})
+ if (!d.__ghost) {
+ d3.select(bgButton)
+ .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}})
+ .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}})
+ .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}})
+ .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) {
+ var op = 1;
+ if (d._def.button.toggle) {
+ op = d[d._def.button.toggle]?1:0.2;
+ }
+ d3.select(this).attr("fill-opacity",op);
+ }})
+ .on("click",nodeButtonClicked)
+ .on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();})
+ }
buttonGroup.appendChild(bgButton);
node[0][0].__buttonGroupButton__ = bgButton;
@@ -4651,13 +4754,15 @@ RED.view = (function() {
mainRect.setAttribute("ry", 5);
mainRect.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def));
node[0][0].__mainRect__ = mainRect;
- d3.select(mainRect)
- .on("mouseup",nodeMouseUp)
- .on("mousedown",nodeMouseDown)
- .on("touchstart",nodeTouchStart)
- .on("touchend",nodeTouchEnd)
- .on("mouseover",nodeMouseOver)
- .on("mouseout",nodeMouseOut);
+ if (!d.__ghost) {
+ d3.select(mainRect)
+ .on("mouseup",nodeMouseUp)
+ .on("mousedown",nodeMouseDown)
+ .on("touchstart",nodeTouchStart)
+ .on("touchend",nodeTouchEnd)
+ .on("mouseover",nodeMouseOver)
+ .on("mouseout",nodeMouseOut);
+ }
nodeContents.appendChild(mainRect);
//node.append("rect").attr("class", "node-gradient-top").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-top)").style("pointer-events","none");
//node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none");
@@ -4739,7 +4844,10 @@ RED.view = (function() {
node[0][0].appendChild(nodeContents);
- RED.hooks.trigger("viewAddNode",{node:d,el:this})
+ if (!d.__ghost) {
+ // Do not trigger hooks for ghost nodes
+ RED.hooks.trigger("viewAddNode",{node:d,el:this})
+ }
});
var nodesReordered = false;
@@ -4862,13 +4970,15 @@ RED.view = (function() {
var inputPorts = thisNode.selectAll(".red-ui-flow-port-input");
if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) {
inputPorts.each(function(d,i) {
- RED.hooks.trigger("viewRemovePort",{
- node:d,
- el:self,
- port:d3.select(this)[0][0],
- portType: "input",
- portIndex: 0
- })
+ if (!d.__ghost) {
+ RED.hooks.trigger("viewRemovePort",{
+ node:d,
+ el:self,
+ port:d3.select(this)[0][0],
+ portType: "input",
+ portIndex: 0
+ })
+ }
}).remove();
} else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) {
var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input");
@@ -4886,13 +4996,15 @@ RED.view = (function() {
inputGroupPorts[0][0].__data__ = this.__data__;
inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT;
inputGroupPorts[0][0].__portIndex__ = 0;
- inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
- .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();})
- .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
- .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
- .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
- .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
- RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
+ if (!d.__ghost) {
+ inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);})
+ .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();})
+ .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} )
+ .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} )
+ .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);})
+ .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);});
+ RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0})
+ }
}
var numOutputs = d.outputs;
if (isLink && d.type === "link out") {
@@ -4907,13 +5019,15 @@ RED.view = (function() {
// Remove extra ports
while (this.__outputs__.length > numOutputs) {
var port = this.__outputs__.pop();
- RED.hooks.trigger("viewRemovePort",{
- node:d,
- el:this,
- port:port,
- portType: "output",
- portIndex: this.__outputs__.length
- })
+ if (!d.__ghost) {
+ RED.hooks.trigger("viewRemovePort",{
+ node:d,
+ el:this,
+ port:port,
+ portType: "output",
+ portIndex: this.__outputs__.length
+ })
+ }
port.remove();
}
for(var portIndex = 0; portIndex < numOutputs; portIndex++ ) {
@@ -4941,16 +5055,20 @@ RED.view = (function() {
portPort.__data__ = this.__data__;
portPort.__portType__ = PORT_TYPE_OUTPUT;
portPort.__portIndex__ = portIndex;
- portPort.addEventListener("mousedown", portMouseDownProxy);
- portPort.addEventListener("touchstart", portTouchStartProxy);
- portPort.addEventListener("mouseup", portMouseUpProxy);
- portPort.addEventListener("touchend", portTouchEndProxy);
- portPort.addEventListener("mouseover", portMouseOverProxy);
- portPort.addEventListener("mouseout", portMouseOutProxy);
+ if (!d.__ghost) {
+ portPort.addEventListener("mousedown", portMouseDownProxy);
+ portPort.addEventListener("touchstart", portTouchStartProxy);
+ portPort.addEventListener("mouseup", portMouseUpProxy);
+ portPort.addEventListener("touchend", portTouchEndProxy);
+ portPort.addEventListener("mouseover", portMouseOverProxy);
+ portPort.addEventListener("mouseout", portMouseOutProxy);
+ }
this.appendChild(portGroup);
this.__outputs__.push(portGroup);
- RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
+ if (!d.__ghost) {
+ RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex})
+ }
} else {
portGroup = this.__outputs__[portIndex];
}
@@ -5067,8 +5185,10 @@ RED.view = (function() {
}
}
}
-
- RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
+ if (!d.__ghost) {
+ // Only trigger redraw hooks for non-ghost nodes
+ RED.hooks.trigger("viewRedrawNode",{node:d,el:this})
+ }
});
if (nodesReordered) {
@@ -5077,13 +5197,20 @@ RED.view = (function() {
})
}
+ let junctionsToDraw = activeJunctions;
+ if (suggestedJunctions.length > 0) {
+ junctionsToDraw = [...activeJunctions, ...suggestedJunctions]
+ }
var junction = junctionLayer.selectAll(".red-ui-flow-junction").data(
- activeJunctions,
+ junctionsToDraw,
d => d.id
)
var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction")
junctionEnter.each(function(d,i) {
var junction = d3.select(this);
+ if (d.__ghost) {
+ junction.classed("red-ui-flow-junction-ghost",true);
+ }
var contents = document.createDocumentFragment();
// d.added = true;
var junctionBack = document.createElementNS("http://www.w3.org/2000/svg","rect");
@@ -5177,8 +5304,12 @@ RED.view = (function() {
})
+ let linksToDraw = activeLinks
+ if (suggestedLinks.length > 0) {
+ linksToDraw = [...activeLinks, ...suggestedLinks]
+ }
var link = linkLayer.selectAll(".red-ui-flow-link").data(
- activeLinks,
+ linksToDraw,
function(d) {
return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i;
}
@@ -5189,44 +5320,50 @@ RED.view = (function() {
var l = d3.select(this);
var pathContents = document.createDocumentFragment();
+ if (d.__ghost) {
+ l.classed("red-ui-flow-link-ghost",true);
+ }
+
d.added = true;
var pathBack = document.createElementNS("http://www.w3.org/2000/svg","path");
pathBack.__data__ = d;
pathBack.setAttribute("class","red-ui-flow-link-background red-ui-flow-link-path"+(d.link?" red-ui-flow-link-link":""));
this.__pathBack__ = pathBack;
pathContents.appendChild(pathBack);
- d3.select(pathBack)
- .on("mousedown",linkMouseDown)
- .on("touchstart",linkTouchStart)
- .on("mousemove", function(d) {
- if (mouse_mode === RED.state.SLICING) {
+ if (!d.__ghost) {
+ d3.select(pathBack)
+ .on("mousedown",linkMouseDown)
+ .on("touchstart",linkTouchStart)
+ .on("mousemove", function(d) {
+ if (mouse_mode === RED.state.SLICING) {
- selectedLinks.add(d)
- l.classed("red-ui-flow-link-splice",true)
- redraw()
- } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) {
- if (!l.classed("red-ui-flow-link-splice")) {
- // Find intersection point
- var lineLength = pathLine.getTotalLength();
- var pos;
- var delta = Infinity;
- for (var i = 0; i < lineLength; i++) {
- var linePos = pathLine.getPointAtLength(i);
- var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
- var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
- var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
- if (posDelta < delta) {
- pos = linePos
- delta = posDelta
- }
- }
- d._sliceLocation = pos
selectedLinks.add(d)
l.classed("red-ui-flow-link-splice",true)
redraw()
+ } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) {
+ if (!l.classed("red-ui-flow-link-splice")) {
+ // Find intersection point
+ var lineLength = pathLine.getTotalLength();
+ var pos;
+ var delta = Infinity;
+ for (var i = 0; i < lineLength; i++) {
+ var linePos = pathLine.getPointAtLength(i);
+ var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor))
+ var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor))
+ var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY
+ if (posDelta < delta) {
+ pos = linePos
+ delta = posDelta
+ }
+ }
+ d._sliceLocation = pos
+ selectedLinks.add(d)
+ l.classed("red-ui-flow-link-splice",true)
+ redraw()
+ }
}
- }
- })
+ })
+ }
var pathOutline = document.createElementNS("http://www.w3.org/2000/svg","path");
pathOutline.__data__ = d;
@@ -5688,16 +5825,21 @@ RED.view = (function() {
* - generateIds - whether to automatically generate new ids for all imported nodes
* - generateDefaultNames - whether to automatically update any nodes with clashing
* default names
+ * - notify - whether to show a notification if the import was successful
*/
function importNodes(newNodesObj,options) {
options = options || {
addFlow: false,
touchImport: false,
generateIds: false,
- generateDefaultNames: false
+ generateDefaultNames: false,
+ notify: true,
+ applyNodeDefaults: false
}
- var addNewFlow = options.addFlow
- var touchImport = options.touchImport;
+ const addNewFlow = options.addFlow
+ const touchImport = options.touchImport;
+ const showNotification = options.notify ?? true
+ const applyNodeDefaults = options.applyNodeDefaults ?? false
if (mouse_mode === RED.state.SELECTING_NODE) {
return;
@@ -5781,7 +5923,8 @@ RED.view = (function() {
addFlow: addNewFlow,
importMap: options.importMap,
markChanged: true,
- modules: modules
+ modules: modules,
+ applyNodeDefaults: applyNodeDefaults
});
if (importResult) {
var new_nodes = importResult.nodes;
@@ -5792,6 +5935,7 @@ RED.view = (function() {
var new_subflows = importResult.subflows;
var removedNodes = importResult.removedNodes;
var new_default_workspace = importResult.missingWorkspace;
+ const nodeMap = importResult.nodeMap;
if (addNewFlow && new_default_workspace) {
RED.workspaces.show(new_default_workspace.id);
}
@@ -5813,16 +5957,18 @@ RED.view = (function() {
var dx = mouse_position[0];
var dy = mouse_position[1];
- if (movingSet.length() > 0) {
- var root_node = movingSet.get(0).n;
- dx = root_node.x;
- dy = root_node.y;
+ if (!touchImport) {
+ if (movingSet.length() > 0) {
+ const root_node = movingSet.get(0).n;
+ dx = root_node.x;
+ dy = root_node.y;
+ }
}
var minX = 0;
var minY = 0;
var i;
- var node,group;
+ var node;
var l =movingSet.length();
for (i=0;i 0) {
+ counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
+ }
+ if (newNodeCount > 0) {
+ counts.push(RED._("clipboard.node",{count:newNodeCount}));
+ }
+ if (newGroupCount > 0) {
+ counts.push(RED._("clipboard.group",{count:newGroupCount}));
+ }
+ if (newConfigNodeCount > 0) {
+ counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
+ }
+ if (new_subflows.length > 0) {
+ counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
+ }
+ if (removedNodes && removedNodes.length > 0) {
+ counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
+ }
+ if (counts.length > 0) {
+ var countList = "";
+ RED.notify(""+RED._("clipboard.nodesImported")+"
"+countList,{id:"clipboard"});
}
- })
- var newGroupCount = new_groups.length;
- var newJunctionCount = new_junctions.length;
- if (new_workspaces.length > 0) {
- counts.push(RED._("clipboard.flow",{count:new_workspaces.length}));
}
- if (newNodeCount > 0) {
- counts.push(RED._("clipboard.node",{count:newNodeCount}));
+ return {
+ nodeMap
}
- if (newGroupCount > 0) {
- counts.push(RED._("clipboard.group",{count:newGroupCount}));
- }
- if (newConfigNodeCount > 0) {
- counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount}));
- }
- if (new_subflows.length > 0) {
- counts.push(RED._("clipboard.subflow",{count:new_subflows.length}));
- }
- if (removedNodes && removedNodes.length > 0) {
- counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length}));
- }
- if (counts.length > 0) {
- var countList = "";
- RED.notify(""+RED._("clipboard.nodesImported")+"
"+countList,{id:"clipboard"});
- }
-
+ }
+ return {
+ nodeMap: {}
}
} catch(error) {
if (error.code === "import_conflict") {
@@ -6307,6 +6459,157 @@ RED.view = (function() {
node.highlighted = true;
RED.view.redraw();
}
+
+ /**
+ * Add a suggested flow to the workspace.
+ *
+ * This appears as a ghost set of nodes.
+ *
+ * {
+ * "nodes": [
+ * {
+ * type: "node-type",
+ * x: 0,
+ * y: 0,
+ * }
+ * ]
+ * }
+ * If `nodes` is a single node without an id property, it will be generated
+ * using its default properties.
+ *
+ * If `nodes` has multiple, they must all have ids and will be assumed to be 'importable'.
+ * In other words, a piece of valid flow json.
+ *
+ * Limitations:
+ * - does not support groups, subflows or whole tabs
+ * - does not support config nodes
+ *
+ * To clear the current suggestion, pass in `null`.
+ *
+ *
+ * @param {Object} suggestion - The suggestion object
+ */
+ function setSuggestedFlow (suggestion) {
+ if (!currentSuggestion && !suggestion) {
+ // Avoid unnecessary redraws
+ return
+ }
+ // Clear up any existing suggestion state
+ clearSuggestedFlow()
+ currentSuggestion = suggestion
+ if (suggestion?.nodes?.length > 0) {
+ const nodeMap = {}
+ const links = []
+ suggestion.nodes.forEach(nodeConfig => {
+ if (!nodeConfig.type || nodeConfig.type === 'group' || nodeConfig.type === 'subflow' || nodeConfig.type === 'tab') {
+ // A node type we don't support previewing
+ return
+ }
+
+ let node
+
+ if (nodeConfig.type === 'junction') {
+ node = {
+ _def: {defaults:{}},
+ type: 'junction',
+ z: RED.workspaces.active(),
+ id: RED.nodes.id(),
+ x: nodeConfig.x,
+ y: nodeConfig.y,
+ w: 0, h: 0,
+ outputs: 1,
+ inputs: 1,
+ dirty: true,
+ moved: true
+ }
+ } else {
+ const def = RED.nodes.getType(nodeConfig.type)
+ if (!def || def.category === 'config') {
+ // Unknown node or config node
+ // TODO: unknown node types could happen...
+ return
+ }
+ const result = createNode(nodeConfig.type, nodeConfig.x, nodeConfig.y)
+ if (!result) {
+ return
+ }
+ node = result.node
+ node["_"] = node._def._;
+
+ for (d in node._def.defaults) {
+ if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'name') {
+ if (nodeConfig[d] !== undefined) {
+ node[d] = nodeConfig[d]
+ } else if (node._def.defaults[d].value) {
+ node[d] = JSON.parse(JSON.stringify(node._def.defaults[d].value))
+ }
+ }
+ }
+ suggestedNodes.push(node)
+ }
+ if (node) {
+ node.id = nodeConfig.id || node.id
+ node.__ghost = true;
+ node.dirty = true;
+ nodeMap[node.id] = node
+
+ if (nodeConfig.wires) {
+ nodeConfig.wires.forEach((wire, i) => {
+ if (wire.length > 0) {
+ wire.forEach(targetId => {
+ links.push({
+ sourceId: nodeConfig.id || node.id,
+ sourcePort: i,
+ targetId: targetId,
+ targetPort: 0,
+ __ghost: true
+ })
+ })
+ }
+ })
+ }
+ }
+ })
+ links.forEach(link => {
+ const sourceNode = nodeMap[link.sourceId]
+ const targetNode = nodeMap[link.targetId]
+ if (sourceNode && targetNode) {
+ link.source = sourceNode
+ link.target = targetNode
+ suggestedLinks.push(link)
+ }
+ })
+ }
+ if (ghostNode) {
+ if (suggestedNodes.length > 0) {
+ ghostNode.style('opacity', 0)
+ } else {
+ ghostNode.style('opacity', 1)
+ }
+ }
+ redraw();
+ }
+
+ function clearSuggestedFlow () {
+ currentSuggestion = null
+ suggestedNodes = []
+ suggestedLinks = []
+ }
+
+ function applySuggestedFlow () {
+ if (currentSuggestion && currentSuggestion.nodes) {
+ const nodesToImport = currentSuggestion.nodes
+ setSuggestedFlow(null)
+ return importNodes(nodesToImport, {
+ generateIds: true,
+ touchImport: true,
+ notify: false,
+ // Ensure the node gets all of its defaults applied
+ applyNodeDefaults: true
+ })
+ }
+ }
+
return {
init: init,
state:function(state) {
@@ -6567,6 +6870,8 @@ RED.view = (function() {
width: space_width,
height: space_height
};
- }
+ },
+ setSuggestedFlow,
+ applySuggestedFlow
};
})();
diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss
index ad055b97c..944536845 100644
--- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss
+++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss
@@ -161,7 +161,15 @@ svg:not(.red-ui-workspace-lasso-active) {
fill: var(--red-ui-group-default-label-color);
}
+.red-ui-flow-node-ghost {
+ opacity: 0.6;
+ rect.red-ui-flow-node {
+ stroke: var(--red-ui-node-border-placeholder);
+ stroke-dasharray:10,4;
+ stroke-width: 2;
+ }
+}
.red-ui-flow-node-unknown {
stroke-dasharray:10,4;
@@ -401,6 +409,13 @@ g.red-ui-flow-node-selected {
g.red-ui-flow-link-selected path.red-ui-flow-link-line {
stroke: var(--red-ui-node-selected-color);
}
+
+g.red-ui-flow-link-ghost path.red-ui-flow-link-line {
+ stroke: var(--red-ui-node-border-placeholder);
+ stroke-width: 2;
+ stroke-dasharray: 10, 4;
+}
+
g.red-ui-flow-link-unknown path.red-ui-flow-link-line {
stroke: var(--red-ui-link-unknown-color);
stroke-width: 2;