mirror of https://github.com/node-red/node-red.git
Allow palette.theme to be set via theme plugin and include icons
parent
3e2e30f4dd
commit
65d68d27ca
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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