Merge branch 'master' into sync-to-dev

pull/5504/head
Nick O'Leary 2026-02-26 10:20:31 +00:00
commit c9f86d18ff
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
18 changed files with 484 additions and 12257 deletions

View File

@ -33,6 +33,17 @@ Nodes
- Add ability to use pfx or p12 file for TLS connection settings option (#4907) @dceejay
#### 4.1.6: Maintenance Release
- Allow palette.theme to be set via theme plugin and include icons (#5500) @knolleary
- Ensure config sidebar tooltip handles html content (#5501) @knolleary
- Allow node-red integrator access to available updates (#5499) @Steve-Mcl
- Add frontend pre and post debug message hooks (#5495) @Steve-Mcl
- Fix: allow middle-click panning over links and ports (#5496) @lklivingstone
- Support ctrl key to select configuration nodes (#5486) @kazuhitoyokoi
- Add § as shortcut meta-key (#5482) @gorenje
- Update dependencies (#5502) @knolleary
#### 4.1.5: Maintenance Release
- chore: bump tar to 7.5.7 (#5472) @bryopsida

11985
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,12 +26,12 @@
}
],
"dependencies": {
"acorn": "8.15.0",
"acorn-walk": "8.3.4",
"ajv": "8.17.1",
"acorn": "8.16.0",
"acorn-walk": "8.3.5",
"ajv": "8.18.0",
"async-mutex": "0.5.0",
"basic-auth": "2.0.1",
"bcryptjs": "3.0.2",
"bcryptjs": "3.0.3",
"body-parser": "1.20.4",
"chalk": "^4.1.2",
"cheerio": "1.0.0-rc.10",
@ -62,7 +62,7 @@
"mime": "3.0.0",
"moment": "2.30.1",
"moment-timezone": "0.5.48",
"mqtt": "5.11.0",
"mqtt": "5.15.0",
"multer": "2.0.2",
"mustache": "4.2.0",
"node-red-admin": "^4.1.3",
@ -74,9 +74,9 @@
"passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2",
"raw-body": "3.0.0",
"rfdc": "^1.3.1",
"semver": "7.7.1",
"tar": "7.5.7",
"rfdc": "1.4.1",
"semver": "7.7.4",
"tar": "7.5.9",
"tough-cookie": "5.1.2",
"uglify-js": "3.19.3",
"uuid": "9.0.1",
@ -111,11 +111,11 @@
"jquery-i18next": "1.2.1",
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"marked": "4.3.0",
"mermaid": "11.9.0",
"mermaid": "11.12.3",
"minami": "1.2.3",
"mocha": "9.2.2",
"node-red-node-test-helper": "^0.3.3",
"nodemon": "3.1.9",
"nodemon": "3.1.14",
"proxy": "^1.0.2",
"sass": "1.62.1",
"should": "13.2.3",

View File

@ -53,10 +53,10 @@ module.exports = {
var opts = {
user: req.user
}
runtimeAPI.settings.getRuntimeSettings(opts).then(function(result) {
runtimeAPI.settings.getRuntimeSettings(opts).then(async function(result) {
if (!settings.disableEditor) {
result.editorTheme = result.editorTheme||{};
var themeSettings = theme.settings();
const themeSettings = await theme.settings();
if (themeSettings) {
// result.editorTheme may already exist with the palette
// disabled. Need to merge that into the receive settings

View File

@ -42,7 +42,13 @@ var defaultContext = {
var settings;
var theme = null;
/**
* themeContext is an object passed to the mustache template to generate the editor index.html.
*/
var themeContext = clone(defaultContext);
/**
* themeSettings is an object passed to the editor client as the "editorTheme" property of the settings object
*/
var themeSettings = null;
var activeTheme = null;
@ -91,6 +97,119 @@ function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) {
return result
}
/**
* Check if a theme is enabled and load its settings.
* This is done lazily as it has to happen after the plugins have been loaded, but before the editor is served.
*/
async function loadThemePlugin () {
if (activeTheme && !activeThemeInitialised) {
const themePlugin = await runtimeAPI.plugins.getPlugin({
id:activeTheme
});
if (themePlugin) {
if (themePlugin.css) {
const cssFiles = serveFilesFromTheme(
themePlugin.css,
themeApp,
"/css/",
themePlugin.path
);
themeContext.page.css = cssFiles.concat(themeContext.page.css || [])
// Mutating `theme` is not ideal, but currently necessary as debug (packages/node_modules/@node-red/nodes/core/common/21-debug.js)
// accesses RED.settings.editorTheme.page._.css directly to apply theme to the debug pop-out window.
theme.page = theme.page || {_:{}}
theme.page._.css = cssFiles.concat(theme.page._.css || [])
}
if (themePlugin.scripts) {
const scriptFiles = serveFilesFromTheme(
themePlugin.scripts,
themeApp,
"/scripts/",
themePlugin.path
)
themeContext.page.scripts = scriptFiles.concat(themeContext.page.scripts || [])
theme.page = theme.page || {_:{}}
theme.page._.scripts = scriptFiles.concat(theme.page._.scripts || [])
}
// check and load page settings from theme
if (themePlugin.page) {
if (themePlugin.page.favicon && !theme.page.favicon) {
const result = serveFilesFromTheme(
[themePlugin.page.favicon],
themeApp,
"/",
themePlugin.path
)
if(result && result.length > 0) {
// update themeContext page favicon
themeContext.page.favicon = result[0]
}
}
if (themePlugin.page.tabicon && themePlugin.page.tabicon.icon && !theme.page.tabicon) {
const result = serveFilesFromTheme(
[themePlugin.page.tabicon.icon],
themeApp,
"/page/",
themePlugin.path
)
if(result && result.length > 0) {
// update themeContext page tabicon
themeContext.page.tabicon.icon = result[0]
themeContext.page.tabicon.colour = themeContext.page.tabicon.colour || themeContext.page.tabicon.colour
}
}
// if the plugin has a title AND the users settings.js does NOT
if (themePlugin.page.title && !theme.page.title) {
themeContext.page.title = themePlugin.page.title || themeContext.page.title
}
}
// check and load header settings from theme
if (themePlugin.header) {
if (themePlugin.header.image && !theme.header.image) {
const result = serveFilesFromTheme(
[themePlugin.header.image],
themeApp,
"/header/",
themePlugin.path
)
if(result && result.length > 0) {
// update themeContext header image
themeContext.header.image = result[0]
}
}
// if the plugin has a title AND the users settings.js does NOT have a title
if (themePlugin.header.title && !theme.header.title) {
themeContext.header.title = themePlugin.header.title || themeContext.header.title
}
// if the plugin has a header url AND the users settings.js does NOT
if (themePlugin.header.url && !theme.header.url) {
themeContext.header.url = themePlugin.header.url || themeContext.header.url
}
}
if (Array.isArray(themePlugin.palette?.theme)) {
themeSettings.palette = themeSettings.palette || {};
themeSettings.palette.theme = themePlugin.palette.theme;
// The theme is providing its own palette theme. It *might* include icons that need namespacing
// to the theme plugin module.
themePlugin.palette.theme.forEach(themeRule => {
if (themeRule.icon && themeRule.icon.indexOf("/") === -1) {
themeRule.icon = `${themePlugin.module}/${themeRule.icon}`;
}
})
}
// These settings are not exposed under `editorTheme`, so we don't have a merge strategy for them
// If they're defined in the theme plugin, they replace any settings.js values.
// But, this direct manipulation of `theme` is not ideal and relies on mutating a passed-in object
theme.codeEditor = theme.codeEditor || {}
theme.codeEditor.options = Object.assign({}, themePlugin.monacoOptions, theme.codeEditor.options);
theme.mermaid = Object.assign({}, themePlugin.mermaid, theme.mermaid)
}
activeThemeInitialised = true;
}
}
module.exports = {
init: function(_settings, _runtimeAPI) {
settings = _settings;
@ -232,6 +351,7 @@ module.exports = {
res.json(themeContext);
})
// Copy the settings that need passing to the editor into themeSettings.
if (theme.hasOwnProperty("menu")) {
themeSettings.menu = theme.menu;
}
@ -263,104 +383,11 @@ module.exports = {
return themeApp;
},
context: async function() {
if (activeTheme && !activeThemeInitialised) {
const themePlugin = await runtimeAPI.plugins.getPlugin({
id:activeTheme
});
if (themePlugin) {
if (themePlugin.css) {
const cssFiles = serveFilesFromTheme(
themePlugin.css,
themeApp,
"/css/",
themePlugin.path
);
themeContext.page.css = cssFiles.concat(themeContext.page.css || [])
theme.page = theme.page || {_:{}}
theme.page._.css = cssFiles.concat(theme.page._.css || [])
}
if (themePlugin.scripts) {
const scriptFiles = serveFilesFromTheme(
themePlugin.scripts,
themeApp,
"/scripts/",
themePlugin.path
)
themeContext.page.scripts = scriptFiles.concat(themeContext.page.scripts || [])
theme.page = theme.page || {_:{}}
theme.page._.scripts = scriptFiles.concat(theme.page._.scripts || [])
}
// check and load page settings from theme
if (themePlugin.page) {
if (themePlugin.page.favicon && !theme.page.favicon) {
const result = serveFilesFromTheme(
[themePlugin.page.favicon],
themeApp,
"/",
themePlugin.path
)
if(result && result.length > 0) {
// update themeContext page favicon
themeContext.page.favicon = result[0]
theme.page = theme.page || {_:{}}
theme.page._.favicon = result[0]
}
}
if (themePlugin.page.tabicon && themePlugin.page.tabicon.icon && !theme.page.tabicon) {
const result = serveFilesFromTheme(
[themePlugin.page.tabicon.icon],
themeApp,
"/page/",
themePlugin.path
)
if(result && result.length > 0) {
// update themeContext page tabicon
themeContext.page.tabicon.icon = result[0]
themeContext.page.tabicon.colour = themeContext.page.tabicon.colour || themeContext.page.tabicon.colour
theme.page = theme.page || {_:{}}
theme.page._.tabicon = theme.page._.tabicon || {}
theme.page._.tabicon.icon = themeContext.page.tabicon.icon
theme.page._.tabicon.colour = themeContext.page.tabicon.colour
}
}
// if the plugin has a title AND the users settings.js does NOT
if (themePlugin.page.title && !theme.page.title) {
themeContext.page.title = themePlugin.page.title || themeContext.page.title
}
}
// check and load header settings from theme
if (themePlugin.header) {
if (themePlugin.header.image && !theme.header.image) {
const result = serveFilesFromTheme(
[themePlugin.header.image],
themeApp,
"/header/",
themePlugin.path
)
if(result && result.length > 0) {
// update themeContext header image
themeContext.header.image = result[0]
}
}
// if the plugin has a title AND the users settings.js does NOT have a title
if (themePlugin.header.title && !theme.header.title) {
themeContext.header.title = themePlugin.header.title || themeContext.header.title
}
// if the plugin has a header url AND the users settings.js does NOT
if (themePlugin.header.url && !theme.header.url) {
themeContext.header.url = themePlugin.header.url || themeContext.header.url
}
}
theme.codeEditor = theme.codeEditor || {}
theme.codeEditor.options = Object.assign({}, themePlugin.monacoOptions, theme.codeEditor.options);
theme.mermaid = Object.assign({}, themePlugin.mermaid, theme.mermaid)
}
activeThemeInitialised = true;
}
await loadThemePlugin();
return themeContext;
},
settings: function() {
settings: async function() {
await loadThemePlugin();
return themeSettings;
},
serveFile: function(baseUrl,file) {

View File

@ -18,7 +18,7 @@
"dependencies": {
"@node-red/util": "5.0.0-beta.2",
"@node-red/editor-client": "5.0.0-beta.2",
"bcryptjs": "3.0.2",
"bcryptjs": "3.0.3",
"body-parser": "1.20.4",
"clone": "2.1.2",
"cors": "2.8.5",

View File

@ -1,135 +1,206 @@
RED.hooks = (function() {
RED.hooks = (function () {
// At the time of writing this PR, VALID_HOOKS were not enforced. There may be a good reason for this
// so the below flag has been added to permit this behaviour. If desired, this can be set to false to
// enforce that only known hooks can be added/triggered.
const knownHooksOnly = false
var VALID_HOOKS = [
const VALID_HOOKS = Object.freeze({
viewRemoveNode: true,
viewAddNode: true,
viewRemovePort: true,
viewAddPort: true,
viewRedrawNode: true,
debugPreProcessMessage: true,
debugPostProcessMessage: true
})
]
/**
* @typedef {keyof typeof VALID_HOOKS} HookId - A string literal type representing a hook identifier (sans label).
*
* @typedef {Object} HookItem - An item in the linked list of hooks for a given HookId
* @property {function} cb - The callback function to be called when the hook is triggered
* @property {HookItem|null} previousHook - The previous hook in the linked list
* @property {HookItem|null} nextHook - The next hook in the linked list
* @property {boolean} removed - Flag indicating if the hook has been removed
*
* @typedef {Record<HookId, HookItem|null>} Hooks - A mapping of HookIds to the head of their linked list of HookItems
*/
var hooks = { }
var labelledHooks = { }
/** @type {Hooks} - A mapping of HookIds to the head of their linked list of HookItems */
let hooks = {}
/** @type {Record<string, Record<HookId, HookItem>>} - A mapping of labels to their hooks */
let labelledHooks = {}
function add(hookId, callback) {
var parts = hookId.split(".");
var id = parts[0], label = parts[1];
const { label, id } = parseLabelledHook(hookId)
// if (VALID_HOOKS.indexOf(id) === -1) {
// throw new Error("Invalid hook '"+id+"'");
// }
if (label && labelledHooks[label] && labelledHooks[label][id]) {
throw new Error("Hook "+hookId+" already registered")
if (knownHooksOnly && !isKnownHook(id)) {
throw new Error("Invalid hook '" + id + "'")
}
if (label && labelledHooks[label] && labelledHooks[label][id]) {
throw new Error("Hook " + hookId + " already registered")
}
if (typeof callback !== "function") {
throw new Error("Invalid hook '" + hookId + "'. Callback must be a function")
}
var hookItem = {cb:callback, previousHook: null, nextHook: null }
var tailItem = hooks[id];
/** @type {HookItem} */
const hookItem = { cb: callback, previousHook: null, nextHook: null }
let tailItem = hooks[id]
if (tailItem === undefined) {
hooks[id] = hookItem;
hooks[id] = hookItem
} else {
while(tailItem.nextHook !== null) {
while (tailItem.nextHook !== null) {
tailItem = tailItem.nextHook
}
tailItem.nextHook = hookItem;
hookItem.previousHook = tailItem;
tailItem.nextHook = hookItem
hookItem.previousHook = tailItem
}
if (label) {
labelledHooks[label] = labelledHooks[label]||{};
labelledHooks[label][id] = hookItem;
labelledHooks[label] = labelledHooks[label] || {}
labelledHooks[label][id] = hookItem
}
}
function remove(hookId) {
var parts = hookId.split(".");
var id = parts[0], label = parts[1];
if ( !label) {
throw new Error("Cannot remove hook without label: "+hookId)
const { label, id } = parseLabelledHook(hookId)
if (!label) {
throw new Error("Cannot remove hook without label: " + hookId)
}
if (labelledHooks[label]) {
if (id === "*") {
// Remove all hooks for this label
var hookList = Object.keys(labelledHooks[label]);
for (var i=0;i<hookList.length;i++) {
removeHook(hookList[i],labelledHooks[label][hookList[i]])
const hookList = Object.keys(labelledHooks[label])
for (let i = 0; i < hookList.length; i++) {
removeHook(hookList[i], labelledHooks[label][hookList[i]])
}
delete labelledHooks[label];
delete labelledHooks[label]
} else if (labelledHooks[label][id]) {
removeHook(id,labelledHooks[label][id])
delete labelledHooks[label][id];
if (Object.keys(labelledHooks[label]).length === 0){
delete labelledHooks[label];
removeHook(id, labelledHooks[label][id])
delete labelledHooks[label][id]
if (Object.keys(labelledHooks[label]).length === 0) {
delete labelledHooks[label]
}
}
}
}
function removeHook(id,hookItem) {
var previousHook = hookItem.previousHook;
var nextHook = hookItem.nextHook;
/**
* Remove a hook from the linked list of hooks for a given id
* @param {HookId} id
* @param {HookItem} hookItem
* @private
*/
function removeHook(id, hookItem) {
let previousHook = hookItem.previousHook
let nextHook = hookItem.nextHook
if (previousHook) {
previousHook.nextHook = nextHook;
previousHook.nextHook = nextHook
} else {
hooks[id] = nextHook;
hooks[id] = nextHook
}
if (nextHook) {
nextHook.previousHook = previousHook;
nextHook.previousHook = previousHook
}
hookItem.removed = true;
hookItem.removed = true
if (!previousHook && !nextHook) {
delete hooks[id];
delete hooks[id]
}
}
function trigger(hookId, payload, done) {
var hookItem = hooks[hookId];
/**
* Trigger a hook, calling all registered callbacks in sequence.
* If any callback returns false, the flow is halted and no further hooks are called.
* @param {HookId} id The id of the hook to trigger (should not include a label - e.g. "viewAddNode", not "viewAddNode.myLabel")
* @param {*} payload The payload to be passed to each hook callback
* @param {function(?Error=):void} [done] Optional callback. If not provided, a Promise will be returned.
* @return {Promise|undefined} Returns a Promise if the done callback is not provided, otherwise undefined
*/
function trigger(id, payload, done) {
let hookItem = hooks[id]
if (!hookItem) {
if (done) {
done();
done()
return
} else {
return Promise.resolve()
}
return;
}
if (!done) {
return new Promise((resolve, reject) => {
invokeStack(hookItem, payload, function (err) {
if (err !== undefined && err !== false) {
if (!(err instanceof Error)) {
err = new Error(err)
}
err.hook = id
reject(err)
} else {
resolve(err)
}
})
})
} else {
invokeStack(hookItem, payload, done)
}
}
/**
* @private
*/
function invokeStack(hookItem, payload, done) {
function callNextHook(err) {
if (!hookItem || err) {
if (done) { done(err) }
return err;
done(err)
return
}
if (hookItem.removed) {
hookItem = hookItem.nextHook;
return callNextHook();
hookItem = hookItem.nextHook
callNextHook()
return
}
var callback = hookItem.cb;
const callback = hookItem.cb
if (callback.length === 1) {
try {
let result = callback(payload);
let result = callback(payload)
if (result === false) {
// Halting the flow
if (done) { done(false) }
return result;
done(false)
return
}
hookItem = hookItem.nextHook;
return callNextHook();
} catch(e) {
console.warn(e);
if (done) { done(e);}
return e;
if (result && typeof result.then === 'function') {
result.then(handleResolve, callNextHook)
return
}
hookItem = hookItem.nextHook
callNextHook()
} catch (e) {
done(e)
return
}
} else {
// There is a done callback
try {
callback(payload,function(result) {
if (result === undefined) {
hookItem = hookItem.nextHook;
callNextHook();
} else {
if (done) { done(result)}
}
})
} catch(e) {
console.warn(e);
if (done) { done(e) }
return e;
callback(payload, handleResolve)
} catch (e) {
done(e)
return
}
}
}
return callNextHook();
function handleResolve(result) {
if (result === undefined) {
hookItem = hookItem.nextHook
callNextHook()
} else {
done(result)
}
}
callNextHook()
}
function clear() {
@ -137,20 +208,48 @@ RED.hooks = (function() {
labelledHooks = {}
}
/**
* Check if a hook with the given id exists
* @param {string} hookId The hook identifier, which may include a label (e.g. "viewAddNode.myLabel")
* @returns {boolean}
*/
function has(hookId) {
var parts = hookId.split(".");
var id = parts[0], label = parts[1];
const { label, id } = parseLabelledHook(hookId)
if (label) {
return !!(labelledHooks[label] && labelledHooks[label][id])
}
return !!hooks[id]
}
return {
has: has,
clear: clear,
add: add,
remove: remove,
trigger: trigger
function isKnownHook(hookId) {
const { id } = parseLabelledHook(hookId)
return !!VALID_HOOKS[id]
}
})();
/**
* Split a hook identifier into its id and label components.
* @param {*} hookId A hook identifier, which may include a label (e.g. "viewAddNode.myLabel")
* @returns {{label: string, id: HookId}}
* @private
*/
function parseLabelledHook(hookId) {
if (typeof hookId !== "string") {
return { label: '', id: '' }
}
const parts = hookId.split(".")
const id = parts[0]
const label = parts[1]
return { label, id }
}
VALID_HOOKS['all'] = true // Special wildcard to allow hooks to indicate they should be triggered for all ids
return {
has,
clear,
add,
remove,
trigger,
isKnownHook
}
})()

View File

@ -43,6 +43,7 @@ RED.keyboard = (function() {
"-":189,
".":190,
"/":191,
"§":192, // <- top left key MacOS
"\\":220,
"'":222,
"?":191, // <- QWERTY specific

View File

@ -40,6 +40,12 @@ RED.palette.editor = (function() {
// Install tab - search input
let searchInput;
// Core and Package Updates
/** @type {Array<{package: string, current: string, available: string}>} */
const moduleUpdates = []
const updateStatusState = { version: null, moduleCount: 0 }
const SMALL_CATALOGUE_SIZE = 40
const typesInUse = {};
@ -1825,8 +1831,6 @@ 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() {
updateStatusWidgetPopover = RED.popover.create({
@ -1835,7 +1839,7 @@ RED.palette.editor = (function() {
interactive: true,
direction: "bottom",
content: function () {
const count = updateAvailable.length || 0;
const count = moduleUpdates.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)
@ -1845,7 +1849,7 @@ RED.palette.editor = (function() {
updateStatusWidgetPopover.close()
RED.actions.invoke("core:manage-palette", {
view: "nodes",
filter: '"' + updateAvailable.join('", "') + '"'
filter: '"' + moduleUpdates.map(u => u.package).join('", "') + '"'
});
}).appendTo(content)
}
@ -1867,7 +1871,7 @@ RED.palette.editor = (function() {
function refreshUpdateStatus() {
clearTimeout(pendingRefreshTimeout)
pendingRefreshTimeout = setTimeout(() => {
updateAvailable = [];
moduleUpdates.length = 0
for (const module of Object.keys(nodeEntries)) {
if (loadedIndex.hasOwnProperty(module)) {
const moduleInfo = nodeEntries[module].info;
@ -1875,35 +1879,51 @@ RED.palette.editor = (function() {
// Module updated
continue;
}
const current = moduleInfo.version
const latest = loadedIndex[module].version
if (updateAllowed &&
semVerCompare(loadedIndex[module].version, moduleInfo.version) > 0 &&
semVerCompare(latest, current) > 0 &&
RED.utils.checkModuleAllowed(module, null, updateAllowList, updateDenyList)
) {
updateAvailable.push(module);
moduleUpdates.push({ package: module, current, latest })
}
}
}
updateStatusState.moduleCount = updateAvailable.length;
updateStatusState.moduleCount = moduleUpdates.length
updateStatus();
}, 200)
}
function updateStatus() {
if (updateStatusState.moduleCount || updateStatusState.version) {
const updates = RED.palette.editor.getAvailableUpdates()
if (updates.count > 0) {
updateStatusWidget.empty();
let count = updateStatusState.moduleCount || 0;
if (updateStatusState.version) {
count ++
}
$(`<span><i class="fa fa-cube"></i> ${RED._("telemetry.updateAvailable", { count: count })}</span>`).appendTo(updateStatusWidget);
$(`<span><i class="fa fa-cube"></i> ${RED._("telemetry.updateAvailable", { count: updates.count })}</span>`).appendTo(updateStatusWidget);
RED.statusBar.show("red-ui-status-package-update");
} else {
RED.statusBar.hide("red-ui-status-package-update");
}
RED.events.emit("registry:updates-available", updates)
}
function getAvailableUpdates () {
const palette = [...moduleUpdates]
let core = null
let count = palette.length
if (updateStatusState.version) {
core = { current: RED.settings.version, latest: updateStatusState.version }
count ++
}
return {
count,
core,
palette
}
}
return {
init: init,
install: install
init,
install,
getAvailableUpdates
}
})();

View File

@ -210,7 +210,7 @@ RED.sidebar.config = (function() {
nodeDiv.addClass("red-ui-palette-node-config-invalid");
RED.popover.tooltip(nodeDivAnnotations, function () {
if (node.validationErrors && node.validationErrors.length > 0) {
return RED._("editor.errors.invalidProperties") + "<br> - " + node.validationErrors.join("<br> - ");
return $('<span>' + RED._("editor.errors.invalidProperties") + "<br> - " + node.validationErrors.join("<br> - ") + '</span>');
}
})
}
@ -218,7 +218,7 @@ RED.sidebar.config = (function() {
nodeDiv.on('click',function(e) {
e.stopPropagation();
RED.view.select(false);
if (e.metaKey) {
if (e.metaKey || e.ctrlKey) {
$(this).toggleClass("selected");
} else {
$(content).find(".red-ui-palette-node").removeClass("selected");

View File

@ -1110,20 +1110,36 @@ RED.utils = (function() {
return result;
}
/**
* Get the default icon for a given node based on its definition.
* @param {*} def
* @param {*} node
* @returns
*/
function getDefaultNodeIcon(def,node) {
def = def || {};
var icon_url;
if (node && node.type === "subflow") {
icon_url = "node-red/subflow.svg";
} else if (typeof def.icon === "function") {
try {
icon_url = def.icon.call(node);
} catch(err) {
console.log("Definition error: "+def.type+".icon",err);
icon_url = "arrow-in.svg";
}
} else {
icon_url = def.icon;
let themeRule = nodeIconCache[def.type]
if (themeRule === undefined) {
// If undefined, we've not checked the theme yet
nodeIconCache[def.type] = getThemeOverrideForNode(def, 'icon') || null;
themeRule = nodeIconCache[def.type];
}
if (themeRule) {
icon_url = themeRule.icon;
} else if (typeof def.icon === "function") {
try {
icon_url = def.icon.call(node);
} catch(err) {
console.log("Definition error: "+def.type+".icon",err);
icon_url = "arrow-in.svg";
}
} else {
icon_url = def.icon;
}
}
var iconPath = separateIconPath(icon_url);
@ -1249,48 +1265,60 @@ RED.utils = (function() {
return label
}
var nodeColorCache = {};
let nodeColorCache = {};
let nodeIconCache = {}
function clearNodeColorCache() {
nodeColorCache = {};
}
function getNodeColor(type, def) {
def = def || {};
var result = def.color;
var paletteTheme = RED.settings.theme('palette.theme') || [];
/**
* Checks if there is a theme override for the given node definition and property
* @param {*} def node definition
* @param {*} property either 'color' or 'icon'
* @returns the theme override value if there is a match, otherwise null
*/
function getThemeOverrideForNode(def, property) {
const paletteTheme = RED.settings.theme('palette.theme') || [];
if (paletteTheme.length > 0) {
if (!nodeColorCache.hasOwnProperty(type)) {
nodeColorCache[type] = def.color;
var l = paletteTheme.length;
for (var i = 0; i < l; i++ ){
var themeRule = paletteTheme[i];
if (themeRule.hasOwnProperty('category')) {
if (!themeRule.hasOwnProperty('_category')) {
themeRule._category = new RegExp(themeRule.category);
}
if (!themeRule._category.test(def.category)) {
continue;
}
for (let i = 0; i < paletteTheme.length; i++ ){
const themeRule = paletteTheme[i];
if (themeRule.hasOwnProperty('category')) {
if (!themeRule.hasOwnProperty('_category')) {
themeRule._category = new RegExp(themeRule.category);
}
if (themeRule.hasOwnProperty('type')) {
if (!themeRule.hasOwnProperty('_type')) {
themeRule._type = new RegExp(themeRule.type);
}
if (!themeRule._type.test(type)) {
continue;
}
if (!themeRule._category.test(def.category)) {
continue;
}
nodeColorCache[type] = themeRule.color || def.color;
break;
}
if (themeRule.hasOwnProperty('type')) {
if (!themeRule.hasOwnProperty('_type')) {
themeRule._type = new RegExp(themeRule.type);
}
if (!themeRule._type.test(def.type)) {
continue;
}
}
// We have found a rule that matches - now see if it provides the requested property
if (themeRule.hasOwnProperty(property)) {
return themeRule;
}
}
result = nodeColorCache[type];
}
if (result) {
return result;
} else {
return "#ddd";
return null;
}
function getNodeColor(type, def) {
def = def || {};
if (!nodeColorCache.hasOwnProperty(type)) {
const paletteTheme = RED.settings.theme('palette.theme') || [];
if (paletteTheme.length > 0) {
const themeRule = getThemeOverrideForNode(def, 'color');
nodeColorCache[type] = themeRule?.color || def.color;
} else {
nodeColorCache[type] = def.color;
}
}
return nodeColorCache[type] || "#ddd";
}
function addSpinnerOverlay(container,contain) {

View File

@ -4032,7 +4032,7 @@ RED.view = (function() {
clearSuggestedFlow();
RED.contextMenu.hide();
evt = evt || d3.event;
if (evt === 1) {
if (evt.button !== 0) {
return;
}
if (mouse_mode === RED.state.SELECTING_NODE) {
@ -4897,7 +4897,7 @@ RED.view = (function() {
d3.event.stopPropagation();
return;
}
if (d3.event.button === 2) {
if (d3.event.button !== 0) {
return
}
mousedown_link = d;

View File

@ -433,12 +433,28 @@ RED.debug = (function() {
if (o) { stack.push(o); }
if (!busy && (stack.length > 0)) {
busy = true;
processDebugMessage(stack.shift());
setTimeout(function() {
busy = false;
handleDebugMessage();
}, 15); // every 15mS = 66 times a second
if (stack.length > numMessages) { stack = stack.splice(-numMessages); }
const message = stack.shift()
// call any preDebugLog hooks, allowing them to modify the message or block it from being displayed
RED.hooks.trigger('debugPreProcessMessage', { message }).then(result => {
if (result === false) {
return false; // A hook returned false - halt processing of this message
}
return processDebugMessage(message);
}).then(processArtifacts => {
if (processArtifacts === false) {
return false; // A hook returned false - halt processing of this message
}
const { message, element, payload } = processArtifacts || {};
return RED.hooks.trigger('debugPostProcessMessage', { message, element, payload });
}).catch(err => {
console.error("Error in debug process message hooks", err);
}).finally(() => {
setTimeout(function() {
busy = false;
handleDebugMessage();
}, 15); // every 15mS = 66 times a second
if (stack.length > numMessages) { stack = stack.splice(-numMessages); }
})
}
} else {
debugPausedMessageCount++
@ -564,10 +580,13 @@ RED.debug = (function() {
sourceId: sourceNode && sourceNode.id,
rootPath: path,
nodeSelector: config.messageSourceClick,
enablePinning: true
enablePinning: true,
tools: o.tools // permit preDebugLog hooks to add extra tools to the <debugMessage> element
});
// Do this in a separate step so the element functions aren't stripped
debugMessage.appendTo(el);
// add the meta row tools container, even if there are no tools, so that the postProcessDebugMessage hook can add tools
const tools = $('<span class="red-ui-debug-msg-tools button-group"></span>').appendTo(metaRow)
// NOTE: relying on function error to have a "type" that all other msgs don't
if (o.hasOwnProperty("type") && (o.type === "function")) {
var errorLvlType = 'error';
@ -579,7 +598,6 @@ RED.debug = (function() {
msg.addClass('red-ui-debug-msg-level-' + errorLvl);
$('<span class="red-ui-debug-msg-topic">function : (' + errorLvlType + ')</span>').appendTo(metaRow);
} else {
var tools = $('<span class="red-ui-debug-msg-tools button-group"></span>').appendTo(metaRow);
var filterMessage = $('<button class="red-ui-button red-ui-button-small"><i class="fa fa-caret-down"></i></button>').appendTo(tools);
filterMessage.on("click", function(e) {
e.preventDefault();
@ -635,6 +653,14 @@ RED.debug = (function() {
if (atBottom) {
messageList.scrollTop(sbc.scrollHeight);
}
// return artifacts to permit postProcessDebugMessage hooks to modify the message element, access the
// processed payload or otherwise modify the message after it has been generated.
return {
message: o, // original debug message object, useful for any hook that might have tagged additional info onto it
element: msg, // the top-level element for this debug message
payload // the reconstructed debug message
}
}
function clearMessageList(clearFilter, filteredOnly) {

View File

@ -15,9 +15,9 @@
}
],
"dependencies": {
"acorn": "8.15.0",
"acorn-walk": "8.3.4",
"ajv": "8.17.1",
"acorn": "8.16.0",
"acorn-walk": "8.3.5",
"ajv": "8.18.0",
"body-parser": "1.20.4",
"cheerio": "1.0.0-rc.10",
"content-type": "1.0.5",
@ -35,7 +35,7 @@
"is-utf8": "0.2.1",
"js-yaml": "4.1.1",
"media-typer": "1.1.0",
"mqtt": "5.11.0",
"mqtt": "5.15.0",
"multer": "2.0.2",
"mustache": "4.2.0",
"node-watch": "0.7.4",

View File

@ -19,8 +19,8 @@
"@node-red/util": "5.0.0-beta.2",
"clone": "2.1.2",
"fs-extra": "11.3.0",
"semver": "7.7.1",
"tar": "7.5.7",
"semver": "7.7.4",
"tar": "7.5.9",
"uglify-js": "3.19.3"
}
}

View File

@ -25,7 +25,7 @@
"fs-extra": "11.3.0",
"got": "12.6.1",
"json-stringify-safe": "5.0.1",
"rfdc": "^1.3.1",
"semver": "7.7.1"
"rfdc": "1.4.1",
"semver": "7.7.4"
}
}

View File

@ -36,13 +36,13 @@
"@node-red/util": "5.0.0-beta.2",
"@node-red/nodes": "5.0.0-beta.2",
"basic-auth": "2.0.1",
"bcryptjs": "3.0.2",
"bcryptjs": "3.0.3",
"cors": "2.8.5",
"express": "4.22.1",
"fs-extra": "11.3.0",
"node-red-admin": "^4.1.3",
"nopt": "5.0.0",
"semver": "7.7.1"
"semver": "7.7.4"
},
"optionalDependencies": {
"@node-rs/bcrypt": "1.10.7"

View File

@ -53,7 +53,7 @@ describe("api/editor/theme", function () {
context.asset.should.have.a.property("main", "red/main.min.js");
context.asset.should.have.a.property("vendorMonaco", "vendor/monaco/monaco-bootstrap.js");
should.not.exist(theme.settings());
should.not.exist(await theme.settings());
});
it("uses non-minified js files when in dev mode", async function () {
@ -158,7 +158,7 @@ describe("api/editor/theme", function () {
context.should.have.a.property("login");
context.login.should.have.a.property("image", "theme/login/image");
var settings = theme.settings();
var settings = await theme.settings();
settings.should.have.a.property("deployButton");
settings.deployButton.should.have.a.property("type", "simple");
settings.deployButton.should.have.a.property("label", "Save");
@ -199,7 +199,7 @@ describe("api/editor/theme", function () {
});
it("test explicit userMenu set to true in theme setting", function () {
it("test explicit userMenu set to true in theme setting", async function () {
theme.init({
editorTheme: {
userMenu: true,
@ -208,7 +208,7 @@ describe("api/editor/theme", function () {
theme.app();
var settings = theme.settings();
var settings = await theme.settings();
settings.should.have.a.property("userMenu");
settings.userMenu.should.be.eql(true);