mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-29 12:39:45 +00:00
add support for custom regions, by detecting what is used in index.html (#3518)
read index.html to discover the regions used, make them the list checked by app.js and check:config test fixes #3504 supercedes #3506 no config.js param required
This commit is contained in:
parent
56736786fd
commit
2b97e0d26e
@ -20,8 +20,8 @@ _This release is scheduled to be released on 2024-10-01._
|
||||
|
||||
- [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424)
|
||||
- [core] Updated dependencies including stylistic-eslint
|
||||
- [core] Allow custom module positions by setting `allowCustomModulePositions` in `config.js` (fixes #3504, related to https://github.com/MagicMirrorOrg/MagicMirror/pull/3445)
|
||||
- [core] Updated SocketIO catch all to new API
|
||||
- [core] Allow custom modules positions by scanning index.html for the defined regions, instead of hard coded(fixes #3504)
|
||||
|
||||
### Fixed
|
||||
|
||||
|
@ -55,6 +55,7 @@
|
||||
<script type="text/javascript" src="js/loader.js"></script>
|
||||
<script type="text/javascript" src="js/socketclient.js"></script>
|
||||
<script type="text/javascript" src="js/animateCSS.js"></script>
|
||||
<script type="text/javascript" src="js/positions.js"></script>
|
||||
<script type="text/javascript" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -256,6 +256,9 @@ function App () {
|
||||
|
||||
Log.setLogLevel(config.logLevel);
|
||||
|
||||
// get the used module positions
|
||||
Utils.getModulePositions();
|
||||
|
||||
let modules = [];
|
||||
for (const module of config.modules) {
|
||||
if (module.disabled) continue;
|
||||
@ -266,10 +269,10 @@ function App () {
|
||||
modules.push(module.module);
|
||||
}
|
||||
} else {
|
||||
Log.warn("Invalid module position found for this configuration:", module);
|
||||
Log.warn("Invalid module position found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
} else {
|
||||
Log.warn("No module name found for this configuration:", module);
|
||||
Log.warn("No module name found for this configuration:" + `\n${JSON.stringify(module, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ const ajv = new Ajv();
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const Utils = require(`${rootPath}/js/utils.js`);
|
||||
|
||||
/**
|
||||
* Returns a string with path of configuration file.
|
||||
@ -68,6 +69,7 @@ function checkConfigFile () {
|
||||
|
||||
Log.info("Checking modules structure configuration... ");
|
||||
|
||||
const position_list = Utils.getModulePositions();
|
||||
// Make Ajv schema confguration of modules config
|
||||
// only scan "module" and "position"
|
||||
const schema = {
|
||||
@ -83,21 +85,7 @@ function checkConfigFile () {
|
||||
},
|
||||
position: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"top_bar",
|
||||
"top_left",
|
||||
"top_center",
|
||||
"top_right",
|
||||
"upper_third",
|
||||
"middle_center",
|
||||
"lower_third",
|
||||
"bottom_left",
|
||||
"bottom_center",
|
||||
"bottom_right",
|
||||
"bottom_bar",
|
||||
"fullscreen_above",
|
||||
"fullscreen_below"
|
||||
]
|
||||
enum: position_list
|
||||
}
|
||||
},
|
||||
required: ["module"]
|
||||
@ -116,10 +104,10 @@ function checkConfigFile () {
|
||||
let position = validate.errors[0].instancePath.split("/")[3];
|
||||
|
||||
Log.error(colors.red("This module configuration contains errors:"));
|
||||
Log.error(data.modules[module]);
|
||||
Log.error(`\n${JSON.stringify(data.modules[module], null, 2)}`);
|
||||
if (position) {
|
||||
Log.error(colors.red(`${position}: ${validate.errors[0].message}`));
|
||||
Log.error(validate.errors[0].params.allowedValues);
|
||||
Log.error(`\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`);
|
||||
} else {
|
||||
Log.error(colors.red(validate.errors[0].message));
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ const defaults = {
|
||||
// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage.
|
||||
checkServerInterval: 30 * 1000,
|
||||
reloadAfterServerRestart: false,
|
||||
allowCustomModulePositions: false,
|
||||
|
||||
modules: [
|
||||
{
|
||||
|
@ -50,7 +50,7 @@ const Loader = (function () {
|
||||
* @returns {object[]} module data as configured in config
|
||||
*/
|
||||
const getAllModules = function () {
|
||||
const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined" || config.allowCustomModulePositions));
|
||||
const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined"));
|
||||
return AllModules;
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut */
|
||||
/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */
|
||||
|
||||
const MM = (function () {
|
||||
let modules = [];
|
||||
@ -450,7 +450,6 @@ const MM = (function () {
|
||||
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||
* update notification is not visible.
|
||||
*/
|
||||
const modulePositions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
|
||||
const updateWrapperStates = function () {
|
||||
modulePositions.forEach(function (position) {
|
||||
|
35
js/utils.js
35
js/utils.js
@ -1,7 +1,17 @@
|
||||
const execSync = require("node:child_process").execSync;
|
||||
const Log = require("logger");
|
||||
const path = require("node:path");
|
||||
|
||||
const rootPath = path.resolve(`${__dirname}/../`);
|
||||
const Log = require(`${rootPath}/js/logger.js`);
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const si = require("systeminformation");
|
||||
|
||||
const modulePositions = []; // will get list from index.html
|
||||
const regionRegEx = /"region ([^"]*)/i;
|
||||
const indexFileName = "index.html";
|
||||
const discoveredPositionsJSFilename = "js/positions.js";
|
||||
|
||||
module.exports = {
|
||||
|
||||
async logSystemInformation () {
|
||||
@ -29,13 +39,32 @@ module.exports = {
|
||||
|
||||
// return all available module positions
|
||||
getAvailableModulePositions () {
|
||||
return ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
|
||||
return modulePositions;
|
||||
},
|
||||
|
||||
// return if postion is on modulePositions Array (true/false)
|
||||
moduleHasValidPosition (position) {
|
||||
if (config.allowCustomModulePositions) return true;
|
||||
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
getModulePositions () {
|
||||
// get the lines of the index.html
|
||||
const lines = fs.readFileSync(indexFileName).toString().split(os.EOL);
|
||||
// loop thru the lines
|
||||
lines.forEach((line) => {
|
||||
// run the regex on each line
|
||||
const results = regionRegEx.exec(line);
|
||||
// if the regex returned something
|
||||
if (results && results.length > 0) {
|
||||
// get the postition parts and replace space with underscore
|
||||
const positionName = results[1].replace(" ", "_");
|
||||
// add it to the list
|
||||
modulePositions.push(positionName);
|
||||
}
|
||||
});
|
||||
fs.writeFileSync(discoveredPositionsJSFilename, `const modulePositions=${JSON.stringify(modulePositions)}`);
|
||||
// return the list to the caller
|
||||
return modulePositions;
|
||||
}
|
||||
};
|
||||
|
23
tests/configs/customregions.js
Normal file
23
tests/configs/customregions.js
Normal file
@ -0,0 +1,23 @@
|
||||
let config = {
|
||||
modules:
|
||||
// Using exotic content. This is why don't accept go to JSON configuration file
|
||||
(() => {
|
||||
let positions = ["row3_left", "top3_left1"];
|
||||
let modules = Array();
|
||||
for (let idx in positions) {
|
||||
modules.push({
|
||||
module: "helloworld",
|
||||
position: positions[idx],
|
||||
config: {
|
||||
text: `Text in ${positions[idx]}`
|
||||
}
|
||||
});
|
||||
}
|
||||
return modules;
|
||||
})()
|
||||
};
|
||||
|
||||
/*************** DO NOT EDIT THE LINE BELOW ***************/
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = config;
|
||||
}
|
30
tests/e2e/custom_module_regions_spec.js
Normal file
30
tests/e2e/custom_module_regions_spec.js
Normal file
@ -0,0 +1,30 @@
|
||||
const helpers = require("./helpers/global-setup");
|
||||
|
||||
describe("Custom Position of modules", () => {
|
||||
beforeAll(async () => {
|
||||
await helpers.fixupIndex();
|
||||
await helpers.startApplication("tests/configs/customregions.js");
|
||||
await helpers.getDocument();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await helpers.stopApplication();
|
||||
await helpers.restoreIndex();
|
||||
});
|
||||
|
||||
const positions = ["row3_left", "top3_left1"];
|
||||
let i = 0;
|
||||
const className1 = positions[i].replace("_", ".");
|
||||
let message1 = positions[i];
|
||||
it(`should show text in ${message1}`, async () => {
|
||||
const elem = await helpers.waitForElement(`.${className1}`);
|
||||
expect(elem).not.toBeNull();
|
||||
expect(elem.textContent).toContain(`Text in ${message1}`);
|
||||
});
|
||||
i = 1;
|
||||
const className2 = positions[i].replace("_", ".");
|
||||
let message2 = positions[i];
|
||||
it(`should NOT show text in ${message2}`, async () => {
|
||||
const elem = await helpers.waitForElement(`.${className2}`, "", 1500);
|
||||
expect(elem).toBeNull();
|
||||
}, 1510);
|
||||
});
|
@ -1,5 +1,20 @@
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const jsdom = require("jsdom");
|
||||
|
||||
const indexFile = `${__dirname}/../../../index.html`;
|
||||
const cssFile = `${__dirname}/../../../css/custom.css`;
|
||||
const sampleCss = [
|
||||
".region.row3 {",
|
||||
" top: 0;",
|
||||
"}",
|
||||
".region.row3.left {",
|
||||
" top: 100%;",
|
||||
"}"
|
||||
];
|
||||
var indexData = [];
|
||||
var cssData = [];
|
||||
|
||||
exports.startApplication = async (configFilename, exec) => {
|
||||
jest.resetModules();
|
||||
if (global.app) {
|
||||
@ -45,11 +60,12 @@ exports.getDocument = () => {
|
||||
});
|
||||
};
|
||||
|
||||
exports.waitForElement = (selector, ignoreValue = "") => {
|
||||
exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => {
|
||||
return new Promise((resolve) => {
|
||||
let oldVal = "dummy12345";
|
||||
let element = null;
|
||||
const interval = setInterval(() => {
|
||||
const element = document.querySelector(selector);
|
||||
element = document.querySelector(selector);
|
||||
if (element) {
|
||||
let newVal = element.textContent;
|
||||
if (newVal === oldVal) {
|
||||
@ -64,6 +80,12 @@ exports.waitForElement = (selector, ignoreValue = "") => {
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
if (timeout !== 0) {
|
||||
setTimeout(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
resolve(null);
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -91,3 +113,34 @@ exports.testMatch = async (element, regex) => {
|
||||
expect(elem.textContent).toMatch(regex);
|
||||
return true;
|
||||
};
|
||||
|
||||
exports.fixupIndex = async () => {
|
||||
// read and save the git level index file
|
||||
indexData = (await fs.promises.readFile(indexFile)).toString();
|
||||
// make lines of the content
|
||||
let workIndexLines = indexData.split(os.EOL);
|
||||
// loop thru the lines to find place to insert new region
|
||||
for (let l in workIndexLines) {
|
||||
if (workIndexLines[l].includes("region top right")) {
|
||||
// insert a new line with new region definition
|
||||
workIndexLines.splice(l, 0, " <div class=\"region row3 left\"><div class=\"container\"></div></div>");
|
||||
break;
|
||||
}
|
||||
}
|
||||
// write out the new index.html file, not append
|
||||
await fs.promises.writeFile(indexFile, workIndexLines.join(os.EOL), { flush: true });
|
||||
// read in the current custom.css
|
||||
cssData = (await fs.promises.readFile(cssFile)).toString();
|
||||
// write out the custom.css for this testcase, matching the new region name
|
||||
await fs.promises.writeFile(cssFile, sampleCss.join(os.EOL), { flush: true });
|
||||
};
|
||||
|
||||
exports.restoreIndex = async () => {
|
||||
// if we read in data
|
||||
if (indexData.length > 1) {
|
||||
//write out saved index.html
|
||||
await fs.promises.writeFile(indexFile, indexData, { flush: true });
|
||||
// write out saved custom.css
|
||||
await fs.promises.writeFile(cssFile, cssData, { flush: true });
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user