mirror of
https://github.com/node-red/node-red.git
synced 2025-12-27 07:31:07 +01:00
Merge pull request #5180 from node-red/suggest-api-improvements
setSuggestedFlow api improvements
This commit is contained in:
@@ -573,6 +573,7 @@
|
||||
"filter": "filter nodes",
|
||||
"search": "search modules",
|
||||
"addCategory": "Add new...",
|
||||
"loadingSuggestions": "Loading suggestions...",
|
||||
"label": {
|
||||
"subflows": "subflows",
|
||||
"network": "network",
|
||||
|
||||
@@ -35,6 +35,7 @@ RED.keyboard = (function() {
|
||||
"backspace": 8,
|
||||
"delete": 46,
|
||||
"space": 32,
|
||||
"tab": 9,
|
||||
";":186,
|
||||
"=":187,
|
||||
"+":187, // <- QWERTY specific
|
||||
|
||||
@@ -182,7 +182,6 @@ RED.typeSearch = (function() {
|
||||
addItem: function(container, i, nodeItem) {
|
||||
// nodeItem can take multiple forms
|
||||
// - A node type: {type: "inject", def: RED.nodes.getType("inject"), label: "Inject"}
|
||||
// - A flow suggestion: { suggestion: true, nodes: [] }
|
||||
// - A placeholder suggestion: { suggestionPlaceholder: true, label: 'loading suggestions...' }
|
||||
|
||||
let nodeDef = nodeItem.def;
|
||||
@@ -262,7 +261,12 @@ RED.typeSearch = (function() {
|
||||
|
||||
}
|
||||
|
||||
let activeSuggestion
|
||||
function updateSuggestion(nodeItem) {
|
||||
if (nodeItem === activeSuggestion) {
|
||||
return
|
||||
}
|
||||
activeSuggestion = nodeItem
|
||||
if (suggestCallback) {
|
||||
if (!nodeItem) {
|
||||
suggestCallback(null);
|
||||
@@ -319,6 +323,7 @@ RED.typeSearch = (function() {
|
||||
}
|
||||
visible = true;
|
||||
} else {
|
||||
updateSuggestion(null)
|
||||
dialog.hide();
|
||||
searchResultsDiv.hide();
|
||||
}
|
||||
@@ -359,9 +364,7 @@ RED.typeSearch = (function() {
|
||||
},200);
|
||||
}
|
||||
function hide(fast) {
|
||||
if (suggestCallback) {
|
||||
suggestCallback(null);
|
||||
}
|
||||
updateSuggestion(null)
|
||||
if (visible) {
|
||||
visible = false;
|
||||
if (dialog !== null) {
|
||||
@@ -460,32 +463,37 @@ RED.typeSearch = (function() {
|
||||
|
||||
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)
|
||||
if (!opts.context?.virtualLink) {
|
||||
// Check for suggestion plugins
|
||||
const suggestionPlugins = RED.plugins.getPluginsByType('node-red-flow-suggestion-source');
|
||||
if (suggestionPlugins.length > 0) {
|
||||
const suggestionItem = {
|
||||
suggestionPlaceholder: true,
|
||||
label: RED._('palette.loadingSuggestions'),
|
||||
separator: true,
|
||||
i: index++
|
||||
}
|
||||
searchResults.editableList('addItem', suggestionItem);
|
||||
suggestionPlugins[0].getSuggestions(opts.context).then(function (suggestedFlows) {
|
||||
searchResults.editableList('removeItem', suggestionItem);
|
||||
if (!Array.isArray(suggestedFlows)) {
|
||||
suggestedFlows = [suggestedFlows];
|
||||
}
|
||||
suggestedFlows.forEach(function(suggestion, index) {
|
||||
const suggestedItem = {
|
||||
suggestion: true,
|
||||
separator: index === suggestedFlows.length - 1,
|
||||
i: suggestionItem.i,
|
||||
...suggestion
|
||||
}
|
||||
if (!suggestion.label && suggestion.nodes && suggestion.nodes.length === 1 && suggestion.nodes[0].type) {
|
||||
suggestedItem.label = getTypeLabel(suggestion.nodes[0].type, RED.nodes.getType(suggestion.nodes[0].type));
|
||||
}
|
||||
searchResults.editableList('addItem', suggestedItem);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for(i=0;i<common.length;i++) {
|
||||
let itemDef
|
||||
@@ -536,9 +544,10 @@ RED.typeSearch = (function() {
|
||||
}
|
||||
|
||||
return {
|
||||
show: show,
|
||||
show,
|
||||
refresh: refreshTypeList,
|
||||
hide: hide
|
||||
hide,
|
||||
isVisible: () => visible
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -1147,8 +1147,8 @@ RED.view.tools = (function() {
|
||||
t: 'multi',
|
||||
events: historyEvents
|
||||
})
|
||||
RED.nodes.dirty(true)
|
||||
}
|
||||
RED.nodes.dirty(true)
|
||||
RED.view.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1402,10 +1402,12 @@ RED.view = (function() {
|
||||
var lastAddedX;
|
||||
var lastAddedWidth;
|
||||
|
||||
const context = {}
|
||||
const context = {
|
||||
workspace: RED.workspaces.active()
|
||||
}
|
||||
|
||||
if (quickAddLink) {
|
||||
context.source = quickAddLink.node.id;
|
||||
context.source = quickAddLink.node;
|
||||
context.sourcePort = quickAddLink.port;
|
||||
context.sourcePortType = quickAddLink.portType;
|
||||
if (quickAddLink?.virtualLink) {
|
||||
@@ -1421,6 +1423,7 @@ RED.view = (function() {
|
||||
y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]),
|
||||
disableFocus: touchTrigger,
|
||||
filter: filter,
|
||||
context,
|
||||
move: function(dx,dy) {
|
||||
if (ghostNode) {
|
||||
var pos = d3.transform(ghostNode.attr("transform")).translate;
|
||||
@@ -1469,8 +1472,9 @@ RED.view = (function() {
|
||||
if (quickAddLink) {
|
||||
// Need to attach the link to the suggestion. This is assumed to be the first
|
||||
// node in the array - as that's the one we've focussed on.
|
||||
const targetNode = importResult.nodeMap[type.nodes[0].id]
|
||||
|
||||
// We need to map from the suggested node's id to the imported node's id,
|
||||
// and then get the proxy object for the node
|
||||
const targetNode = RED.nodes.node(importResult.nodeMap[type.nodes[0].id].id)
|
||||
const drag_line = quickAddLink;
|
||||
let src = null, dst, src_port;
|
||||
if (drag_line.portType === PORT_TYPE_OUTPUT && (targetNode.inputs > 0 || drag_line.virtualLink) ) {
|
||||
@@ -1743,15 +1747,11 @@ RED.view = (function() {
|
||||
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]
|
||||
const deltaX = (suggestion.nodes[0].x || 0) - point[0]
|
||||
const deltaY = (suggestion.nodes[0].y || 0) - 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
|
||||
}
|
||||
node.x = (node.x || 0) - deltaX
|
||||
node.y = (node.y || 0) - deltaY
|
||||
})
|
||||
}
|
||||
setSuggestedFlow(suggestion);
|
||||
@@ -4762,6 +4762,10 @@ RED.view = (function() {
|
||||
.on("touchend",nodeTouchEnd)
|
||||
.on("mouseover",nodeMouseOver)
|
||||
.on("mouseout",nodeMouseOut);
|
||||
} else if (d.__ghostClick) {
|
||||
d3.select(mainRect)
|
||||
.on("mousedown",d.__ghostClick)
|
||||
.on("touchstart",d.__ghostClick)
|
||||
}
|
||||
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");
|
||||
@@ -5004,6 +5008,10 @@ RED.view = (function() {
|
||||
.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})
|
||||
} else if (d.__ghostClick) {
|
||||
inputGroupPorts
|
||||
.on("mousedown",d.__ghostClick)
|
||||
.on("touchstart",d.__ghostClick)
|
||||
}
|
||||
}
|
||||
var numOutputs = d.outputs;
|
||||
@@ -5062,6 +5070,9 @@ RED.view = (function() {
|
||||
portPort.addEventListener("touchend", portTouchEndProxy);
|
||||
portPort.addEventListener("mouseover", portMouseOverProxy);
|
||||
portPort.addEventListener("mouseout", portMouseOutProxy);
|
||||
} else if (d.__ghostClick) {
|
||||
portPort.addEventListener("mousedown", d.__ghostClick)
|
||||
portPort.addEventListener("touchstart", d.__ghostClick)
|
||||
}
|
||||
|
||||
this.appendChild(portGroup);
|
||||
@@ -5363,6 +5374,10 @@ RED.view = (function() {
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (d.__ghostClick) {
|
||||
d3.select(pathBack)
|
||||
.on("mousedown",d.__ghostClick)
|
||||
.on("touchstart",d.__ghostClick)
|
||||
}
|
||||
|
||||
var pathOutline = document.createElementNS("http://www.w3.org/2000/svg","path");
|
||||
@@ -6379,10 +6394,10 @@ RED.view = (function() {
|
||||
nn.w = RED.view.node_width;
|
||||
nn.h = Math.max(RED.view.node_height, (nn.outputs || 0) * 15);
|
||||
nn.resize = true;
|
||||
if (x != null && typeof x == "number" && x >= 0) {
|
||||
if (x != null && typeof x == "number") {
|
||||
nn.x = x;
|
||||
}
|
||||
if (y != null && typeof y == "number" && y >= 0) {
|
||||
if (y != null && typeof y == "number") {
|
||||
nn.y = y;
|
||||
}
|
||||
var historyEvent = {
|
||||
@@ -6472,7 +6487,9 @@ RED.view = (function() {
|
||||
* x: 0,
|
||||
* y: 0,
|
||||
* }
|
||||
* ]
|
||||
* ],
|
||||
* "source": <sourceNode>,
|
||||
* "sourcePort": <sourcePort>,
|
||||
* }
|
||||
* If `nodes` is a single node without an id property, it will be generated
|
||||
* using its default properties.
|
||||
@@ -6480,6 +6497,9 @@ RED.view = (function() {
|
||||
* 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.
|
||||
*
|
||||
* `source`/`sourcePort` are option and used to indicate a node the suggestion should be connected to.
|
||||
* If provided, a ghost wire will be added between the source and the first node in the suggestion.
|
||||
*
|
||||
* Limitations:
|
||||
* - does not support groups, subflows or whole tabs
|
||||
* - does not support config nodes
|
||||
@@ -6490,6 +6510,7 @@ RED.view = (function() {
|
||||
* @param {Object} suggestion - The suggestion object
|
||||
*/
|
||||
function setSuggestedFlow (suggestion) {
|
||||
$(window).off('keydown.suggestedFlow')
|
||||
if (!currentSuggestion && !suggestion) {
|
||||
// Avoid unnecessary redraws
|
||||
return
|
||||
@@ -6500,6 +6521,24 @@ RED.view = (function() {
|
||||
if (suggestion?.nodes?.length > 0) {
|
||||
const nodeMap = {}
|
||||
const links = []
|
||||
const positionOffset = { x: 0, y: 0 }
|
||||
if (suggestion.source && suggestion.position === 'relative') {
|
||||
// If the suggestion is relative to a source node, use its position plus a suitable offset
|
||||
let targetX = suggestion.source.x + (suggestion.source.w || 120) + (3 * gridSize)
|
||||
const targetY = suggestion.source.y
|
||||
// Keep targetY where it is, but ensure targetX is grid aligned
|
||||
if (snapGrid) {
|
||||
// This isn't a perfect grid snap, as we don't have the true node width at this point.
|
||||
// TODO: defer grid snapping until the node is created?
|
||||
const gridOffset = RED.view.tools.calculateGridSnapOffsets({ x: targetX, y: targetY, w: node_width, h: node_height });
|
||||
targetX += gridOffset.x
|
||||
}
|
||||
|
||||
positionOffset.x = targetX - (suggestion.nodes[0].x || 0)
|
||||
positionOffset.y = targetY - (suggestion.nodes[0].y || 0)
|
||||
}
|
||||
|
||||
|
||||
suggestion.nodes.forEach(nodeConfig => {
|
||||
if (!nodeConfig.type || nodeConfig.type === 'group' || nodeConfig.type === 'subflow' || nodeConfig.type === 'tab') {
|
||||
// A node type we don't support previewing
|
||||
@@ -6507,8 +6546,9 @@ RED.view = (function() {
|
||||
}
|
||||
|
||||
let node
|
||||
|
||||
if (nodeConfig.type === 'junction') {
|
||||
nodeConfig.x = (nodeConfig.x || 0) + positionOffset.x
|
||||
nodeConfig.y = (nodeConfig.y || 0) + positionOffset.y
|
||||
node = {
|
||||
_def: {defaults:{}},
|
||||
type: 'junction',
|
||||
@@ -6529,6 +6569,8 @@ RED.view = (function() {
|
||||
// TODO: unknown node types could happen...
|
||||
return
|
||||
}
|
||||
nodeConfig.x = (nodeConfig.x || 0) + positionOffset.x
|
||||
nodeConfig.y = (nodeConfig.y || 0) + positionOffset.y
|
||||
const result = createNode(nodeConfig.type, nodeConfig.x, nodeConfig.y)
|
||||
if (!result) {
|
||||
return
|
||||
@@ -6551,6 +6593,11 @@ RED.view = (function() {
|
||||
node.id = nodeConfig.id || node.id
|
||||
node.__ghost = true;
|
||||
node.dirty = true;
|
||||
if (suggestion.clickToApply) {
|
||||
node.__ghostClick = function () {
|
||||
applySuggestedFlow()
|
||||
}
|
||||
}
|
||||
nodeMap[node.id] = node
|
||||
|
||||
if (nodeConfig.wires) {
|
||||
@@ -6579,6 +6626,30 @@ RED.view = (function() {
|
||||
suggestedLinks.push(link)
|
||||
}
|
||||
})
|
||||
if (suggestion.source && suggestedNodes[0]?._def?.inputs > 0) {
|
||||
suggestedLinks.push({
|
||||
source: suggestion.source,
|
||||
sourcePort: suggestion.sourcePort || 0,
|
||||
target: suggestedNodes[0],
|
||||
targetPort: 0,
|
||||
__ghost: true
|
||||
})
|
||||
}
|
||||
if (!RED.typeSearch.isVisible()) {
|
||||
$(window).on('keydown.suggestedFlow', function (evt) {
|
||||
if (evt.keyCode === 9) { // tab
|
||||
applySuggestedFlow();
|
||||
} else {
|
||||
clearSuggestedFlow();
|
||||
RED.view.redraw(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (suggestion.clickToApply) {
|
||||
$(window).on('mousedown.suggestedFlow', function (evnt) {
|
||||
clearSuggestedFlow();
|
||||
})
|
||||
}
|
||||
}
|
||||
if (ghostNode) {
|
||||
if (suggestedNodes.length > 0) {
|
||||
@@ -6591,6 +6662,8 @@ RED.view = (function() {
|
||||
}
|
||||
|
||||
function clearSuggestedFlow () {
|
||||
$(window).off('mousedown.suggestedFlow');
|
||||
$(window).off('keydown.suggestedFlow')
|
||||
currentSuggestion = null
|
||||
suggestedNodes = []
|
||||
suggestedLinks = []
|
||||
@@ -6599,14 +6672,40 @@ RED.view = (function() {
|
||||
function applySuggestedFlow () {
|
||||
if (currentSuggestion && currentSuggestion.nodes) {
|
||||
const nodesToImport = currentSuggestion.nodes
|
||||
const sourceNode = currentSuggestion.source
|
||||
const sourcePort = currentSuggestion.sourcePort || 0
|
||||
setSuggestedFlow(null)
|
||||
return importNodes(nodesToImport, {
|
||||
const result = importNodes(nodesToImport, {
|
||||
generateIds: true,
|
||||
touchImport: true,
|
||||
notify: false,
|
||||
// Ensure the node gets all of its defaults applied
|
||||
applyNodeDefaults: true
|
||||
})
|
||||
if (sourceNode) {
|
||||
const firstNode = result.nodeMap[nodesToImport[0].id]
|
||||
if (firstNode && firstNode._def?.inputs > 0) {
|
||||
// Connect the source node to the first node in the suggestion
|
||||
const link = {
|
||||
source: sourceNode,
|
||||
target: RED.nodes.node(firstNode.id),
|
||||
sourcePort: sourcePort,
|
||||
targetPort: 0
|
||||
};
|
||||
RED.nodes.addLink(link)
|
||||
let historyEvent = RED.history.peek();
|
||||
if (historyEvent.t === "multi") {
|
||||
historyEvent = historyEvent.events.find(e => e.t === "add")
|
||||
}
|
||||
if (historyEvent) {
|
||||
historyEvent.links = historyEvent.links || [];
|
||||
historyEvent.links.push(link);
|
||||
}
|
||||
RED.view.redraw(true);
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user