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 = {
|
var opts = {
|
||||||
user: req.user
|
user: req.user
|
||||||
}
|
}
|
||||||
runtimeAPI.settings.getRuntimeSettings(opts).then(function(result) {
|
runtimeAPI.settings.getRuntimeSettings(opts).then(async function(result) {
|
||||||
if (!settings.disableEditor) {
|
if (!settings.disableEditor) {
|
||||||
result.editorTheme = result.editorTheme||{};
|
result.editorTheme = result.editorTheme||{};
|
||||||
var themeSettings = theme.settings();
|
const themeSettings = await theme.settings();
|
||||||
if (themeSettings) {
|
if (themeSettings) {
|
||||||
// result.editorTheme may already exist with the palette
|
// result.editorTheme may already exist with the palette
|
||||||
// disabled. Need to merge that into the receive settings
|
// disabled. Need to merge that into the receive settings
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,13 @@ var defaultContext = {
|
||||||
var settings;
|
var settings;
|
||||||
|
|
||||||
var theme = null;
|
var theme = null;
|
||||||
|
/**
|
||||||
|
* themeContext is an object passed to the mustache template to generate the editor index.html.
|
||||||
|
*/
|
||||||
var themeContext = clone(defaultContext);
|
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 themeSettings = null;
|
||||||
|
|
||||||
var activeTheme = null;
|
var activeTheme = null;
|
||||||
|
|
@ -91,6 +97,119 @@ function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) {
|
||||||
return result
|
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 = {
|
module.exports = {
|
||||||
init: function(_settings, _runtimeAPI) {
|
init: function(_settings, _runtimeAPI) {
|
||||||
settings = _settings;
|
settings = _settings;
|
||||||
|
|
@ -232,6 +351,7 @@ module.exports = {
|
||||||
res.json(themeContext);
|
res.json(themeContext);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Copy the settings that need passing to the editor into themeSettings.
|
||||||
if (theme.hasOwnProperty("menu")) {
|
if (theme.hasOwnProperty("menu")) {
|
||||||
themeSettings.menu = theme.menu;
|
themeSettings.menu = theme.menu;
|
||||||
}
|
}
|
||||||
|
|
@ -263,104 +383,11 @@ module.exports = {
|
||||||
return themeApp;
|
return themeApp;
|
||||||
},
|
},
|
||||||
context: async function() {
|
context: async function() {
|
||||||
if (activeTheme && !activeThemeInitialised) {
|
await loadThemePlugin();
|
||||||
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;
|
|
||||||
}
|
|
||||||
return themeContext;
|
return themeContext;
|
||||||
},
|
},
|
||||||
settings: function() {
|
settings: async function() {
|
||||||
|
await loadThemePlugin();
|
||||||
return themeSettings;
|
return themeSettings;
|
||||||
},
|
},
|
||||||
serveFile: function(baseUrl,file) {
|
serveFile: function(baseUrl,file) {
|
||||||
|
|
|
||||||
|
|
@ -1110,20 +1110,36 @@ RED.utils = (function() {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default icon for a given node based on its definition.
|
||||||
|
* @param {*} def
|
||||||
|
* @param {*} node
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
function getDefaultNodeIcon(def,node) {
|
function getDefaultNodeIcon(def,node) {
|
||||||
def = def || {};
|
def = def || {};
|
||||||
var icon_url;
|
var icon_url;
|
||||||
if (node && node.type === "subflow") {
|
if (node && node.type === "subflow") {
|
||||||
icon_url = "node-red/subflow.svg";
|
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 {
|
} 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);
|
var iconPath = separateIconPath(icon_url);
|
||||||
|
|
@ -1249,48 +1265,60 @@ RED.utils = (function() {
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
var nodeColorCache = {};
|
let nodeColorCache = {};
|
||||||
|
let nodeIconCache = {}
|
||||||
function clearNodeColorCache() {
|
function clearNodeColorCache() {
|
||||||
nodeColorCache = {};
|
nodeColorCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeColor(type, def) {
|
/**
|
||||||
def = def || {};
|
* Checks if there is a theme override for the given node definition and property
|
||||||
var result = def.color;
|
* @param {*} def node definition
|
||||||
var paletteTheme = RED.settings.theme('palette.theme') || [];
|
* @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 (paletteTheme.length > 0) {
|
||||||
if (!nodeColorCache.hasOwnProperty(type)) {
|
for (let i = 0; i < paletteTheme.length; i++ ){
|
||||||
nodeColorCache[type] = def.color;
|
const themeRule = paletteTheme[i];
|
||||||
var l = paletteTheme.length;
|
if (themeRule.hasOwnProperty('category')) {
|
||||||
for (var i = 0; i < l; i++ ){
|
if (!themeRule.hasOwnProperty('_category')) {
|
||||||
var themeRule = paletteTheme[i];
|
themeRule._category = new RegExp(themeRule.category);
|
||||||
if (themeRule.hasOwnProperty('category')) {
|
|
||||||
if (!themeRule.hasOwnProperty('_category')) {
|
|
||||||
themeRule._category = new RegExp(themeRule.category);
|
|
||||||
}
|
|
||||||
if (!themeRule._category.test(def.category)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (themeRule.hasOwnProperty('type')) {
|
if (!themeRule._category.test(def.category)) {
|
||||||
if (!themeRule.hasOwnProperty('_type')) {
|
continue;
|
||||||
themeRule._type = new RegExp(themeRule.type);
|
|
||||||
}
|
|
||||||
if (!themeRule._type.test(type)) {
|
|
||||||
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 null;
|
||||||
return result;
|
}
|
||||||
} else {
|
|
||||||
return "#ddd";
|
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) {
|
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("main", "red/main.min.js");
|
||||||
context.asset.should.have.a.property("vendorMonaco", "vendor/monaco/monaco-bootstrap.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 () {
|
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.should.have.a.property("login");
|
||||||
context.login.should.have.a.property("image", "theme/login/image");
|
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.should.have.a.property("deployButton");
|
||||||
settings.deployButton.should.have.a.property("type", "simple");
|
settings.deployButton.should.have.a.property("type", "simple");
|
||||||
settings.deployButton.should.have.a.property("label", "Save");
|
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({
|
theme.init({
|
||||||
editorTheme: {
|
editorTheme: {
|
||||||
userMenu: true,
|
userMenu: true,
|
||||||
|
|
@ -208,7 +208,7 @@ describe("api/editor/theme", function () {
|
||||||
|
|
||||||
theme.app();
|
theme.app();
|
||||||
|
|
||||||
var settings = theme.settings();
|
var settings = await theme.settings();
|
||||||
settings.should.have.a.property("userMenu");
|
settings.should.have.a.property("userMenu");
|
||||||
settings.userMenu.should.be.eql(true);
|
settings.userMenu.should.be.eql(true);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue