From c989ff339d1bb8afde3371d12dab8cc227941691 Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Wed, 26 Oct 2022 10:52:48 +0000 Subject: [PATCH] [IMP] web: report module loading errors in the DOM Previously, if there were module loading errors, they would typically happen before the error service was ready, and so there is almost always no user-facing error that shows up even though the application state may be severely corrupted. This can be very confusing for developers who are not used to working with JS and might not check the devtools console. This commit makes it so that the module system will replace the contents of the body with an error when some modules were unable to be loaded, with the list of modules and the reason, so that the developer is not confused as to why things aren't working as expected. closes odoo/odoo#104155 Signed-off-by: Samuel Degueldre --- addons/web/static/src/boot.js | 118 ++++++++++++------ addons/web/views/report_templates.xml | 12 ++ addons/web/views/webclient_templates.xml | 6 +- .../website_slides_templates_lesson_embed.xml | 5 + 4 files changed, 104 insertions(+), 37 deletions(-) diff --git a/addons/web/static/src/boot.js b/addons/web/static/src/boot.js index fc42d8108af..707c0ec0a41 100644 --- a/addons/web/static/src/boot.js +++ b/addons/web/static/src/boot.js @@ -32,6 +32,7 @@ var factories = Object.create(null); var jobDeps = []; var jobPromises = []; + const failed = []; var services = Object.create({}); @@ -75,41 +76,23 @@ }); }, getWaitedJobs: function () { - return jobs - .map(function (job) { - return job.name; - }) - .filter(function (item, index, self) { - // uniq - return self.indexOf(item) === index; - }); + return new Set(jobs.map((job) => job.name)); }, - getMissingJobs: function () { - var self = this; - var waited = this.getWaitedJobs(); - var missing = []; - waited.forEach(function (job) { - self.getDependencies(job).forEach(function (job) { - if (!(job in self.services)) { - missing.push(job); + getMissingJobs() { + const waited = this.getWaitedJobs(); + const missing = new Set(); + for (const job of waited) { + for (const dep of this.getDependencies(job)) { + if ( + !(dep in this.services) && + !waited.has(dep) && + !failed.find((job) => job.name === dep) + ) { + missing.add(dep); } - }); - }); - return missing - .filter(function (item, index, self) { - return self.indexOf(item) === index; - }) - .filter(function (item) { - return waited.indexOf(item) < 0; - }) - .filter(function (job) { - return !job.error; - }); - }, - getFailedJobs: function () { - return jobs.filter(function (job) { - return !!job.error; - }); + } + } + return [...missing]; }, processJobs: function () { var job; @@ -120,6 +103,7 @@ var jobExec; function onError(e) { job.error = e; + failed.push(job); console.error(`Error while loading ${job.name}: ${e.message}`, e); } var def = new Promise(function (resolve) { @@ -254,7 +238,6 @@ }; odoo.log = function () { var missing = []; - var failed = []; var cycle = null; if (jobs.length) { @@ -305,7 +288,6 @@ } } missing = odoo.__DEBUG__.getMissingJobs(); - failed = odoo.__DEBUG__.getFailedJobs(); var unloaded = Object.keys(debugJobs) // Object.values is not supported .map(function (key) { return debugJobs[key]; @@ -356,12 +338,20 @@ } } } - odoo.__DEBUG__.jsModules = { + const moduleInfo = { missing: missing, failed: failed.map((mod) => mod.name), unloaded: unloaded ? unloaded.map((mod) => mod.name) : [], cycle, }; + odoo.__DEBUG__.jsModules = moduleInfo; + // The assets_tests bundle contains a lot of module whose dependencies are in other bundles + // that may or may not be loaded. This global is used to silence module loading errors when + // this bundle is present + if (!window.__odooIgnoreModuleErrors) { + displayModuleErrors(moduleInfo); + } + didLogInfoResolve(true); }; /** @@ -440,4 +430,60 @@ // visit each root to find cycles return visitJobs(jobs.map((j) => j.name)); } + + function displayModuleErrors({ failed, missing, unloaded, cycle }) { + const list = (heading, arr) => { + const frag = document.createDocumentFragment(); + if (!arr || !arr.length) { + return frag; + } + frag.textContent = heading; + const ul = document.createElement("ul"); + for (const el of arr) { + const li = document.createElement("li"); + li.textContent = el; + ul.append(li); + } + frag.appendChild(ul); + return frag; + }; + if ([failed, missing, unloaded].some((arr) => arr.length) || cycle) { + // Empty body + while (document.body.childNodes.length) { + document.body.childNodes[0].remove(); + } + const container = document.createElement("div"); + container.className = + "position-fixed w-100 h-100 d-flex align-items-center flex-column bg-white overflow-auto"; + container.style.zIndex = "10000"; + const alert = document.createElement("div"); + alert.className = "alert alert-danger o_error_detail fw-bold m-auto"; + container.appendChild(alert); + alert.appendChild( + list( + "The following modules failed to load because of an error, you may find more information in the devtools console:", + failed + ) + ); + alert.appendChild( + list( + "The following modules could not be loaded because they form a dependency cycle:", + cycle && [cycle] + ) + ); + alert.appendChild( + list( + "The following modules are needed by other modules but have not been defined, they may not be present in the correct asset bundle:", + missing + ) + ); + alert.appendChild( + list( + "The following modules could not be loaded because they have unmet dependencies, this is a secondary error which is likely caused by one of the above problems:", + unloaded + ) + ); + document.body.appendChild(container); + } + } })(); diff --git a/addons/web/views/report_templates.xml b/addons/web/views/report_templates.xml index 5e3762bb9e0..8c70b2c95c0 100644 --- a/addons/web/views/report_templates.xml +++ b/addons/web/views/report_templates.xml @@ -20,6 +20,11 @@ @@ -46,6 +51,13 @@ <t t-esc="title or 'Odoo Report'"/> +