Merge branch 'dev' into feature/export-module-info-2

This commit is contained in:
Nick O'Leary
2025-06-06 10:07:32 +01:00
committed by GitHub
54 changed files with 1751 additions and 502 deletions

View File

@@ -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) {
@@ -1859,14 +1859,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;
@@ -2260,6 +2269,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])
@@ -2534,6 +2550,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]);
}
}
@@ -2787,7 +2810,8 @@ RED.nodes = (function() {
workspaces:new_workspaces,
subflows:new_subflows,
missingWorkspace: missingWorkspace,
removedNodes: removedNodes
removedNodes: removedNodes,
nodeMap: node_map
}
}

View File

@@ -358,7 +358,10 @@ var RED = (function() {
});
return;
}
if (notificationId === "update-available") {
// re-emit as an event to be handled in editor-client/src/js/ui/palette-editor.js
RED.events.emit("notification/update-available", msg)
}
if (msg.text) {
msg.default = msg.text;
var text = RED._(msg.text,msg);
@@ -672,14 +675,48 @@ var RED = (function() {
setTimeout(function() {
loader.end();
checkFirstRun(function() {
if (showProjectWelcome) {
RED.projects.showStartup();
}
});
checkTelemetry(function () {
checkFirstRun(function() {
if (showProjectWelcome) {
RED.projects.showStartup();
}
});
})
},100);
}
function checkTelemetry(done) {
const telemetrySettings = RED.settings.telemetryEnabled;
// Can only get telemetry permission from a user with permission to modify settings
if (RED.user.hasPermission("settings.write") && telemetrySettings === undefined) {
const dialog = RED.popover.dialog({
title: RED._("telemetry.settingsTitle"),
content: `${RED._("telemetry.settingsDescription")}${RED._("telemetry.settingsDescription2")}`,
closeButton: false,
buttons: [
{
text: RED._("telemetry.enableLabel"),
click: () => {
RED.settings.set("telemetryEnabled", true)
dialog.close()
done()
}
},
{
text: RED._("telemetry.disableLabel"),
click: () => {
RED.settings.set("telemetryEnabled", false)
dialog.close()
done()
}
}
]
})
} else {
done()
}
}
function checkFirstRun(done) {
if (RED.settings.theme("tours") === false) {
done();

View File

@@ -163,13 +163,18 @@ RED.popover = (function() {
}
var timer = null;
let isOpen = false
var active;
var div;
var contentDiv;
var currentStyle;
var openPopup = function(instant) {
if (isOpen) {
return
}
if (active) {
isOpen = true
var existingPopover = target.data("red-ui-popover");
if (options.tooltip && existingPopover) {
active = false;
@@ -334,6 +339,7 @@ RED.popover = (function() {
}
var closePopup = function(instant) {
isOpen = false
$(document).off('mousedown.red-ui-popover');
if (!active) {
if (div) {
@@ -673,6 +679,74 @@ RED.popover = (function() {
show:show,
hide:hide
}
},
dialog: function(options) {
const dialogContent = $('<div style="position:relative"></div>');
if (options.closeButton !== false) {
$('<button type="button" class="red-ui-button red-ui-button-small" style="float: right; margin-top: -4px; margin-right: -4px;"><i class="fa fa-times"></i></button>').appendTo(dialogContent).click(function(evt) {
evt.preventDefault();
close();
})
}
const dialogBody = $('<div class="red-ui-dialog-body"></div>').appendTo(dialogContent);
if (options.title) {
$('<h2>').text(options.title).appendTo(dialogBody);
}
$('<div>').css("text-align","left").html(options.content).appendTo(dialogBody);
const stepToolbar = $('<div>',{class:"red-ui-dialog-toolbar"}).appendTo(dialogContent);
if (options.buttons) {
options.buttons.forEach(button => {
const btn = $('<button type="button" class="red-ui-button"></button>').text(button.text).appendTo(stepToolbar);
if (button.class) {
btn.addClass(button.class);
}
if (button.click) {
btn.on('click', function(evt) {
evt.preventDefault();
button.click();
})
}
})
}
const width = 500;
const maxWidth = Math.min($(window).width()-10,Math.max(width || 0, 300));
let shade = $('<div class="red-ui-shade" style="z-index: 2000"></div>').appendTo(document.body);
shade.fadeIn()
let popover = RED.popover.create({
target: $(".red-ui-editor"),
width: width || "auto",
maxWidth: maxWidth+"px",
direction: "inset",
class: "red-ui-dialog",
trigger: "manual",
content: dialogContent
}).open()
function close() {
if (shade) {
shade.fadeOut(() => {
shade.remove()
shade = null
})
}
if (popover) {
popover.close()
popover = null
}
}
return {
close
}
}
}

View File

@@ -46,10 +46,20 @@ RED.contextMenu = (function () {
hasEnabledNode = true;
}
}
if (n.l === undefined || n.l) {
hasLabeledNode = true;
if (n.l === undefined) {
// Check if the node sets showLabel in the defaults
// as that determines the default behaviour for the node
if (n._def.showLabel !== false) {
hasLabeledNode = true;
} else {
hasUnlabeledNode = true;
}
} else {
hasUnlabeledNode = true;
if (n.l) {
hasLabeledNode = true;
} else {
hasUnlabeledNode = true;
}
}
}
}

View File

@@ -284,37 +284,80 @@ RED.palette.editor = (function() {
function _refreshNodeModule(module) {
if (!nodeEntries.hasOwnProperty(module)) {
nodeEntries[module] = {info:RED.nodes.registry.getModule(module)};
var index = [module];
for (var s in nodeEntries[module].info.sets) {
if (nodeEntries[module].info.sets.hasOwnProperty(s)) {
index.push(s);
index = index.concat(nodeEntries[module].info.sets[s].types)
const nodeInfo = RED.nodes.registry.getModule(module);
let index = [module];
nodeEntries[module] = {
info: {
name: nodeInfo.name,
version: nodeInfo.version,
local: nodeInfo.local,
nodeSet: nodeInfo.sets,
},
};
if (nodeInfo.pending_version) {
nodeEntries[module].info.pending_version = nodeInfo.pending_version;
}
for (const set in nodeInfo.sets) {
if (nodeInfo.sets.hasOwnProperty(set)) {
index.push(set);
index = index.concat(nodeInfo.sets[set].types);
}
}
nodeEntries[module].index = index.join(",").toLowerCase();
nodeList.editableList('addItem', nodeEntries[module]);
} else {
var moduleInfo = nodeEntries[module].info;
var nodeEntry = nodeEntries[module].elements;
if (nodeEntry) {
if (moduleInfo.plugin) {
nodeEntry.enableButton.hide();
nodeEntry.removeButton.show();
if (nodeEntries[module].info.pluginSet && !nodeEntries[module].info.nodeSet) {
// Since plugins are loaded before nodes, check if the module has nodes too
const nodeInfo = RED.nodes.registry.getModule(module);
if (nodeInfo) {
let index = [nodeEntries[module].index];
for (const set in nodeInfo.sets) {
if (nodeInfo.sets.hasOwnProperty(set)) {
index.push(set);
index = index.concat(nodeInfo.sets[set].types)
}
}
nodeEntries[module].info.nodeSet = nodeInfo.sets;
nodeEntries[module].index = index.join(",").toLowerCase();
}
}
const moduleInfo = nodeEntries[module].info;
const nodeEntry = nodeEntries[module].elements;
if (nodeEntry) {
const setCount = [];
if (moduleInfo.pluginSet) {
let pluginCount = 0;
for (let setName in moduleInfo.sets) {
if (moduleInfo.sets.hasOwnProperty(setName)) {
let set = moduleInfo.sets[setName];
if (set.plugins) {
for (const setName in moduleInfo.pluginSet) {
if (moduleInfo.pluginSet.hasOwnProperty(setName)) {
let set = moduleInfo.pluginSet[setName];
if (set.plugins && set.plugins.length) {
pluginCount += set.plugins.length;
} else if (set.plugins && !!RED.plugins.getPlugin(setName)) {
// `registerPlugin` in runtime not called but called in editor, add it
pluginCount++;
}
}
}
nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount}));
} else {
setCount.push(RED._('palette.editor.pluginCount', { count: pluginCount }));
if (!moduleInfo.nodeSet) {
// Module only have plugins
nodeEntry.enableButton.hide();
nodeEntry.removeButton.show();
}
}
if (moduleInfo.nodeSet) {
var activeTypeCount = 0;
var typeCount = 0;
var errorCount = 0;
@@ -322,10 +365,10 @@ RED.palette.editor = (function() {
nodeEntries[module].totalUseCount = 0;
nodeEntries[module].setUseCount = {};
for (var setName in moduleInfo.sets) {
if (moduleInfo.sets.hasOwnProperty(setName)) {
var inUseCount = 0;
const set = moduleInfo.sets[setName];
for (const setName in moduleInfo.nodeSet) {
if (moduleInfo.nodeSet.hasOwnProperty(setName)) {
let inUseCount = 0;
const set = moduleInfo.nodeSet[setName];
const setElements = nodeEntry.sets[setName]
if (set.err) {
@@ -342,8 +385,8 @@ RED.palette.editor = (function() {
activeTypeCount += set.types.length;
}
typeCount += set.types.length;
for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
var t = moduleInfo.sets[setName].types[i];
for (var i=0;i<moduleInfo.nodeSet[setName].types.length;i++) {
var t = moduleInfo.nodeSet[setName].types[i];
inUseCount += (typesInUse[t]||0);
if (setElements && set.enabled) {
var def = RED.nodes.getType(t);
@@ -379,8 +422,8 @@ RED.palette.editor = (function() {
nodeEntry.errorRow.show();
}
var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));
const nodeCount = (activeTypeCount === typeCount) ? typeCount : activeTypeCount + " / " + typeCount;
setCount.push(RED._('palette.editor.nodeCount', { count: typeCount, label: nodeCount }));
if (nodeEntries[module].totalUseCount > 0) {
nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
@@ -399,6 +442,7 @@ RED.palette.editor = (function() {
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
}
}
nodeEntry.setCount.text(setCount.join(" & ") || RED._("sidebar.info.empty"));
}
if (moduleInfo.pending_version) {
nodeEntry.versionSpan.html(moduleInfo.version+' <i class="fa fa-long-arrow-right"></i> '+moduleInfo.pending_version).appendTo(nodeEntry.metaRow)
@@ -793,19 +837,36 @@ RED.palette.editor = (function() {
})
RED.events.on("registry:plugin-module-added", function(module) {
if (!nodeEntries.hasOwnProperty(module)) {
nodeEntries[module] = {info:RED.plugins.getModule(module)};
var index = [module];
for (var s in nodeEntries[module].info.sets) {
if (nodeEntries[module].info.sets.hasOwnProperty(s)) {
index.push(s);
index = index.concat(nodeEntries[module].info.sets[s].types)
const pluginInfo = RED.plugins.getModule(module);
let index = [module];
nodeEntries[module] = {
info: {
name: pluginInfo.name,
version: pluginInfo.version,
local: pluginInfo.local,
pluginSet: pluginInfo.sets,
}
};
if (pluginInfo.pending_version) {
nodeEntries[module].info.pending_version = pluginInfo.pending_version;
}
for (const set in pluginInfo.sets) {
if (pluginInfo.sets.hasOwnProperty(set)) {
index.push(set);
// TODO: not sure plugin has `types` property
index = index.concat(pluginInfo.sets[set].types)
}
}
nodeEntries[module].index = index.join(",").toLowerCase();
nodeList.editableList('addItem', nodeEntries[module]);
} else {
// Since plugins are loaded before nodes,
// `nodeEntries[module]` should be undefined
_refreshNodeModule(module);
}
@@ -819,6 +880,14 @@ RED.palette.editor = (function() {
}
}
});
RED.events.on("notification/update-available", function (msg) {
const updateKnownAbout = updateStatusState.version === msg.version
updateStatusState.version = msg.version
if (updateStatusWidgetPopover && !updateKnownAbout) {
setTimeout(() => { updateStatusWidgetPopover.open(); setTimeout(() => updateStatusWidgetPopover.close(), 20000) }, 1000)
}
})
}
function getSettingsPane() {
@@ -980,12 +1049,28 @@ RED.palette.editor = (function() {
}
})
const populateSetList = function () {
var setList = Object.keys(entry.sets)
setList.sort(function(A,B) {
const setList = [...Object.keys(entry.nodeSet || {}), ...Object.keys(entry.pluginSet || {})];
setList.sort(function (A, B) {
return A.toLowerCase().localeCompare(B.toLowerCase());
});
setList.forEach(function(setName) {
var set = entry.sets[setName];
setList.forEach(function (setName) {
const set = (entry.nodeSet && setName in entry.nodeSet) ? entry.nodeSet[setName] : entry.pluginSet[setName];
if (set.plugins && !set.plugins.length) {
// `registerPlugin` in the runtime not called
if (!!RED.plugins.getPlugin(setName)) {
// Add plugin if registered in editor but not in runtime
// Can happen if plugin doesn't have .js file
set.plugins.push({ id: setName });
} else {
// `registerPlugin` in the editor not called - do not add this empty set
return;
}
} else if (set.types && !set.types.length) {
// `registerPlugin` in the runtime not called - do not add this empty set
return;
}
var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow);
var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
var typeSwatches = {};
@@ -1202,7 +1287,17 @@ RED.palette.editor = (function() {
var headerRow = $('<div>',{class:"red-ui-palette-module-header"}).appendTo(container);
var titleRow = $('<div class="red-ui-palette-module-meta red-ui-palette-module-name"><i class="fa fa-cube"></i></div>').appendTo(headerRow);
$('<span>').text(entry.name||entry.id).appendTo(titleRow);
$('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href',entry.url).appendTo(titleRow);
if (entry.url) {
$('<a target="_blank" class="red-ui-palette-module-link"><i class="fa fa-external-link"></i></a>').attr('href',entry.url).appendTo(titleRow);
}
if (entry.deprecated) {
const deprecatedWarning = $('<span class="red-ui-palette-module-deprecated"></span>').text(RED._('palette.editor.deprecated')).appendTo(titleRow);
let message = $('<span>').text(RED._('palette.editor.deprecatedTip'))
if (typeof entry.deprecated === 'string') {
$('<p>').text(entry.deprecated).appendTo(message)
}
RED.popover.tooltip(deprecatedWarning, message);
}
var descRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
$('<div>',{class:"red-ui-palette-module-description"}).text(entry.description).appendTo(descRow);
var metaRow = $('<div class="red-ui-palette-module-meta"></div>').appendTo(headerRow);
@@ -1452,7 +1547,7 @@ RED.palette.editor = (function() {
}
} else {
// dedicated list management for plugins
if (entry.plugin) {
if (entry.pluginSet) {
let e = nodeEntries[entry.name];
if (e) {
@@ -1465,9 +1560,9 @@ RED.palette.editor = (function() {
// cleans the editor accordingly of its left-overs.
let found_onremove = true;
let keys = Object.keys(entry.sets);
let keys = Object.keys(entry.pluginSet);
keys.forEach((key) => {
let set = entry.sets[key];
let set = entry.pluginSet[key];
for (let i=0; i<set.plugins?.length; i++) {
let plgn = RED.plugins.getPlugin(set.plugins[i].id);
if (plgn && plgn.onremove && typeof plgn.onremove === 'function') {
@@ -1667,28 +1762,43 @@ RED.palette.editor = (function() {
}
const updateStatusWidget = $('<button type="button" class="red-ui-footer-button red-ui-update-status"></button>');
let updateStatusWidgetPopover;
const updateStatusState = { moduleCount: 0 }
let updateAvailable = [];
function addUpdateInfoToStatusBar() {
updateStatusWidget.on("click", function (evt) {
RED.actions.invoke("core:manage-palette", {
view: "nodes",
filter: '"' + updateAvailable.join('", "') + '"'
});
});
RED.popover.tooltip(updateStatusWidget, function () {
const count = updateAvailable.length || 0;
return RED._("palette.editor.updateCount", { count: count });
updateStatusWidgetPopover = RED.popover.create({
target: updateStatusWidget,
trigger: "click",
interactive: true,
direction: "bottom",
content: function () {
const count = updateAvailable.length || 0;
const content = $('<div style="display: flex; flex-direction: column; gap: 5px;"></div>');
if (updateStatusState.version) {
$(`<a class='red-ui-button' href="https://github.com/node-red/node-red/releases/tag/${updateStatusState.version}" target="_blank">${RED._("telemetry.updateAvailableDesc", updateStatusState)}</a>`).appendTo(content)
}
if (count > 0) {
$(`<button type="button" class="red-ui-button"><i class="fa fa-cube"></i> ${RED._("palette.editor.updateCount", { count: count })}</button>`).on("click", function (evt) {
updateStatusWidgetPopover.close()
RED.actions.invoke("core:manage-palette", {
view: "nodes",
filter: '"' + updateAvailable.join('", "') + '"'
});
}).appendTo(content)
}
return content
},
delay: { show: 750, hide: 250 }
});
RED.statusBar.add({
id: "update",
id: "red-ui-status-package-update",
align: "right",
element: updateStatusWidget
});
updateStatus({ count: 0 });
updateStatus();
}
let pendingRefreshTimeout
@@ -1711,18 +1821,22 @@ RED.palette.editor = (function() {
}
}
}
updateStatus({ count: updateAvailable.length });
updateStatusState.moduleCount = updateAvailable.length;
updateStatus();
}, 200)
}
function updateStatus(opts) {
if (opts.count) {
RED.statusBar.show("update");
function updateStatus() {
if (updateStatusState.moduleCount || updateStatusState.version) {
updateStatusWidget.empty();
$('<span><i class="fa fa-cube"></i> ' + opts.count + '</span>').appendTo(updateStatusWidget);
let count = updateStatusState.moduleCount || 0;
if (updateStatusState.version) {
count ++
}
$(`<span><i class="fa fa-cube"></i> ${RED._("telemetry.updateAvailable", { count: count })}</span>`).appendTo(updateStatusWidget);
RED.statusBar.show("red-ui-status-package-update");
} else {
RED.statusBar.hide("update");
RED.statusBar.hide("red-ui-status-package-update");
}
}

View File

@@ -435,10 +435,15 @@ RED.tourGuide = (function() {
function listTour() {
return [
{
id: "4_1",
label: "4.1",
path: "./tours/welcome.js"
},
{
id: "4_0",
label: "4.0",
path: "./tours/welcome.js"
path: "./tours/4.0/welcome.js"
},
{
id: "3_1",

View File

@@ -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,

View File

@@ -144,6 +144,18 @@ RED.userSettings = (function() {
{setting:"view-node-show-label",label:"menu.label.showNodeLabelDefault",default: true, toggle:true}
]
},
{
title: "telemetry.label",
options: [
{
global: true,
setting: "telemetryEnabled",
label: "telemetry.settingsTitle",
description: "telemetry.settingsDescription",
toggle: true
},
]
},
{
title: "menu.label.other",
options: [
@@ -170,13 +182,20 @@ RED.userSettings = (function() {
var initialState;
if (opt.local) {
initialState = localStorage.getItem(opt.setting);
} else if (opt.global) {
initialState = RED.settings.get(opt.setting);
} else {
initialState = currentEditorSettings.view[opt.setting];
}
var row = $('<div class="red-ui-settings-row"></div>').appendTo(pane);
var input;
if (opt.toggle) {
input = $('<label for="user-settings-'+opt.setting+'"><input id="user-settings-'+opt.setting+'" type="checkbox"> '+RED._(opt.label)+'</label>').appendTo(row).find("input");
let label = RED._(opt.label)
if (opt.description) {
label = `<p>${label}</p>${RED._(opt.description)}`;
}
input = $('<input id="user-settings-'+opt.setting+'" type="checkbox">').appendTo(row)
$('<label for="user-settings-'+opt.setting+'">'+label+'</label>').appendTo(row)
input.prop('checked',initialState);
} else if (opt.options) {
$('<label for="user-settings-'+opt.setting+'">'+RED._(opt.label)+'</label>').appendTo(row);
@@ -210,6 +229,8 @@ RED.userSettings = (function() {
var opt = allSettings[id];
if (opt.local) {
localStorage.setItem(opt.setting,value);
} else if (opt.global) {
RED.settings.set(opt.setting, value)
} else {
var currentEditorSettings = RED.settings.get('editor') || {};
currentEditorSettings.view = currentEditorSettings.view || {};
@@ -238,7 +259,7 @@ RED.userSettings = (function() {
addPane({
id:'view',
title: RED._("menu.label.view.view"),
title: RED._("menu.label.settings"),
get: createViewPane,
close: function() {
viewSettings.forEach(function(section) {

View File

@@ -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)

View File

@@ -176,8 +176,8 @@ RED.view.tools = (function() {
}
nodes.forEach(function(n) {
var modified = false;
var oldValue = n.l === undefined?true:n.l;
var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true;
var showLabel = n._def.hasOwnProperty("showLabel") ? n._def.showLabel : true;
var oldValue = n.l === undefined ? showLabel : n.l;
if (labelShown) {
if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) {

View File

@@ -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
};
})();