mirror of https://github.com/node-red/node-red.git
Merge branch 'master' into sync-to-dev
commit
c9f86d18ff
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ RED.keyboard = (function() {
|
|||
"-":189,
|
||||
".":190,
|
||||
"/":191,
|
||||
"§":192, // <- top left key MacOS
|
||||
"\\":220,
|
||||
"'":222,
|
||||
"?":191, // <- QWERTY specific
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue