mirror of
https://github.com/node-red/node-red.git
synced 2025-12-27 07:31:07 +01:00
Merge pull request #5135 from node-red/node-suggest-api
Add node suggestion api to editor and apply to typeSearch
This commit is contained in:
@@ -689,7 +689,7 @@ RED.nodes = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function addNode(n) {
|
||||
function addNode(n, opt) {
|
||||
let newNode
|
||||
if (!n.__isProxy__) {
|
||||
newNode = new Proxy(n, nodeProxyHandler)
|
||||
@@ -728,7 +728,7 @@ RED.nodes = (function() {
|
||||
nodeLinks[n.id] = {in:[],out:[]};
|
||||
}
|
||||
}
|
||||
RED.events.emit('nodes:add',newNode);
|
||||
RED.events.emit('nodes:add',newNode, opt);
|
||||
return newNode
|
||||
}
|
||||
function addLink(l) {
|
||||
@@ -1848,14 +1848,23 @@ RED.nodes = (function() {
|
||||
* - id:copy - import with new id
|
||||
* - 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)
|
||||
*/
|
||||
function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) {
|
||||
const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} }
|
||||
const defOpts = {
|
||||
generateIds: false,
|
||||
addFlow: false,
|
||||
markChanged: false,
|
||||
reimport: false,
|
||||
importMap: {},
|
||||
applyNodeDefaults: false
|
||||
}
|
||||
options = Object.assign({}, defOpts, options)
|
||||
options.importMap = options.importMap || {}
|
||||
const createNewIds = options.generateIds;
|
||||
const reimport = (!createNewIds && !!options.reimport)
|
||||
const createMissingWorkspace = options.addFlow;
|
||||
const applyNodeDefaults = options.applyNodeDefaults;
|
||||
var i;
|
||||
var n;
|
||||
var newNodes;
|
||||
@@ -2234,6 +2243,13 @@ RED.nodes = (function() {
|
||||
for (d in def.defaults) {
|
||||
if (def.defaults.hasOwnProperty(d)) {
|
||||
configNode[d] = n[d];
|
||||
if (applyNodeDefaults && n[d] === undefined) {
|
||||
// If the node has a default value, but the imported node does not
|
||||
// set it, then set it to the default value
|
||||
if (def.defaults[d].value !== undefined) {
|
||||
configNode[d] = JSON.parse(JSON.stringify(def.defaults[d].value))
|
||||
}
|
||||
}
|
||||
configNode._config[d] = JSON.stringify(n[d]);
|
||||
if (def.defaults[d].type) {
|
||||
configNode._configNodeReferences.add(n[d])
|
||||
@@ -2508,6 +2524,13 @@ RED.nodes = (function() {
|
||||
for (d in node._def.defaults) {
|
||||
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
|
||||
node[d] = n[d];
|
||||
if (applyNodeDefaults && n[d] === undefined) {
|
||||
// If the node has a default value, but the imported node does not
|
||||
// set it, then set it to the default value
|
||||
if (node._def.defaults[d].value !== undefined) {
|
||||
node[d] = JSON.parse(JSON.stringify(node._def.defaults[d].value))
|
||||
}
|
||||
}
|
||||
node._config[d] = JSON.stringify(n[d]);
|
||||
}
|
||||
}
|
||||
@@ -2761,7 +2784,8 @@ RED.nodes = (function() {
|
||||
workspaces:new_workspaces,
|
||||
subflows:new_subflows,
|
||||
missingWorkspace: missingWorkspace,
|
||||
removedNodes: removedNodes
|
||||
removedNodes: removedNodes,
|
||||
nodeMap: node_map
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ RED.typeSearch = (function() {
|
||||
var addCallback;
|
||||
var cancelCallback;
|
||||
var moveCallback;
|
||||
var suggestCallback
|
||||
|
||||
var typesUsed = {};
|
||||
|
||||
@@ -25,6 +26,11 @@ RED.typeSearch = (function() {
|
||||
selected = 0;
|
||||
searchResults.children().removeClass('selected');
|
||||
searchResults.children(":visible:first").addClass('selected');
|
||||
const children = searchResults.children(":visible");
|
||||
const n = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (n) {
|
||||
updateSuggestion(n)
|
||||
}
|
||||
},100);
|
||||
|
||||
}
|
||||
@@ -63,7 +69,7 @@ RED.typeSearch = (function() {
|
||||
}
|
||||
});
|
||||
searchInput.on('keydown',function(evt) {
|
||||
var children = searchResults.children(":visible");
|
||||
const children = searchResults.children(":visible");
|
||||
if (evt.keyCode === 40 && evt.shiftKey) {
|
||||
evt.preventDefault();
|
||||
moveDialog(0,10);
|
||||
@@ -86,9 +92,14 @@ RED.typeSearch = (function() {
|
||||
selected++;
|
||||
}
|
||||
$(children[selected]).addClass('selected');
|
||||
const n = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (n) {
|
||||
updateSuggestion(n)
|
||||
}
|
||||
ensureSelectedIsVisible();
|
||||
evt.preventDefault();
|
||||
} else if (evt.keyCode === 38) {
|
||||
// Up
|
||||
if (selected > 0) {
|
||||
if (selected < children.length) {
|
||||
$(children[selected]).removeClass('selected');
|
||||
@@ -96,6 +107,10 @@ RED.typeSearch = (function() {
|
||||
selected--;
|
||||
}
|
||||
$(children[selected]).addClass('selected');
|
||||
const n = $(children[selected]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (n) {
|
||||
updateSuggestion(n)
|
||||
}
|
||||
ensureSelectedIsVisible();
|
||||
evt.preventDefault();
|
||||
} else if ((evt.metaKey || evt.ctrlKey) && evt.keyCode === 13 ) {
|
||||
@@ -103,14 +118,14 @@ RED.typeSearch = (function() {
|
||||
// (ctrl or cmd) and enter
|
||||
var index = Math.max(0,selected);
|
||||
if (index < children.length) {
|
||||
var n = $(children[index]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (!/^_action_:/.test(n.type)) {
|
||||
const n = $(children[index]).find(".red-ui-editableList-item-content").data('data');
|
||||
if (!n.nodes && !/^_action_:/.test(n.type)) {
|
||||
typesUsed[n.type] = Date.now();
|
||||
}
|
||||
if (n.def.outputs === 0) {
|
||||
confirm(n);
|
||||
} else {
|
||||
addCallback(n.type,true);
|
||||
addCallback(n, true);
|
||||
}
|
||||
$("#red-ui-type-search-input").val("").trigger("keyup");
|
||||
setTimeout(function() {
|
||||
@@ -142,7 +157,7 @@ RED.typeSearch = (function() {
|
||||
if (activeFilter === "" ) {
|
||||
return true;
|
||||
}
|
||||
if (data.recent || data.common) {
|
||||
if (data.recent || data.common || data.suggestion) {
|
||||
return false;
|
||||
}
|
||||
return (activeFilter==="")||(data.index.indexOf(activeFilter) > -1);
|
||||
@@ -164,67 +179,116 @@ RED.typeSearch = (function() {
|
||||
}
|
||||
return Ai-Bi;
|
||||
},
|
||||
addItem: function(container,i,object) {
|
||||
var def = object.def;
|
||||
object.index = object.type.toLowerCase();
|
||||
if (object.separator) {
|
||||
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;
|
||||
let nodeType = nodeItem.type;
|
||||
if (nodeItem.suggestion && nodeItem.nodes.length > 0) {
|
||||
nodeDef = RED.nodes.getType(nodeItem.nodes[0].type);
|
||||
nodeType = nodeItem.nodes[0].type;
|
||||
}
|
||||
|
||||
nodeItem.index = nodeItem.type?.toLowerCase() || '';
|
||||
if (nodeItem.separator) {
|
||||
container.addClass("red-ui-search-result-separator")
|
||||
}
|
||||
var div = $('<div>',{class:"red-ui-search-result"}).appendTo(container);
|
||||
|
||||
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
|
||||
if (object.type === "junction") {
|
||||
const div = $('<div>',{class:"red-ui-search-result"}).appendTo(container);
|
||||
const nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).appendTo(div);
|
||||
|
||||
if (nodeItem.suggestionPlaceholder) {
|
||||
nodeDiv.addClass("red-ui-palette-icon-suggestion")
|
||||
const iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
|
||||
$('<i class="spinner" style="margin-top: -1px">').appendTo(iconContainer);
|
||||
} else if (nodeType === "junction") {
|
||||
nodeDiv.addClass("red-ui-palette-icon-junction");
|
||||
} else if (/^_action_:/.test(object.type)) {
|
||||
nodeDiv.addClass("red-ui-palette-icon-junction")
|
||||
} else {
|
||||
var colour = RED.utils.getNodeColor(object.type,def);
|
||||
nodeDiv.css('backgroundColor',colour);
|
||||
nodeDiv.css('backgroundColor', RED.utils.getNodeColor(nodeType, nodeDef));
|
||||
}
|
||||
var icon_url = RED.utils.getNodeIcon(def);
|
||||
|
||||
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
|
||||
RED.utils.createIconElement(icon_url, iconContainer, false);
|
||||
if (nodeDef) {
|
||||
// Add the node icon
|
||||
const icon_url = RED.utils.getNodeIcon(nodeDef);
|
||||
const iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv);
|
||||
RED.utils.createIconElement(icon_url, iconContainer, false);
|
||||
}
|
||||
|
||||
if (/^subflow:/.test(object.type)) {
|
||||
var sf = RED.nodes.subflow(object.type.substring(8));
|
||||
if (/^subflow:/.test(nodeType)) {
|
||||
var sf = RED.nodes.subflow(nodeType.substring(8));
|
||||
if (sf.in.length > 0) {
|
||||
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
|
||||
}
|
||||
if (sf.out.length > 0) {
|
||||
$('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
|
||||
}
|
||||
} else if (!/^_action_:/.test(object.type) && object.type !== "junction") {
|
||||
if (def.inputs > 0) {
|
||||
} else if (nodeDef && nodeType !== "junction") {
|
||||
if (nodeDef.inputs > 0) {
|
||||
$('<div/>',{class:"red-ui-search-result-node-port"}).appendTo(nodeDiv);
|
||||
}
|
||||
if (def.outputs > 0) {
|
||||
if (nodeDef.outputs > 0) {
|
||||
$('<div/>',{class:"red-ui-search-result-node-port red-ui-search-result-node-output"}).appendTo(nodeDiv);
|
||||
}
|
||||
}
|
||||
|
||||
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
|
||||
|
||||
var label = object.label;
|
||||
object.index += "|"+label.toLowerCase();
|
||||
var label = nodeItem.label;
|
||||
nodeItem.index += "|"+label.toLowerCase();
|
||||
|
||||
$('<div>',{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() {
|
||||
const children = searchResults.children(":visible");
|
||||
if (selected > -1 && selected < children.length) {
|
||||
$(children[selected]).removeClass('selected');
|
||||
}
|
||||
const editableListItem = container.parent()
|
||||
selected = children.index(editableListItem);
|
||||
$(children[selected]).addClass('selected');
|
||||
updateSuggestion(nodeItem);
|
||||
})
|
||||
},
|
||||
scrollOnAdd: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateSuggestion(nodeItem) {
|
||||
if (suggestCallback) {
|
||||
if (!nodeItem) {
|
||||
suggestCallback(null);
|
||||
} else 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
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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 +338,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) {
|
||||
@@ -294,6 +359,9 @@ RED.typeSearch = (function() {
|
||||
},200);
|
||||
}
|
||||
function hide(fast) {
|
||||
if (suggestCallback) {
|
||||
suggestCallback(null);
|
||||
}
|
||||
if (visible) {
|
||||
visible = false;
|
||||
if (dialog !== null) {
|
||||
@@ -356,11 +424,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 +439,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 +447,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 +458,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<common.length;i++) {
|
||||
var itemDef = RED.nodes.getType(common[i]);
|
||||
let itemDef
|
||||
if (common[i] === 'junction') {
|
||||
itemDef = { inputs:1, outputs: 1, label: 'junction', type: 'junction'}
|
||||
} else if (/^_action_:/.test(common[i]) ) {
|
||||
itemDef = { inputs:1, outputs: 1, label: common[i], type: common[i]}
|
||||
} else {
|
||||
itemDef = RED.nodes.getType(common[i]);
|
||||
}
|
||||
if (itemDef) {
|
||||
item = {
|
||||
const item = {
|
||||
type: common[i],
|
||||
common: true,
|
||||
def: itemDef,
|
||||
@@ -414,7 +511,7 @@ RED.typeSearch = (function() {
|
||||
}
|
||||
}
|
||||
for(i=0;i<Math.min(5,recentlyUsed.length);i++) {
|
||||
item = {
|
||||
const item = {
|
||||
type:recentlyUsed[i],
|
||||
def: RED.nodes.getType(recentlyUsed[i]),
|
||||
recent: true,
|
||||
|
||||
@@ -24,7 +24,7 @@ RED.view.annotations = (function() {
|
||||
refreshAnnotation = !!evt.node[opts.refresh]
|
||||
delete evt.node[opts.refresh]
|
||||
} else if (typeof opts.refresh === "function") {
|
||||
refreshAnnotation = opts.refresh(evnt.node)
|
||||
refreshAnnotation = opts.refresh(evt.node)
|
||||
}
|
||||
if (refreshAnnotation) {
|
||||
refreshAnnotationElement(annotation.id, annotation.node, annotation.element)
|
||||
|
||||
@@ -100,6 +100,11 @@ RED.view = (function() {
|
||||
var clipboard = "";
|
||||
let clipboardSource
|
||||
|
||||
let currentSuggestion = null;
|
||||
let suggestedNodes = [];
|
||||
let suggestedLinks = [];
|
||||
let suggestedJunctions = [];
|
||||
|
||||
// Note: these are the permitted status colour aliases. The actual RGB values
|
||||
// are set in the CSS - flow.scss/colors.scss
|
||||
const status_colours = {
|
||||
@@ -548,6 +553,8 @@ RED.view = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
clearSuggestedFlow();
|
||||
|
||||
RED.menu.setDisabled("menu-item-workspace-edit", activeFlowLocked || activeSubflow || event.workspace === 0);
|
||||
RED.menu.setDisabled("menu-item-workspace-delete",activeFlowLocked || event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow);
|
||||
|
||||
@@ -653,7 +660,7 @@ RED.view = (function() {
|
||||
return;
|
||||
}
|
||||
var historyEvent = result.historyEvent;
|
||||
var nn = RED.nodes.add(result.node);
|
||||
var nn = RED.nodes.add(result.node, { source: 'palette' });
|
||||
|
||||
var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label");
|
||||
if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) {
|
||||
@@ -1395,6 +1402,20 @@ RED.view = (function() {
|
||||
var lastAddedX;
|
||||
var lastAddedWidth;
|
||||
|
||||
const context = {}
|
||||
|
||||
if (quickAddLink) {
|
||||
context.source = quickAddLink.node.id;
|
||||
context.sourcePort = quickAddLink.port;
|
||||
context.sourcePortType = quickAddLink.portType;
|
||||
if (quickAddLink?.virtualLink) {
|
||||
context.virtualLink = true;
|
||||
}
|
||||
context.flow = RED.nodes.createExportableNodeSet(RED.nodes.getAllFlowNodes(quickAddLink.node))
|
||||
}
|
||||
|
||||
// console.log(context)
|
||||
|
||||
RED.typeSearch.show({
|
||||
x:clientX-mainPos.left-node_width/2 - (ox-point[0]),
|
||||
y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]),
|
||||
@@ -1430,7 +1451,63 @@ RED.view = (function() {
|
||||
keepAdding = false;
|
||||
resetMouseVars();
|
||||
}
|
||||
if (typeof type !== 'string') {
|
||||
if (type.nodes) {
|
||||
|
||||
// Importing a flow definition
|
||||
// console.log('Importing flow definition', type.nodes)
|
||||
const importResult = importNodes(type.nodes, {
|
||||
generateIds: true,
|
||||
touchImport: true,
|
||||
notify: false,
|
||||
// Ensure the node gets all of its defaults applied
|
||||
applyNodeDefaults: true
|
||||
})
|
||||
quickAddActive = false;
|
||||
ghostNode.remove();
|
||||
|
||||
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]
|
||||
|
||||
const drag_line = quickAddLink;
|
||||
let src = null, dst, src_port;
|
||||
if (drag_line.portType === PORT_TYPE_OUTPUT && (targetNode.inputs > 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<l;i++) {
|
||||
node = movingSet.get(i);
|
||||
@@ -5953,42 +6099,48 @@ RED.view = (function() {
|
||||
|
||||
updateActiveNodes();
|
||||
redraw();
|
||||
|
||||
var counts = [];
|
||||
var newNodeCount = 0;
|
||||
var newConfigNodeCount = 0;
|
||||
new_nodes.forEach(function(n) {
|
||||
if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
|
||||
newNodeCount++;
|
||||
} else {
|
||||
newConfigNodeCount++;
|
||||
if (showNotification) {
|
||||
var counts = [];
|
||||
var newNodeCount = 0;
|
||||
var newConfigNodeCount = 0;
|
||||
new_nodes.forEach(function(n) {
|
||||
if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
|
||||
newNodeCount++;
|
||||
} else {
|
||||
newConfigNodeCount++;
|
||||
}
|
||||
})
|
||||
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}));
|
||||
}
|
||||
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 = "<ul><li>"+counts.join("</li><li>")+"</li></ul>";
|
||||
RED.notify("<p>"+RED._("clipboard.nodesImported")+"</p>"+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 = "<ul><li>"+counts.join("</li><li>")+"</li></ul>";
|
||||
RED.notify("<p>"+RED._("clipboard.nodesImported")+"</p>"+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 (let 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
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user