Support multiple suggestions at once

This commit is contained in:
Nick O'Leary
2025-07-11 09:32:51 +01:00
parent 2608beeea5
commit 8380c06a19
3 changed files with 93 additions and 23 deletions

View File

@@ -1909,6 +1909,7 @@ RED.nodes = (function() {
* - id:replace - import over the top of existing
* - modules: map of module:version - hints for unknown nodes
* - applyNodeDefaults - whether to apply default values to the imported nodes (default: false)
* - eventContext - context to include in the `nodes:add` event
*/
function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) {
const defOpts = {
@@ -1917,7 +1918,8 @@ RED.nodes = (function() {
markChanged: false,
reimport: false,
importMap: {},
applyNodeDefaults: false
applyNodeDefaults: false,
eventContext: null
}
options = Object.assign({}, defOpts, options)
options.importMap = options.importMap || {}
@@ -2793,7 +2795,7 @@ RED.nodes = (function() {
// Now the nodes have been fully updated, add them.
for (i=0;i<new_nodes.length;i++) {
new_nodes[i] = addNode(new_nodes[i])
new_nodes[i] = addNode(new_nodes[i], options.eventContext)
node_map[new_nodes[i].id] = new_nodes[i]
}

View File

@@ -302,23 +302,27 @@ RED.keyboard = (function() {
return resolveKeyEvent(evt);
}
}
d3.select(window).on("keydown",function() {
d3.select(window).on("keydown", () => {
handleEvent(d3.event)
})
function handleEvent (evt) {
if (!handlersActive) {
return;
}
if (metaKeyCodes[d3.event.keyCode]) {
if (metaKeyCodes[evt]) {
return;
}
var handler = resolveKeyEvent(d3.event);
var handler = resolveKeyEvent(evt);
if (handler && handler.ondown) {
if (typeof handler.ondown === "string") {
RED.actions.invoke(handler.ondown);
} else {
handler.ondown();
}
d3.event.preventDefault();
}
});
evt.preventDefault();
}
}
function addHandler(scope,key,modifiers,ondown) {
var mod = modifiers;
@@ -700,7 +704,8 @@ RED.keyboard = (function() {
formatKey: formatKey,
validateKey: validateKey,
disable: disable,
enable: enable
enable: enable,
handle: handleEvent
}
})();

View File

@@ -793,7 +793,11 @@ RED.view = (function() {
if (RED.workspaces.isLocked()) {
return
}
importNodes(clipboard,{generateIds: clipboardSource === 'copy', generateDefaultNames: clipboardSource === 'copy'});
importNodes(clipboard, {
generateIds: clipboardSource === 'copy',
generateDefaultNames: clipboardSource === 'copy',
eventContext: { source: 'clipboard' }
});
});
RED.actions.add("core:detach-selected-nodes", function() { detachSelectedNodes() })
@@ -1746,6 +1750,7 @@ RED.view = (function() {
}
},
suggest: function (suggestion) {
// TypeSearch only provides one suggestion at a time
if (suggestion?.nodes?.length > 0) {
// Reposition the suggestion relative to the existing ghost node position
const deltaX = (suggestion.nodes[0].x || 0) - point[0]
@@ -5850,7 +5855,8 @@ RED.view = (function() {
generateIds: false,
generateDefaultNames: false,
notify: true,
applyNodeDefaults: false
applyNodeDefaults: false,
eventContext: null
}
const addNewFlow = options.addFlow
const touchImport = options.touchImport;
@@ -5940,7 +5946,8 @@ RED.view = (function() {
importMap: options.importMap,
markChanged: true,
modules: modules,
applyNodeDefaults: applyNodeDefaults
applyNodeDefaults: applyNodeDefaults,
eventContext: options.eventContext
});
if (importResult) {
var new_nodes = importResult.nodes;
@@ -6512,6 +6519,8 @@ RED.view = (function() {
*/
function setSuggestedFlow (suggestion) {
$(window).off('keydown.suggestedFlow')
$(window).off('mousedown.suggestedFlow');
RED.keyboard.enable()
if (!currentSuggestion && !suggestion) {
// Avoid unnecessary redraws
return
@@ -6519,7 +6528,28 @@ RED.view = (function() {
// Clear up any existing suggestion state
clearSuggestedFlow()
currentSuggestion = suggestion
if (suggestion?.nodes?.length > 0) {
if (suggestion && suggestion.nodes) {
// If suggestion.nodes is an array of arrays, then there are multiple suggestions being provided
// Normalise the shape of the suggestion.nodes to be an array of arrays
if (!Array.isArray(suggestion.nodes[0])) {
suggestion.nodes = [suggestion.nodes]
}
suggestion.count = suggestion.nodes.length
suggestion.currentIndex = 0
suggestion.current = suggestion.nodes[suggestion.currentIndex]
refreshSuggestedFlow()
} else {
redraw()
}
}
function refreshSuggestedFlow () {
const suggestion = currentSuggestion
suggestedNodes = []
suggestedLinks = []
currentSuggestion.current = currentSuggestion.nodes[currentSuggestion.currentIndex]
if (suggestion.current.length > 0) {
const nodeMap = {}
const links = []
const positionOffset = { x: 0, y: 0 }
@@ -6535,12 +6565,11 @@ RED.view = (function() {
targetX += gridOffset.x
}
positionOffset.x = targetX - (suggestion.nodes[0].x || 0)
positionOffset.y = targetY - (suggestion.nodes[0].y || 0)
positionOffset.x = targetX - (suggestion.current[0].x || 0)
positionOffset.y = targetY - (suggestion.current[0].y || 0)
}
suggestion.nodes.forEach(nodeConfig => {
suggestion.current.forEach(nodeConfig => {
if (!nodeConfig.type || nodeConfig.type === 'group' || nodeConfig.type === 'subflow' || nodeConfig.type === 'tab') {
// A node type we don't support previewing
return
@@ -6637,12 +6666,38 @@ RED.view = (function() {
})
}
if (!RED.typeSearch.isVisible()) {
// Disable the core keyboard handling so we get priority.
// Ideally we'd be able to do this via actions, but we can't currently scope
// actions finely enough to only be handled when the suggested flow is active.
RED.keyboard.disable()
$(window).on('keydown.suggestedFlow', function (evt) {
if (evt.keyCode === 9) { // tab
$(window).off('keydown.suggestedFlow')
RED.keyboard.enable()
if (evt.keyCode === 9) { // tab; apply suggestion
evt.stopPropagation();
evt.preventDefault();
applySuggestedFlow();
} else {
} else if (evt.keyCode === 38 && currentSuggestion.count > 1) { // up arrow
evt.stopPropagation();
evt.preventDefault();
currentSuggestion.currentIndex--
if (currentSuggestion.currentIndex < 0) {
currentSuggestion.currentIndex = currentSuggestion.count - 1
}
refreshSuggestedFlow();
} else if (evt.keyCode === 40 && currentSuggestion.count > 1) { // down arrow
evt.stopPropagation();
evt.preventDefault();
currentSuggestion.currentIndex++
if (currentSuggestion.currentIndex === currentSuggestion.count) {
currentSuggestion.currentIndex = 0
}
refreshSuggestedFlow();
} else { // Anything else; clear the suggestion
clearSuggestedFlow();
RED.view.redraw(true);
// manually push the event to the keyboard handler
RED.keyboard.handle(evt)
}
});
}
@@ -6659,20 +6714,27 @@ RED.view = (function() {
ghostNode.style('opacity', 1)
}
}
redraw();
if (currentSuggestion.count > 1 && suggestedNodes.length > 0) {
suggestedNodes[0].status = {
text: `${currentSuggestion.currentIndex + 1} / ${currentSuggestion.count}`,
}
suggestedNodes[0].dirtyStatus = true
}
redraw()
}
function clearSuggestedFlow () {
$(window).off('mousedown.suggestedFlow');
$(window).off('keydown.suggestedFlow')
RED.keyboard.enable()
currentSuggestion = null
suggestedNodes = []
suggestedLinks = []
}
function applySuggestedFlow () {
if (currentSuggestion && currentSuggestion.nodes) {
const nodesToImport = currentSuggestion.nodes
if (currentSuggestion && currentSuggestion.current) {
const nodesToImport = currentSuggestion.current
const sourceNode = currentSuggestion.source
const sourcePort = currentSuggestion.sourcePort || 0
setSuggestedFlow(null)
@@ -6681,7 +6743,8 @@ RED.view = (function() {
touchImport: true,
notify: false,
// Ensure the node gets all of its defaults applied
applyNodeDefaults: true
applyNodeDefaults: true,
eventContext: { source: 'suggestion' }
})
if (sourceNode) {
const firstNode = result.nodeMap[nodesToImport[0].id]