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)
|
- [weather] Updated `apiVersion` default from 2.5 to 3.0 (#3424)
|
||||||
- [core] Updated dependencies including stylistic-eslint
|
- [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] 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
|
### Fixed
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@
|
|||||||
<script type="text/javascript" src="js/loader.js"></script>
|
<script type="text/javascript" src="js/loader.js"></script>
|
||||||
<script type="text/javascript" src="js/socketclient.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/animateCSS.js"></script>
|
||||||
|
<script type="text/javascript" src="js/positions.js"></script>
|
||||||
<script type="text/javascript" src="js/main.js"></script>
|
<script type="text/javascript" src="js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -256,6 +256,9 @@ function App () {
|
|||||||
|
|
||||||
Log.setLogLevel(config.logLevel);
|
Log.setLogLevel(config.logLevel);
|
||||||
|
|
||||||
|
// get the used module positions
|
||||||
|
Utils.getModulePositions();
|
||||||
|
|
||||||
let modules = [];
|
let modules = [];
|
||||||
for (const module of config.modules) {
|
for (const module of config.modules) {
|
||||||
if (module.disabled) continue;
|
if (module.disabled) continue;
|
||||||
@ -266,10 +269,10 @@ function App () {
|
|||||||
modules.push(module.module);
|
modules.push(module.module);
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
} 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 rootPath = path.resolve(`${__dirname}/../`);
|
||||||
const Log = require(`${rootPath}/js/logger.js`);
|
const Log = require(`${rootPath}/js/logger.js`);
|
||||||
|
const Utils = require(`${rootPath}/js/utils.js`);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string with path of configuration file.
|
* Returns a string with path of configuration file.
|
||||||
@ -68,6 +69,7 @@ function checkConfigFile () {
|
|||||||
|
|
||||||
Log.info("Checking modules structure configuration... ");
|
Log.info("Checking modules structure configuration... ");
|
||||||
|
|
||||||
|
const position_list = Utils.getModulePositions();
|
||||||
// Make Ajv schema confguration of modules config
|
// Make Ajv schema confguration of modules config
|
||||||
// only scan "module" and "position"
|
// only scan "module" and "position"
|
||||||
const schema = {
|
const schema = {
|
||||||
@ -83,21 +85,7 @@ function checkConfigFile () {
|
|||||||
},
|
},
|
||||||
position: {
|
position: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: [
|
enum: position_list
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ["module"]
|
required: ["module"]
|
||||||
@ -116,10 +104,10 @@ function checkConfigFile () {
|
|||||||
let position = validate.errors[0].instancePath.split("/")[3];
|
let position = validate.errors[0].instancePath.split("/")[3];
|
||||||
|
|
||||||
Log.error(colors.red("This module configuration contains errors:"));
|
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) {
|
if (position) {
|
||||||
Log.error(colors.red(`${position}: ${validate.errors[0].message}`));
|
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 {
|
} else {
|
||||||
Log.error(colors.red(validate.errors[0].message));
|
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.
|
// (interval 30 seconds). If startup-timestamp has changed the client reloads the magicmirror webpage.
|
||||||
checkServerInterval: 30 * 1000,
|
checkServerInterval: 30 * 1000,
|
||||||
reloadAfterServerRestart: false,
|
reloadAfterServerRestart: false,
|
||||||
allowCustomModulePositions: false,
|
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
{
|
{
|
||||||
|
@ -50,7 +50,7 @@ const Loader = (function () {
|
|||||||
* @returns {object[]} module data as configured in config
|
* @returns {object[]} module data as configured in config
|
||||||
*/
|
*/
|
||||||
const getAllModules = function () {
|
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;
|
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 () {
|
const MM = (function () {
|
||||||
let modules = [];
|
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
|
* an ugly top margin. By using this function, the top bar will be hidden if the
|
||||||
* update notification is not visible.
|
* 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 () {
|
const updateWrapperStates = function () {
|
||||||
modulePositions.forEach(function (position) {
|
modulePositions.forEach(function (position) {
|
||||||
|
35
js/utils.js
35
js/utils.js
@ -1,7 +1,17 @@
|
|||||||
const execSync = require("node:child_process").execSync;
|
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 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 = {
|
module.exports = {
|
||||||
|
|
||||||
async logSystemInformation () {
|
async logSystemInformation () {
|
||||||
@ -29,13 +39,32 @@ module.exports = {
|
|||||||
|
|
||||||
// return all available module positions
|
// return all available module positions
|
||||||
getAvailableModulePositions () {
|
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)
|
// return if postion is on modulePositions Array (true/false)
|
||||||
moduleHasValidPosition (position) {
|
moduleHasValidPosition (position) {
|
||||||
if (config.allowCustomModulePositions) return true;
|
|
||||||
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
|
if (this.getAvailableModulePositions().indexOf(position) === -1) return false;
|
||||||
return true;
|
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 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) => {
|
exports.startApplication = async (configFilename, exec) => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
if (global.app) {
|
if (global.app) {
|
||||||
@ -45,11 +60,12 @@ exports.getDocument = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.waitForElement = (selector, ignoreValue = "") => {
|
exports.waitForElement = (selector, ignoreValue = "", timeout = 0) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let oldVal = "dummy12345";
|
let oldVal = "dummy12345";
|
||||||
|
let element = null;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const element = document.querySelector(selector);
|
element = document.querySelector(selector);
|
||||||
if (element) {
|
if (element) {
|
||||||
let newVal = element.textContent;
|
let newVal = element.textContent;
|
||||||
if (newVal === oldVal) {
|
if (newVal === oldVal) {
|
||||||
@ -64,6 +80,12 @@ exports.waitForElement = (selector, ignoreValue = "") => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 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);
|
expect(elem.textContent).toMatch(regex);
|
||||||
return true;
|
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