diff --git a/addons/bus/__manifest__.py b/addons/bus/__manifest__.py index b07c18ef443..f1dfdae281b 100644 --- a/addons/bus/__manifest__.py +++ b/addons/bus/__manifest__.py @@ -33,7 +33,7 @@ ], 'bus.websocket_worker_assets': [ 'web/static/src/legacy/js/promise_extension.js', - 'web/static/src/boot.js', + 'web/static/src/module_loader.js', 'bus/static/src/workers/*', ], }, diff --git a/addons/im_livechat/tests/test_im_livechat_support_page.py b/addons/im_livechat/tests/test_im_livechat_support_page.py index fe08c73bbc5..9bea4f5446e 100644 --- a/addons/im_livechat/tests/test_im_livechat_support_page.py +++ b/addons/im_livechat/tests/test_im_livechat_support_page.py @@ -13,7 +13,7 @@ class TestImLivechatSupportPage(HttpCase): # interrupt errors then ensures all the assets are loaded. check_js_modules = """ setTimeout(() => { - const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules; + const { missing, failed, unloaded } = odoo.loader.findErrors(); if ([missing, failed, unloaded].some(arr => arr.length)) { console.error("Couldn't load all JS modules.", JSON.stringify({ missing, failed, unloaded })); } else { @@ -27,4 +27,4 @@ class TestImLivechatSupportPage(HttpCase): }, 1000); """ - self.browser_js("/im_livechat/support/1", code=check_js_modules, ready="odoo.__DEBUG__.didLogInfo") + self.browser_js("/im_livechat/support/1", code=check_js_modules) diff --git a/addons/mail/static/src/discuss/core/public/boot.js b/addons/mail/static/src/discuss/core/public/boot.js index aa9e3c1fcab..961dd16b70f 100644 --- a/addons/mail/static/src/discuss/core/public/boot.js +++ b/addons/mail/static/src/discuss/core/public/boot.js @@ -1,7 +1,6 @@ /* @odoo-module */ import { DiscussPublic } from "@mail/discuss/core/public/discuss_public"; -import { data } from "mail.discuss_public_template"; import { mount, whenReady } from "@odoo/owl"; @@ -16,7 +15,7 @@ import { makeEnv, startServices } from "@web/env"; const mainComponentsRegistry = registry.category("main_components"); mainComponentsRegistry.add("DiscussPublic", { Component: DiscussPublic, - props: { data }, + props: { data: odoo.discuss_data }, }); const env = makeEnv(); diff --git a/addons/mail/static/tests/tours/discuss_public_tour.js b/addons/mail/static/tests/tours/discuss_public_tour.js index 4dd7ff2dc8f..e8ee2209f86 100644 --- a/addons/mail/static/tests/tours/discuss_public_tour.js +++ b/addons/mail/static/tests/tours/discuss_public_tour.js @@ -18,17 +18,14 @@ registry.category("web_tour.tours").add("mail/static/tests/tours/discuss_public_ if (!window.location.pathname.startsWith("/discuss/channel")) { console.error("Channel secret token is still present in URL."); } - // Wait for modules to be loaded or failed for the next step - odoo.__DEBUG__.didLogInfo.then(() => { - const { missing, failed, unloaded } = odoo.__DEBUG__.jsModules; - if ([missing, failed, unloaded].some((arr) => arr.length)) { - console.error( - "Couldn't load all JS modules.", - JSON.stringify({ missing, failed, unloaded }) - ); - } - document.body.classList.add("o_discuss_channel_public_modules_loaded"); - }); + const { missing, failed, unloaded } = odoo.loader.findErrors(); + if ([missing, failed, unloaded].some((arr) => arr.length)) { + console.error( + "Couldn't load all JS modules.", + JSON.stringify({ missing, failed, unloaded }) + ); + } + document.body.classList.add("o_discuss_channel_public_modules_loaded"); }, extraTrigger: ".o_discuss_channel_public_modules_loaded", }, diff --git a/addons/mail/views/discuss_public_templates.xml b/addons/mail/views/discuss_public_templates.xml index 093940cc2e7..ff2b81ca2c1 100644 --- a/addons/mail/views/discuss_public_templates.xml +++ b/addons/mail/views/discuss_public_templates.xml @@ -12,6 +12,7 @@ __session_info__: , csrf_token: "", debug: "", + discuss_data: }; { const { user_context, cache_hashes } = odoo.__session_info__; @@ -25,13 +26,6 @@ direction: ; } - diff --git a/addons/mrp_subcontracting/__manifest__.py b/addons/mrp_subcontracting/__manifest__.py index 5552ebcce79..fbc804db704 100644 --- a/addons/mrp_subcontracting/__manifest__.py +++ b/addons/mrp_subcontracting/__manifest__.py @@ -60,7 +60,7 @@ 'web/static/src/scss/fontawesome_overridden.scss', 'web/static/src/legacy/js/promise_extension.js', - 'web/static/src/boot.js', + 'web/static/src/module_loader.js', 'web/static/src/session.js', 'web/static/src/legacy/js/core/cookie_utils.js', diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index af670a67db6..ebfa9661d3f 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -101,8 +101,18 @@ 'web/static/src/scss/bootstrap_overridden.scss', 'web/static/src/scss/fontawesome_overridden.scss', 'web/static/fonts/fonts.scss', - - + # JS boot + 'web/static/src/module_loader.js', + # libs (should be loaded before framework) + 'point_of_sale/static/lib/**/*', + 'web/static/lib/luxon/luxon.js', + 'web/static/lib/owl/owl.js', + 'web/static/lib/owl/odoo_module.js', + 'web_editor/static/lib/html2canvas.js', + 'web/static/lib/zxing-library/zxing-library.js', + # FIXME POSREF legacy dependencies to remove + 'web/static/lib/moment/moment.js', + 'web/static/src/legacy/js/promise_extension.js', # JS framework ('include', 'web._assets_core'), ('remove', 'web/static/src/core/errors/error_handlers.js'), # error handling in PoS is different from the webclient @@ -125,10 +135,6 @@ 'bus/static/src/workers/*', # report download utils 'web/static/src/webclient/actions/reports/utils.js', - # libs - 'point_of_sale/static/lib/**/*', - 'web_editor/static/lib/html2canvas.js', - 'web/static/lib/zxing-library/zxing-library.js', # PoS files 'point_of_sale/static/src/**/*', ('remove', 'point_of_sale/static/src/backend/**/*'), @@ -138,10 +144,6 @@ 'web/static/lib/jquery/jquery.js', 'web_tour/static/src/tour_pointer/**/*', 'web_tour/static/src/tour_service/**/*', - - # FIXME POSREF legacy dependencies to remove - 'web/static/lib/moment/moment.js', - 'web/static/src/legacy/js/promise_extension.js', ], # Bundle that starts the pos, loaded on /pos/ui 'point_of_sale.assets_prod': [ diff --git a/addons/point_of_sale/static/src/app/main.js b/addons/point_of_sale/static/src/app/main.js index 4fe7264f763..d9b20ec18f2 100644 --- a/addons/point_of_sale/static/src/app/main.js +++ b/addons/point_of_sale/static/src/app/main.js @@ -26,8 +26,6 @@ whenReady(() => { isEnterprise: session.server_version_info.slice(-1)[0] === "e", }; - // Wait for all templates - await odoo.ready(/\.bundle\.xml/); // Make a temporary app to be able to use renderToString method before the main app is available. const renderToStringApp = new App(Component, { name: "renderToString app", diff --git a/addons/point_of_sale/views/pos_assets_index.xml b/addons/point_of_sale/views/pos_assets_index.xml index 621eab1c868..bd6e8286796 100644 --- a/addons/point_of_sale/views/pos_assets_index.xml +++ b/addons/point_of_sale/views/pos_assets_index.xml @@ -38,7 +38,9 @@ odoo.loadMenusPromise = Promise.resolve(); - + + + diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index d1f069a95d1..26d33e7846b 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -85,7 +85,6 @@ ], 'web.assets_tests': [ 'project/static/tests/tours/**/*', - 'project/static/tests/views/**/*', ], 'project.webclient': [ ('include', 'web._assets_helpers'), @@ -111,7 +110,7 @@ 'web/static/src/scss/fontawesome_overridden.scss', 'web/static/src/legacy/js/promise_extension.js', - 'web/static/src/boot.js', + 'web/static/src/module_loader.js', 'web/static/src/session.js', 'web/static/src/legacy/js/core/cookie_utils.js', diff --git a/addons/survey/static/tests/tours/certification_failure.js b/addons/survey/static/tests/tours/certification_failure.js index 0be9ca10a9c..4c71be24fb9 100644 --- a/addons/survey/static/tests/tours/certification_failure.js +++ b/addons/survey/static/tests/tours/certification_failure.js @@ -1,15 +1,17 @@ /** @odoo-module **/ -import SurveyFormWidget from "@survey/js/survey_form"; /** * Speed up fade-in fade-out to avoid useless delay in tests. - */ -SurveyFormWidget.include({ - _submitForm: function () { - this.fadeInOutDelay = 0; - return this._super.apply(this, arguments); - } -}); +*/ +function patchSurveyWidget() { + const SurveyFormWidget = odoo.loader.modules.get('@survey/js/survey_form')[Symbol.for('default')] + SurveyFormWidget.include({ + _submitForm: function () { + this.fadeInOutDelay = 0; + return this._super.apply(this, arguments); + } + }); +} /** * This tour will test that, for the demo certification allowing 2 attempts, a user can @@ -18,6 +20,14 @@ SurveyFormWidget.include({ import { registry } from "@web/core/registry"; +var patch = [{ + content: "Patching Survey Widget", + trigger: 'body', + run: function(){ + patchSurveyWidget(); + } +}] + var failSteps = [{ // Page-1 content: "Clicking on Start Certification", trigger: 'button.btn.btn-primary.btn-lg:contains("Start Certification")', @@ -104,4 +114,4 @@ var lastSteps = [{ registry.category("web_tour.tours").add('test_certification_failure', { test: true, url: '/survey/start/4ead4bc8-b8f2-4760-a682-1fde8daaaaac', - steps: () => [].concat(failSteps, retrySteps, failSteps, lastSteps) }); + steps: () => [].concat(patch, failSteps, retrySteps, failSteps, lastSteps) }); diff --git a/addons/survey/static/tests/tours/certification_success.js b/addons/survey/static/tests/tours/certification_success.js index 126788f9462..08ef9ed19af 100644 --- a/addons/survey/static/tests/tours/certification_success.js +++ b/addons/survey/static/tests/tours/certification_success.js @@ -1,22 +1,30 @@ /** @odoo-module **/ -import SurveyFormWidget from "@survey/js/survey_form"; /** * Speed up fade-in fade-out to avoid useless delay in tests. */ -SurveyFormWidget.include({ - _submitForm: function () { - this.fadeInOutDelay = 0; - return this._super.apply(this, arguments); - } -}); +function patchSurveyWidget() { + const SurveyFormWidget = odoo.loader.modules.get('@survey/js/survey_form')[Symbol.for('default')] + SurveyFormWidget.include({ + _submitForm: function () { + this.fadeInOutDelay = 0; + return this._super.apply(this, arguments); + } + }); +} import { registry } from "@web/core/registry"; registry.category("web_tour.tours").add('test_certification_success', { test: true, url: '/survey/start/4ead4bc8-b8f2-4760-a682-1fde8daaaaac', - steps: () => [{ // Page-1 + steps: () => [{ + content: "Patching Survey Widget", + trigger: 'body', + run: function(){ + patchSurveyWidget(); + } + }, { // Page-1 content: "Clicking on Start Certification", trigger: 'button.btn.btn-primary.btn-lg:contains("Start Certification")', }, { // Question: Do we sell Acoustic Bloc Screens? diff --git a/addons/survey/static/tests/tours/survey_session_manage_test.js b/addons/survey/static/tests/tours/survey_session_manage_test.js index 1f9b3b26703..aba7b69898e 100644 --- a/addons/survey/static/tests/tours/survey_session_manage_test.js +++ b/addons/survey/static/tests/tours/survey_session_manage_test.js @@ -1,35 +1,39 @@ /** @odoo-module **/ -import SessionManager from "@survey/js/survey_session_manage"; -const { DateTime } = luxon; -/** - * Small override for test/tour purposes. - */ -SessionManager.include({ +function patchSessionManager() { + const { DateTime } = luxon; + const SessionManager = odoo.loader.modules.get('@survey/js/survey_session_manage')[Symbol.for('default')] /** - * - Trigger the fetch of answer results immediately at the start. - * (Instead of wasting 2 seconds waiting after the start). - * - Set the fade in/out time to 1 ms to avoid unnecessary delays. - * - Avoid refreshing the results every 2 seconds + * Small override for test/tour purposes. */ - start: function () { - var self = this; - return this._super.apply(this, arguments) - .then(this._refreshResults.bind(this)) - .then(function () { - self.fadeInOutTime = 1; - clearInterval(self.resultsRefreshInterval); - }); - }, + SessionManager.include({ + /** + * - Trigger the fetch of answer results immediately at the start. + * (Instead of wasting 2 seconds waiting after the start). + * - Set the fade in/out time to 1 ms to avoid unnecessary delays. + * - Avoid refreshing the results every 2 seconds + */ + start: function () { + var self = this; + return this._super.apply(this, arguments) + .then(this._refreshResults.bind(this)) + .then(function () { + self.fadeInOutTime = 1; + clearInterval(self.resultsRefreshInterval); + }); + }, - /** - * Force the timer to "now" to avoid introducing potential test breaking - * timely variables (rpc/small server delay/...) if the start_question_time flickers. - */ - _startTimer: function () { - this.$el.data('timer', DateTime.utc()); - return this._super.apply(this, arguments); - } -}); + /** + * Force the timer to "now" to avoid introducing potential test breaking + * timely variables (rpc/small server delay/...) if the start_question_time flickers. + */ + _startTimer: function () { + this.$el.data('timer', DateTime.utc()); + return this._super.apply(this, arguments); + } + }) +}; -export default SessionManager; + + +export default patchSessionManager; diff --git a/addons/survey/static/tests/tours/survey_tour_session_manage.js b/addons/survey/static/tests/tours/survey_tour_session_manage.js index b1e5a6d867c..fd3f31694e8 100644 --- a/addons/survey/static/tests/tours/survey_tour_session_manage.js +++ b/addons/survey/static/tests/tours/survey_tour_session_manage.js @@ -5,6 +5,8 @@ import { zip } from "@web/core/utils/arrays"; import { TourError } from "@web_tour/tour_service/tour_utils"; import { accessSurveysteps } from "./survey_tour_session_tools"; +let rootWidget = null; + /** * Since the chart is rendered using SVG, we can't use jQuery triggers to check if everything * is correctly rendered. @@ -13,7 +15,6 @@ import { accessSurveysteps } from "./survey_tour_session_tools"; */ const getChartData = () => { const chartData = []; - const rootWidget = odoo.__DEBUG__.services['root.widget']; const surveyManagePublicWidget = rootWidget.publicWidgets.find((widget) => { return widget.$el.hasClass('o_survey_session_manage'); }); @@ -144,6 +145,9 @@ registry.category("web_tour.tours").add('test_survey_session_manage_tour', { }, { trigger: 'h1:contains("Nickname")', isCheck: true // check nickname question is displayed +}, { + trigger: 'body', + run: async () => { rootWidget = await odoo.loader.modules.get('root.widget'); } }, { trigger: 'h1', run: nextScreen @@ -242,8 +246,8 @@ registry.category("web_tour.tours").add('test_survey_session_manage_tour', { } }, { trigger: 'h1:contains("Scored Simple Choice")', - // Wait for Button to be updated ("late" enough DOM change after onNext() is triggered). - extra_trigger: '.o_survey_session_navigation_next_label:contains("Show Correct Answer(s)")', + // Wait for progressbar to be updated ("late" enough DOM change after onNext() is triggered). + extra_trigger: '.o_survey_session_progress_small[style*="width: 100%"]', run: () => { checkAnswers(getChartData(), [ {value: 1, type: "regular"}, @@ -255,7 +259,7 @@ registry.category("web_tour.tours").add('test_survey_session_manage_tour', { } }, { trigger: 'h1:contains("Scored Simple Choice")', - // Same as above + // Wait for Button to be updated ("late" enough DOM change after onNext() is triggered). extra_trigger: '.o_survey_session_navigation_next_label:contains("Show Leaderboard")', run: () => { checkAnswers(getChartData(), [ diff --git a/addons/test_website/__manifest__.py b/addons/test_website/__manifest__.py index 963f60da096..be5a0d16638 100644 --- a/addons/test_website/__manifest__.py +++ b/addons/test_website/__manifest__.py @@ -28,7 +28,7 @@ models which only purpose is to run tests.""", 'http://test.external.link/javascript1.js', '/web/static/lib/jquery.ui/jquery-ui.css', 'http://test.external.link/style1.css', - '/web/static/src/boot.js', + '/web/static/src/module_loader.js', 'http://test.external.link/javascript2.js', 'http://test.external.link/style2.css', ], diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 01162fd2504..6a825d0d021 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -156,7 +156,7 @@ This module provides the core of the Odoo Web Client. ], 'web.assets_frontend_minimal': [ 'web/static/src/legacy/js/promise_extension.js', - 'web/static/src/boot.js', + 'web/static/src/module_loader.js', 'web/static/src/session.js', 'web/static/src/legacy/js/core/cookie_utils.js', 'web/static/src/legacy/js/public/lazyloader.js', @@ -256,6 +256,8 @@ This module provides the core of the Odoo Web Client. ('remove', 'web/static/src/core/debug/debug_menu.js'), ('remove', 'web/static/src/core/file_viewer/file_viewer.dark.scss'), ('remove', 'web/static/src/core/emoji_picker/emoji_data.js'), + 'web/static/src/core/commands/default_providers.js', + 'web/static/src/core/commands/command_palette.js', 'web/static/src/public/error_notifications.js', 'web/static/src/public/datetime_picker_widget.js', 'web/static/src/libs/pdfjs.js', @@ -274,7 +276,7 @@ This module provides the core of the Odoo Web Client. ('include', 'web.assets_frontend'), # Remove assets_frontend_minimal ('remove', 'web/static/src/legacy/js/promise_extension.js'), - ('remove', 'web/static/src/boot.js'), + ('remove', 'web/static/src/module_loader.js'), ('remove', 'web/static/src/session.js'), ('remove', 'web/static/src/legacy/js/core/cookie_utils.js'), ('remove', 'web/static/src/legacy/js/public/lazyloader.js'), @@ -368,7 +370,7 @@ This module provides the core of the Odoo Web Client. # Bare javascript essentials: module loader, core folder and core libs 'web._assets_core': [ # module loader - 'web/static/src/boot.js', + 'web/static/src/module_loader.js', # libs 'web/static/lib/luxon/luxon.js', 'web/static/lib/owl/owl.js', diff --git a/addons/web/static/src/boot.js b/addons/web/static/src/boot.js deleted file mode 100644 index 76bde50dc55..00000000000 --- a/addons/web/static/src/boot.js +++ /dev/null @@ -1,455 +0,0 @@ -/** - *------------------------------------------------------------------------------ - * Odoo Web Boostrap Code - *------------------------------------------------------------------------------ - * - * Each module can return a promise. In that case, the module is marked as loaded - * only when the promise is resolved, and its value is equal to the resolved value. - * The module can be rejected (unloaded). This will be logged in the console as info. - * - * logs: - * Missing dependencies: - * These modules do not appear in the page. It is possible that the - * JavaScript file is not in the page or that the module name is wrong - * Failed modules: - * A javascript error is detected - * Rejected modules: - * The module returns a rejected promise. It (and its dependent modules) - * is not loaded. - * Rejected linked modules: - * Modules who depend on a rejected module - * Non loaded modules: - * Modules who depend on a missing or a failed module - * Debug: - * Non loaded or failed module informations for debugging - */ -(function () { - "use strict"; - - var jobs = []; - var factories = Object.create(null); - var jobDeps = []; - var jobPromises = []; - const failed = []; - - var services = Object.create({}); - - if (!globalThis.odoo) { - globalThis.odoo = {}; - } - var odoo = globalThis.odoo; - if (odoo.debug && !new URLSearchParams(location.search).has("debug")) { - // remove debug mode if not explicitely set in url - odoo.debug = ""; - } - var debug = odoo.debug; - - var didLogInfoResolve; - var didLogInfoPromise = new Promise(function (resolve) { - didLogInfoResolve = resolve; - }); - - odoo.remainingJobs = jobs; - odoo.__DEBUG__ = { - didLogInfo: didLogInfoPromise, - getDependencies: function (name, transitive) { - const deps = new Set(); - let queue = [name]; - do { - queue = queue.flatMap((job) => - jobDeps.filter((dep) => dep.to === job).map((dep) => dep.from) - ); - for (const dep of queue) { - deps.add(dep); - } - } while (queue.length && transitive); - return [...deps]; - }, - getDependents: function (name) { - return jobDeps - .filter(function (dep) { - return dep.from === name; - }) - .map(function (dep) { - return dep.to; - }); - }, - getMissingJobs() { - const waited = new Set( - jobs.filter((job) => !job.ignoreMissingDeps).map((job) => job.name) - ); - 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]; - }, - processJobs: function () { - var job; - - function processJob(job) { - var require = makeRequire(job); - - var jobExec; - function onError(e) { - job.error = e; - failed.push(job); - console.error(`Error while loading ${job.name}: ${e.message}`, e); - Promise.reject(e); - } - var def = new Promise(function (resolve) { - try { - jobExec = job.factory.call(null, require); - jobs.splice(jobs.indexOf(job), 1); - } catch (e) { - onError(e); - } - if (!job.error) { - Promise.resolve(jobExec) - .then(function (data) { - services[job.name] = data; - resolve(); - odoo.__DEBUG__.processJobs(); - }) - .catch(function (e) { - if (e instanceof Error) { - onError(e); - } - resolve(); - }); - } else { - resolve(); - } - }); - jobPromises.push(def); - def.then(job.resolve); - } - - function isReady(job) { - return ( - !job.error && - job.factory.deps.every(function (name) { - return name in services; - }) - ); - } - - function makeRequire(job) { - var deps = {}; - Object.keys(services) - .filter(function (item) { - return job.deps.indexOf(item) >= 0; - }) - .forEach(function (key) { - deps[key] = services[key]; - }); - - return function require(name) { - if (!(name in deps)) { - console.error("Undefined dependency: ", name); - } - return deps[name]; - }; - } - - while (jobs.length) { - job = undefined; - for (var i = 0; i < jobs.length; i++) { - if (isReady(jobs[i])) { - job = jobs[i]; - break; - } - } - if (!job) { - break; - } - processJob(job); - } - - return services; - }, - factories: factories, - services: services, - }; - odoo.define = function (name, deps, factory) { - if (!Array.isArray(deps)) { - throw new Error("Dependencies should be defined by an array", deps); - } - if (typeof factory !== "function") { - throw new Error("Factory should be defined by a function", factory); - } - if (typeof name !== "string") { - throw new Error("Invalid name definition (should be a string", name); - } - if (name in factories) { - throw new Error("Service " + name + " already defined"); - } - - factory.deps = deps; - factories[name] = factory; - - let promiseResolve; - const promise = new Promise((resolve) => { - promiseResolve = resolve; - }); - jobs.push({ - name: name, - factory: factory, - deps: deps, - resolve: promiseResolve, - promise: promise, - ignoreMissingDeps: globalThis.__odooIgnoreMissingDependencies, - }); - - deps.forEach(function (dep) { - jobDeps.push({ from: dep, to: name }); - }); - - odoo.__DEBUG__.processJobs(); - }; - odoo.log = function () { - var missing = []; - var cycle = null; - - if (jobs.length) { - var debugJobs = {}; - var job; - var jobdep; - - for (var k = 0; k < jobs.length; k++) { - if (jobs[k].ignoreMissingDeps) { - continue; - } - debugJobs[jobs[k].name] = job = { - dependencies: jobs[k].deps, - dependents: odoo.__DEBUG__.getDependents(jobs[k].name), - name: jobs[k].name, - }; - if (jobs[k].error) { - job.error = jobs[k].error; - } - var deps = odoo.__DEBUG__.getDependencies(job.name); - for (var i = 0; i < deps.length; i++) { - if (job.name !== deps[i] && !(deps[i] in services)) { - jobdep = debugJobs[deps[i]]; - if (!jobdep && deps[i] in factories) { - for (var j = 0; j < jobs.length; j++) { - if (jobs[j].name === deps[i]) { - jobdep = jobs[j]; - break; - } - } - } - if (!job.missing) { - job.missing = []; - } - job.missing.push(deps[i]); - } - } - } - missing = odoo.__DEBUG__.getMissingJobs(); - var unloaded = Object.keys(debugJobs) // Object.values is not supported - .map(function (key) { - return debugJobs[key]; - }) - .filter(function (job) { - return job.missing; - }); - - if (debug || failed.length || unloaded.length) { - var log = globalThis.console[ - !failed.length || !unloaded.length ? "info" : "error" - ].bind(globalThis.console); - log( - (failed.length ? "error" : unloaded.length ? "warning" : "info") + - ": Some modules could not be started" - ); - if (missing.length) { - log("Missing dependencies: ", missing); - } - if (failed.length) { - log( - "Failed modules: ", - failed.map(function (fail) { - return fail.name; - }) - ); - } - if (unloaded.length) { - cycle = findCycle(unloaded); - if (cycle) { - console.error("Cyclic dependencies: " + cycle); - } - log( - "Non loaded modules: ", - unloaded.map(function (unload) { - return unload.name; - }) - ); - } - if (debug && Object.keys(debugJobs).length) { - log("Debug: ", debugJobs); - } - } - } - const moduleInfo = { - missing: missing, - failed: failed.map((mod) => mod.name), - unloaded: unloaded ? unloaded.map((mod) => mod.name) : [], - cycle, - }; - odoo.__DEBUG__.jsModules = moduleInfo; - displayModuleErrors(moduleInfo); - - didLogInfoResolve(true); - }; - /** - * Returns a resolved promise when the targeted services are loaded. - * If no service is found the promise is used directly. - * - * @param {string|RegExp} serviceName name of the service to expect - * or regular expression matching the service. - * @returns {Promise} resolved when the services ares - * loaded. The value is equal to the number of services found. - */ - odoo.ready = async function (serviceName) { - function match(name) { - return typeof serviceName === "string" ? name === serviceName : serviceName.test(name); - } - await Promise.all(jobs.filter((job) => match(job.name)).map((job) => job.promise)); - return Object.keys(factories).filter(match).length; - }; - - odoo.runtimeImport = function (moduleName) { - if (!(moduleName in services)) { - throw new Error(`Service "${moduleName} is not defined or isn't finished loading."`); - } - return services[moduleName]; - }; - - // Automatically log errors detected when loading modules - globalThis.addEventListener("load", function logWhenLoaded() { - const len = jobPromises.length; - Promise.all(jobPromises).then(function () { - if (len === jobPromises.length) { - odoo.log(); - } else { - logWhenLoaded(); - } - }); - }); - - /** - * Visit the list of jobs, and return the first found cycle, if any - * - * @param {any[]} jobs - * @returns {null | string} either a string describing a cycle, or null - */ - function findCycle(jobs) { - // build dependency graph - const dependencyGraph = new Map(); - for (const job of jobs) { - dependencyGraph.set(job.name, job.dependencies); - } - - // helpers - function visitJobs(jobs, visited = new Set()) { - for (const job of jobs) { - const result = visitJob(job, visited); - if (result) { - return result; - } - } - return null; - } - - function visitJob(job, visited) { - if (visited.has(job)) { - const jobs = Array.from(visited).concat([job]); - const index = jobs.indexOf(job); - return jobs - .slice(index) - .map((j) => `"${j}"`) - .join(" => "); - } - const deps = dependencyGraph.get(job); - return deps ? visitJobs(deps, new Set(visited).add(job)) : null; - } - - // visit each root to find cycles - return visitJobs(jobs.map((j) => j.name)); - } - - function displayModuleErrors({ failed, missing, unloaded, cycle }) { - if (window.__odooAssetError) { - // this flag is set when we have an error loading the script for an - // asset. We sometimes see a 404, probably due to the fact that the - // attachment has been deleted. It is unclear in which circumstances - // it can happen, but we sometimes observe it. We could here force a - // reload, but it seems dangerous, since in case of errors, this - // could easily lead to an infinite loop. - return; - } - 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 modal"; - 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/static/src/core/assets.js b/addons/web/static/src/core/assets.js index c6dbc3ba676..043ba9bc2cf 100644 --- a/addons/web/static/src/core/assets.js +++ b/addons/web/static/src/core/assets.js @@ -169,11 +169,6 @@ assets.loadBundle = async function loadBundle(desc) { if (typeof urlData === "string") { // serial loading await assets.loadJS(urlData); - // Wait template if the JavaScript come from bundle. - const bundle = urlData.match(/\/web\/assets\/.*\/([^/]+?)(\.min)?\.js/); - if (bundle) { - await odoo.ready(bundle[1] + ".bundle.xml"); - } } else { // parallel loading await Promise.all(urlData.map(loadJS)); @@ -202,6 +197,7 @@ assets.loadBundle = async function loadBundle(desc) { ); } } + odoo.loader.checkAndReportErrors(); }; export const loadJS = function (url) { diff --git a/addons/web/static/src/core/emoji_picker/emoji_picker.js b/addons/web/static/src/core/emoji_picker/emoji_picker.js index 78368a5d81f..943bff29c09 100644 --- a/addons/web/static/src/core/emoji_picker/emoji_picker.js +++ b/addons/web/static/src/core/emoji_picker/emoji_picker.js @@ -115,7 +115,7 @@ export const loader = { export async function loadEmoji() { try { await loader.loadEmoji(); - return odoo.runtimeImport("@web/core/emoji_picker/emoji_data"); + return odoo.loader.modules.get("@web/core/emoji_picker/emoji_data"); } catch { // Could be intentional (tour ended successfully while emoji still loading) return { emojis: [], categories: [] }; diff --git a/addons/web/static/src/module_loader.js b/addons/web/static/src/module_loader.js new file mode 100644 index 00000000000..996fdba5e1f --- /dev/null +++ b/addons/web/static/src/module_loader.js @@ -0,0 +1,226 @@ +/** + *------------------------------------------------------------------------------ + * Odoo Web Boostrap Code + *------------------------------------------------------------------------------ + */ +(function () { + "use strict"; + + class ModuleLoader { + /** @type {Map} mapping name => deps/fn */ + factories = new Map(); + /** @type {Set} names of modules waiting to be started */ + jobs = new Set(); + /** @type {Set} names of failed modules */ + failed = new Set(); + + /** @type {Map} mapping name => value */ + modules = new Map(); + + bus = new EventTarget(); + + checkErrorProm = null; + + /** + * @param {string} name + * @param {string[]} deps + * @param {Function} factory + */ + define(name, deps, factory) { + if (typeof name !== "string") { + throw new Error(`Invalid name definition: ${name} (should be a string)"`); + } + if (!(deps instanceof Array)) { + throw new Error(`Dependencies should be defined by an array: ${deps}`); + } + if (typeof factory !== "function") { + throw new Error(`Factory should be defined by a function ${factory}`); + } + if (!this.factories.has(name)) { + this.factories.set(name, { + deps, + fn: factory, + ignoreMissingDeps: globalThis.__odooIgnoreMissingDependencies, + }); + this.addJob(name); + this.checkErrorProm ||= Promise.resolve().then(() => { + this.checkAndReportErrors(); + this.checkErrorProm = null; + }); + } + } + + addJob(name) { + this.jobs.add(name); + this.startModules(); + } + + findJob() { + for (const job of this.jobs) { + if (this.factories.get(job).deps.every((dep) => this.modules.has(dep))) { + return job; + } + } + return null; + } + + startModules() { + let job; + while ((job = this.findJob())) { + this.startModule(job); + } + } + + startModule(name) { + const require = (name) => this.modules.get(name); + this.jobs.delete(name); + const factory = this.factories.get(name); + let value = null; + try { + value = factory.fn(require); + } catch (error) { + this.failed.add(name); + throw new Error(`Error while loading "${name}":\n${error}`); + } + this.modules.set(name, value); + this.bus.dispatchEvent( + new CustomEvent("module-started", { detail: { moduleName: name, module: value } }) + ); + } + + findErrors() { + // cycle detection + const dependencyGraph = new Map(); + for (const job of this.jobs) { + dependencyGraph.set(job, this.factories.get(job).deps); + } + function visitJobs(jobs, visited = new Set()) { + for (const job of jobs) { + const result = visitJob(job, visited); + if (result) { + return result; + } + } + return null; + } + + function visitJob(job, visited) { + if (visited.has(job)) { + const jobs = Array.from(visited).concat([job]); + const index = jobs.indexOf(job); + return jobs + .slice(index) + .map((j) => `"${j}"`) + .join(" => "); + } + const deps = dependencyGraph.get(job); + return deps ? visitJobs(deps, new Set(visited).add(job)) : null; + } + + // missing dependencies + const missing = new Set(); + for (const job of this.jobs) { + const factory = this.factories.get(job); + if (factory.ignoreMissingDeps) { + continue; + } + for (const dep of factory.deps) { + if (!this.factories.has(dep)) { + missing.add(dep); + } + } + } + + return { + failed: [...this.failed], + cycle: visitJobs(this.jobs), + missing: [...missing], + unloaded: [...this.jobs].filter((j) => !this.factories.get(j).ignoreMissingDeps), + }; + } + + async checkAndReportErrors() { + const { failed, cycle, missing, unloaded } = this.findErrors(); + if (!failed.length && !unloaded.length) { + return; + } + + function domReady(cb) { + if (document.readyState === "complete") { + cb(); + } else { + document.addEventListener("DOMContentLoaded", cb); + } + } + + function list(heading, names) { + const frag = document.createDocumentFragment(); + if (!names || !names.length) { + return frag; + } + frag.textContent = heading; + const ul = document.createElement("ul"); + for (const el of names) { + const li = document.createElement("li"); + li.textContent = el; + ul.append(li); + } + frag.appendChild(ul); + return frag; + } + + domReady(() => { + // 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 modal"; + 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); + }); + } + } + + if (!globalThis.odoo) { + globalThis.odoo = {}; + } + const odoo = globalThis.odoo; + if (odoo.debug && !new URLSearchParams(location.search).has("debug")) { + // remove debug mode if not explicitely set in url + odoo.debug = ""; + } + + const loader = new ModuleLoader(); + odoo.define = loader.define.bind(loader); + + odoo.loader = loader; +})(); diff --git a/addons/web/static/src/webclient/clickbot/clickbot.js b/addons/web/static/src/webclient/clickbot/clickbot.js index 53e1ac940ce..2604e2e3730 100644 --- a/addons/web/static/src/webclient/clickbot/clickbot.js +++ b/addons/web/static/src/webclient/clickbot/clickbot.js @@ -23,7 +23,7 @@ const { isEnterprise } = odoo.info; const { onWillStart } = owl; let appsMenusOnly = false; - const isStudioInstalled = "@web_studio/studio_service" in odoo.__DEBUG__.services; + const isStudioInstalled = odoo.loader.modules.has("@web_studio/studio_service"); let actionCount = 0; let viewUpdateCount = 0; let studioCount = 0; @@ -49,8 +49,8 @@ actionCount++; }); - const { patch } = odoo.__DEBUG__.services["@web/core/utils/patch"]; - const { WithSearch } = odoo.__DEBUG__.services["@web/search/with_search/with_search"]; + const { patch } = odoo.loader.modules.get("@web/core/utils/patch"); + const { WithSearch } = odoo.loader.modules.get("@web/search/with_search/with_search"); patch(WithSearch.prototype, { setup() { @@ -67,7 +67,7 @@ // This test file is not respecting Odoo module dependencies. // The following module might not be loaded (eg. if mail is not installed). - const DiscussWidgetModule = odoo.__DEBUG__.services["@mail/widgets/discuss/discuss"]; + const DiscussWidgetModule = odoo.loader.modules.get("@mail/widgets/discuss/discuss"); const DiscussWidget = DiscussWidgetModule && DiscussWidgetModule[Symbol.for("default")]; if (DiscussWidget) { DiscussWidget.include({ diff --git a/addons/web/static/tests/dependencies_tests.js b/addons/web/static/tests/dependencies_tests.js index 71b045d504c..971daf3f2e7 100644 --- a/addons/web/static/tests/dependencies_tests.js +++ b/addons/web/static/tests/dependencies_tests.js @@ -9,12 +9,12 @@ function invalidImportsFrom(folder, allowedFolders) { // modules within a folder can always depend on one another allowedFolders.push(folder); - const modulesToCheck = Object.keys(odoo.__DEBUG__.services).filter((module) => + const modulesToCheck = Array.from(odoo.loader.modules.keys()).filter((module) => module.startsWith(`@web/${folder}/`) ); const invalidDeps = {}; for (const module of modulesToCheck) { - const invalid = odoo.__DEBUG__.getDependencies(module).filter((dep) => { + const invalid = odoo.loader.factories.get(module).deps.filter((dep) => { // owl and @web/session are allowed everywhere if (dep === "@odoo/owl" || dep === "@web/session") { return false; diff --git a/addons/web/static/tests/qunit.js b/addons/web/static/tests/qunit.js index 6621a7529b0..bacf1acd6ba 100644 --- a/addons/web/static/tests/qunit.js +++ b/addons/web/static/tests/qunit.js @@ -257,7 +257,7 @@ export function setupQUnit() { modulesAlert.classList.add("alert-info"); modulesAlert.textContent = "Waiting for modules check..."; document.getElementById("qunit").appendChild(modulesAlert); - const info = odoo.__DEBUG__.jsModules; + const info = odoo.loader.findErrors(); if (info.missing.length || info.failed.length || info.unloaded.length) { document.querySelector("#qunit-banner").classList.add("qunit-fail"); modulesAlert.classList.toggle("alert-danger"); @@ -295,7 +295,6 @@ export function setupQUnit() { } } - QUnit.begin(() => odoo.__DEBUG__.didLogInfo); /** * If we want to log several errors, we have to log all of them at once, as * browser_js is closed as soon as an error is logged. diff --git a/addons/web/tests/test_click_everywhere.py b/addons/web/tests/test_click_everywhere.py index 1b2b6aee651..1809b44c732 100644 --- a/addons/web/tests/test_click_everywhere.py +++ b/addons/web/tests/test_click_everywhere.py @@ -14,7 +14,7 @@ class TestMenusAdmin(odoo.tests.HttpCase): for app_id in menus['root']['children']: with self.subTest(app=menus[app_id]['name']): _logger.runbot('Testing %s', menus[app_id]['name']) - self.browser_js("/web", "odoo.__DEBUG__.services['@web/webclient/clickbot/clickbot_loader'].startClickEverywhere('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="admin", timeout=600) + self.browser_js("/web", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="admin", timeout=600) @odoo.tests.tagged('click_all', 'post_install', '-at_install', '-standard') @@ -26,16 +26,16 @@ class TestMenusDemo(odoo.tests.HttpCase): for app_id in menus['root']['children']: with self.subTest(app=menus[app_id]['name']): _logger.runbot('Testing %s', menus[app_id]['name']) - self.browser_js("/web", "odoo.__DEBUG__.services['@web/webclient/clickbot/clickbot_loader'].startClickEverywhere('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="demo", timeout=600) + self.browser_js("/web", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere('%s');" % menus[app_id]['xmlid'], "odoo.isReady === true", login="demo", timeout=600) @odoo.tests.tagged('post_install', '-at_install') class TestMenusAdminLight(odoo.tests.HttpCase): allow_end_on_form = True def test_01_click_apps_menus_as_admin(self): - self.browser_js("/web", "odoo.__DEBUG__.services['@web/webclient/clickbot/clickbot_loader'].startClickEverywhere(undefined, true);", "odoo.isReady === true", login="admin", timeout=120) + self.browser_js("/web", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere(undefined, true);", "odoo.isReady === true", login="admin", timeout=120) @odoo.tests.tagged('post_install', '-at_install',) class TestMenusDemoLight(odoo.tests.HttpCase): allow_end_on_form = True def test_01_click_apps_menus_as_demo(self): - self.browser_js("/web", "odoo.__DEBUG__.services['@web/webclient/clickbot/clickbot_loader'].startClickEverywhere(undefined, true);", "odoo.isReady === true", login="demo", timeout=120) + self.browser_js("/web", "odoo.loader.modules.get('@web/webclient/clickbot/clickbot_loader').startClickEverywhere(undefined, true);", "odoo.isReady === true", login="demo", timeout=120) diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 00ee3462e85..83d4e828126 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -4,8 +4,13 @@ @@ -47,7 +52,9 @@ } - + + + diff --git a/addons/web_editor/__manifest__.py b/addons/web_editor/__manifest__.py index 16f141bada4..4eac49fae24 100644 --- a/addons/web_editor/__manifest__.py +++ b/addons/web_editor/__manifest__.py @@ -49,7 +49,7 @@ Odoo Web Editor widget. 'web/static/src/scss/fontawesome_overridden.scss', 'web/static/src/legacy/js/promise_extension.js', - 'web/static/src/boot.js', + 'web/static/src/module_loader.js', 'web/static/src/session.js', 'web/static/src/legacy/js/core/cookie_utils.js', @@ -260,7 +260,7 @@ Odoo Web Editor widget. ], 'web_editor.mocha_tests': [ 'web/static/src/legacy/js/promise_extension.js', - 'web/static/src/boot.js', + 'web/static/src/module_loader.js', # insert module dependencies here 'web/static/src/core/utils/concurrency.js', diff --git a/addons/web_editor/static/src/js/backend/convert_inline.js b/addons/web_editor/static/src/js/backend/convert_inline.js index 66fef131345..3e90a66bdba 100644 --- a/addons/web_editor/static/src/js/backend/convert_inline.js +++ b/addons/web_editor/static/src/js/backend/convert_inline.js @@ -801,7 +801,7 @@ function flattenBackgroundImages(editable) { */ function fontToImg($editable) { const editable = $editable.get(0); - const { fonts } = odoo.__DEBUG__.services["@web_editor/js/wysiwyg/fonts"]; + const { fonts } = odoo.loader.modules.get("@web_editor/js/wysiwyg/fonts"); for (const font of editable.querySelectorAll('.fa')) { let icon, content; diff --git a/addons/web_editor/static/src/js/frontend/loader.js b/addons/web_editor/static/src/js/frontend/loader.js index 34571216fde..019603386ac 100644 --- a/addons/web_editor/static/src/js/frontend/loader.js +++ b/addons/web_editor/static/src/js/frontend/loader.js @@ -8,12 +8,8 @@ export async function loadLegacyWysiwygAssets(additionnalAssets = []) { } export async function requireLegacyModule(moduleName, loadCallback = () => {}) { - if (!(await odoo.ready(moduleName))) { - await loadCallback(); - await odoo.ready(moduleName); - } - const mod = odoo.__DEBUG__.services[moduleName] - return mod[Symbol.for('default')] || mod; + await loadCallback(); + return odoo.loader.modules.get(moduleName)[Symbol.for('default')] || odoo.loader.modules.get(moduleName); } export async function requireWysiwygLegacyModule(moduleName) { diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index 183db7fb8cf..a3b130e8205 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -164,7 +164,6 @@ 'web.assets_tests': [ 'website/static/tests/tour_utils/**/*', 'website/static/tests/tours/**/*', - 'website/static/src/client_actions/website_preview/website_preview_test_mode.js', ], 'web.assets_backend': [ ('include', 'website.assets_editor'), @@ -175,7 +174,6 @@ 'website/static/src/js/backend/**/*', 'website/static/src/js/tours/tour_utils.js', 'website/static/src/client_actions/*/*', - ('remove', 'website/static/src/client_actions/website_preview/website_preview_test_mode.js'), 'website/static/src/components/fields/*', 'website/static/src/components/fullscreen_indication/fullscreen_indication.js', 'website/static/src/components/fullscreen_indication/fullscreen_indication.scss', @@ -254,6 +252,11 @@ 'website/static/src/js/widgets/link_popover_widget.js', 'website/static/src/xml/website.cookies_bar.xml', ], + 'website.assets_all_wysiwyg': [ + ('include', 'web_editor.assets_wysiwyg'), + ('include', 'web_editor.assets_legacy_wysiwyg'), + ('include', 'website.assets_wysiwyg'), + ], 'web_editor.assets_media_dialog': [ 'website/static/src/components/media_dialog/image_selector.js', ], diff --git a/addons/website/static/src/client_actions/website_preview/website_preview_test_mode.js b/addons/website/static/src/client_actions/website_preview/website_preview_test_mode.js deleted file mode 100644 index 55c12970678..00000000000 --- a/addons/website/static/src/client_actions/website_preview/website_preview_test_mode.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @odoo-module **/ -import { patch } from "@web/core/utils/patch"; -import { WebsitePreview } from '@website/client_actions/website_preview/website_preview'; - -patch(WebsitePreview.prototype, { - /** - * @override - */ - get testMode() { - return true; - } -}); diff --git a/addons/website/static/src/js/content/website_root_instance.js b/addons/website/static/src/js/content/website_root_instance.js index 9b6f7c16c15..5ff2ff4874b 100644 --- a/addons/website/static/src/js/content/website_root_instance.js +++ b/addons/website/static/src/js/content/website_root_instance.js @@ -2,15 +2,15 @@ import { createPublicRoot } from "@web/legacy/js/public/public_root"; import { WebsiteRoot } from "./website_root"; -import { loadLegacyWysiwygAssets } from "@web_editor/js/frontend/loader"; +import { getBundle, loadBundle } from "@web/core/assets"; -export default createPublicRoot(WebsiteRoot).then(rootInstance => { +export default createPublicRoot(WebsiteRoot).then(async (rootInstance) => { // This data attribute is set by the WebsitePreview client action for a // restricted editor user. if (window.frameElement && window.frameElement.dataset.loadWysiwyg === 'true') { - loadLegacyWysiwygAssets(['web_editor.assets_wysiwyg', 'website.assets_wysiwyg']).then(() => { - window.dispatchEvent(new CustomEvent('PUBLIC-ROOT-READY', {detail: {rootInstance}})); - }); + const assets = await getBundle("website.assets_all_wysiwyg"); + await loadBundle(assets); + window.dispatchEvent(new CustomEvent('PUBLIC-ROOT-READY', {detail: {rootInstance}})); } return rootInstance; }); diff --git a/addons/website/static/src/services/website_service.js b/addons/website/static/src/services/website_service.js index bfbafa0b242..c4a25c9ae6b 100644 --- a/addons/website/static/src/services/website_service.js +++ b/addons/website/static/src/services/website_service.js @@ -2,7 +2,7 @@ import { _t } from "@web/core/l10n/translation"; import { registry } from '@web/core/registry'; -import { loadLegacyWysiwygAssets } from '@web_editor/js/frontend/loader'; +import { getBundle, loadBundle } from "@web/core/assets"; import { FullscreenIndication } from '../components/fullscreen_indication/fullscreen_indication'; import { WebsiteLoader } from '../components/website_loader/website_loader'; @@ -221,7 +221,8 @@ export const websiteService = { websites = [...(await orm.searchRead('website', [], ['domain', 'id', 'name']))]; }, async loadWysiwyg() { - await loadLegacyWysiwygAssets(['website.assets_wysiwyg']); + const assets = await getBundle("website.assets_all_wysiwyg"); + await loadBundle(assets); }, blockPreview(showLoader, processId) { if (!blockingProcesses.length) { diff --git a/addons/website/static/tests/tour_utils/focus_blur_snippets_options.js b/addons/website/static/tests/tour_utils/focus_blur_snippets_options.js index 722430dc48f..002ce91e460 100644 --- a/addons/website/static/tests/tour_utils/focus_blur_snippets_options.js +++ b/addons/website/static/tests/tour_utils/focus_blur_snippets_options.js @@ -1,16 +1,18 @@ /** @odoo-module **/ -import options from "@web_editor/js/editor/snippets.options"; - -const FocusBlur = options.Class.extend({ - onFocus() { - window.focusBlurSnippetsResult.push(`focus ${this.focusBlurName}`); - }, - onBlur() { - window.focusBlurSnippetsResult.push(`blur ${this.focusBlurName}`); - }, +odoo.loader.bus.addEventListener("module-started", (e) => { + if (e.detail.moduleName === "@web_editor/js/editor/snippets.options"){ + const options = e.detail.module[Symbol.for("default")]; + const FocusBlur = options.Class.extend({ + onFocus() { + window.focusBlurSnippetsResult.push(`focus ${this.focusBlurName}`); + }, + onBlur() { + window.focusBlurSnippetsResult.push(`blur ${this.focusBlurName}`); + }, + }); + options.registry.FocusBlurParent = FocusBlur.extend({focusBlurName: 'parent'}); + options.registry.FocusBlurChild1 = FocusBlur.extend({focusBlurName: 'child1'}); + options.registry.FocusBlurChild2 = FocusBlur.extend({focusBlurName: 'child2'}); + } }); - -options.registry.FocusBlurParent = FocusBlur.extend({focusBlurName: 'parent'}); -options.registry.FocusBlurChild1 = FocusBlur.extend({focusBlurName: 'child1'}); -options.registry.FocusBlurChild2 = FocusBlur.extend({focusBlurName: 'child2'}); diff --git a/addons/website/static/tests/tour_utils/website_preview_test.js b/addons/website/static/tests/tour_utils/website_preview_test.js new file mode 100644 index 00000000000..30821afb81d --- /dev/null +++ b/addons/website/static/tests/tour_utils/website_preview_test.js @@ -0,0 +1,17 @@ +/** @odoo-module */ + +import { patch } from "@web/core/utils/patch"; + +// It's an optionnal import, to patch only when the WebsitePreview is loaded. +const WebsitePreviewLoader = odoo.loader.modules.get("@website/client_actions/website_preview/website_preview"); + +if (WebsitePreviewLoader) { + patch(WebsitePreviewLoader.WebsitePreview.prototype, { + /** + * @override + */ + get testMode() { + return true; + } + }); +} diff --git a/addons/website/static/tests/tours/public_user_editor_dep_widget.js b/addons/website/static/tests/tours/public_user_editor_dep_widget.js index f46b23609f0..e30b7244b60 100644 --- a/addons/website/static/tests/tours/public_user_editor_dep_widget.js +++ b/addons/website/static/tests/tours/public_user_editor_dep_widget.js @@ -1,16 +1,18 @@ -/** @odoo-module **/ +odoo.loader.bus.addEventListener("module-started", (e) => { + if (e.detail.moduleName === "@web_editor/js/frontend/loadWysiwygFromTextarea") { + const publicWidget = odoo.loader.modules.get("@web/legacy/js/public/public_widget")[Symbol.for('default')]; + const { loadWysiwygFromTextarea } = e.detail.module; -import publicWidget from "@web/legacy/js/public/public_widget"; -import { loadWysiwygFromTextarea } from "@web_editor/js/frontend/loadWysiwygFromTextarea"; + publicWidget.registry['public_user_editor_test'] = publicWidget.Widget.extend({ + selector: 'textarea.o_public_user_editor_test_textarea', -publicWidget.registry['public_user_editor_test'] = publicWidget.Widget.extend({ - selector: 'textarea.o_public_user_editor_test_textarea', - - /** - * @override - */ - start: async function () { - await this._super(...arguments); - await loadWysiwygFromTextarea(this, this.el, {}); - }, -}); + /** + * @override + */ + start: async function () { + await this._super(...arguments); + await loadWysiwygFromTextarea(this, this.el, {}); + }, + }); + } +}) diff --git a/addons/website/static/tests/tours/rte.js b/addons/website/static/tests/tours/rte.js index 3a932f3ad64..c4ade3ac829 100644 --- a/addons/website/static/tests/tours/rte.js +++ b/addons/website/static/tests/tours/rte.js @@ -1,7 +1,6 @@ /** @odoo-module **/ import wTourUtils from "@website/js/tours/tour_utils"; -import { Wysiwyg } from "@web_editor/js/wysiwyg/wysiwyg"; import { whenReady } from "@odoo/owl"; wTourUtils.registerWebsitePreviewTour('rte_translator', { @@ -113,6 +112,7 @@ wTourUtils.dragNDrop({ trigger: 'iframe #wrap p font:first', run: function (actionHelper) { actionHelper.text('translated Parseltongue text'); + const { Wysiwyg } = odoo.loader.modules.get('@web_editor/js/wysiwyg/wysiwyg'); Wysiwyg.setRange(this.$anchor.contents()[0], 22); this.$anchor.trigger($.Event("keyup", {key: '_', keyCode: 95})); this.$anchor.trigger('input'); @@ -123,6 +123,7 @@ wTourUtils.dragNDrop({ run: function (actionHelper) { actionHelper.click(); this.$anchor.prepend('<{translated}>'); + const { Wysiwyg } = odoo.loader.modules.get('@web_editor/js/wysiwyg/wysiwyg'); Wysiwyg.setRange(this.$anchor.contents()[0], 0); this.$anchor.trigger($.Event("keyup", {key: '_', keyCode: 95})); this.$anchor.trigger('input'); @@ -190,6 +191,7 @@ wTourUtils.dragNDrop({ mousedown.initMouseEvent('mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); el.dispatchEvent(mousedown); var mouseup = document.createEvent('MouseEvents'); + const { Wysiwyg } = odoo.loader.modules.get('@web_editor/js/wysiwyg/wysiwyg'); Wysiwyg.setRange(el.childNodes[2], 6, el.childNodes[2], 13); mouseup.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); el.dispatchEvent(mouseup); diff --git a/addons/website/static/tests/tours/snippets_all_drag_and_drop.js b/addons/website/static/tests/tours/snippets_all_drag_and_drop.js index 264a19a9887..91406074b55 100644 --- a/addons/website/static/tests/tours/snippets_all_drag_and_drop.js +++ b/addons/website/static/tests/tours/snippets_all_drag_and_drop.js @@ -1,17 +1,19 @@ /** @odoo-module **/ -import { WysiwygAdapterComponent } from "@website/components/wysiwyg_adapter/wysiwyg_adapter"; import websiteTourUtils from "@website/js/tours/tour_utils"; import { patch } from "@web/core/utils/patch"; -const patchWysiwygAdapter = () => patch(WysiwygAdapterComponent.prototype, { - _trigger_up(ev) { - super._trigger_up(...arguments); - if (ev.name === 'snippet_removed') { - $('body').attr('test-dd-snippet-removed', true); +const patchWysiwygAdapter = () => { + const { WysiwygAdapterComponent } = odoo.loader.modules.get("@website/components/wysiwyg_adapter/wysiwyg_adapter"); + return patch(WysiwygAdapterComponent.prototype, { + _trigger_up(ev) { + super._trigger_up(...arguments); + if (ev.name === 'snippet_removed') { + $('body').attr('test-dd-snippet-removed', true); + } } - } -}); + }); +}; let unpatchWysiwygAdapter = null; diff --git a/addons/website_blog/__manifest__.py b/addons/website_blog/__manifest__.py index 3c295939672..2a4067d06b4 100644 --- a/addons/website_blog/__manifest__.py +++ b/addons/website_blog/__manifest__.py @@ -32,10 +32,10 @@ 'assets': { 'website.assets_wysiwyg': [ 'website_blog/static/src/js/options.js', - 'website_blog/static/src/js/wysiwyg.js', 'website_blog/static/src/snippets/s_blog_posts/options.js', ], 'website.assets_editor': [ + 'website_blog/static/src/js/wysiwyg_adapter.js', 'website_blog/static/src/js/tours/website_blog.js', 'website_blog/static/src/js/systray_items/*.js', ], diff --git a/addons/website_blog/static/src/js/wysiwyg.js b/addons/website_blog/static/src/js/wysiwyg_adapter.js similarity index 98% rename from addons/website_blog/static/src/js/wysiwyg.js rename to addons/website_blog/static/src/js/wysiwyg_adapter.js index e89def38a1e..bde32850f52 100644 --- a/addons/website_blog/static/src/js/wysiwyg.js +++ b/addons/website_blog/static/src/js/wysiwyg_adapter.js @@ -1,7 +1,6 @@ /** @odoo-module **/ import { WysiwygAdapterComponent } from '@website/components/wysiwyg_adapter/wysiwyg_adapter'; -import "@website/js/editor/snippets.options"; import { patch } from "@web/core/utils/patch"; patch(WysiwygAdapterComponent.prototype, { diff --git a/addons/website_livechat/static/tests/tours/website_livechat_common.js b/addons/website_livechat/static/tests/tours/website_livechat_common.js index 974e0fe1065..c1f308e7696 100644 --- a/addons/website_livechat/static/tests/tours/website_livechat_common.js +++ b/addons/website_livechat/static/tests/tours/website_livechat_common.js @@ -1,7 +1,5 @@ /** @odoo-module **/ -import { RATING } from "@im_livechat/embed/core/livechat_service"; - /******************************* * Common Steps *******************************/ @@ -55,6 +53,13 @@ export const endDiscussion = [ ]; export const feedback = [ + { + content: "Patching Livechat", + trigger: "textarea[placeholder='Explain your note']", + run: function() { + document.body.classList.add("feedback_sent"); + } + }, { content: "Type a feedback", trigger: "textarea[placeholder='Explain your note']", @@ -114,20 +119,20 @@ export const close = [ export const goodRating = [ { content: "Choose Good Rating", - trigger: `img[src*=rating][alt=${RATING.GOOD}]`, + trigger: `img[src*=rating][alt=5]`, }, ]; export const okRating = [ { content: "Choose ok Rating", - trigger: `img[src*=rating][alt=${RATING.OK}]`, + trigger: `img[src*=rating][alt=3]`, }, ]; export const sadRating = [ { content: "Choose bad Rating", - trigger: `img[src*=rating][alt=${RATING.BAD}]`, + trigger: `img[src*=rating][alt=1]`, }, ]; diff --git a/addons/website_livechat/static/tests/tours/website_livechat_request.js b/addons/website_livechat/static/tests/tours/website_livechat_request.js index 916e1c048b0..e1ed34580b3 100644 --- a/addons/website_livechat/static/tests/tours/website_livechat_request.js +++ b/addons/website_livechat/static/tests/tours/website_livechat_request.js @@ -3,30 +3,34 @@ import { patch } from "@web/core/utils/patch"; import { endDiscussion, okRating, feedback, transcript, close } from "./website_livechat_common"; import { registry } from "@web/core/registry"; -import { LivechatService, ODOO_VERSION_KEY } from "@im_livechat/embed/core/livechat_service"; -// Livechat sessions are deleted if they come from a different odoo version than -// the current one. Since each test is run with a different browser the version -// is never set. This patch sets the version to the current one. -patch(LivechatService.prototype, { - async initialize() { - try { - const response = await fetch("/web/webclient/version_info", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({}), - }); - const { server_version: serverVersion } = (await response.json()).result; - localStorage.setItem(ODOO_VERSION_KEY, serverVersion); - } catch { - // Could be intentional (tour ended successfully while version still loading) - } - - return super.initialize(...arguments); - }, -}); +odoo.loader.bus.addEventListener("module-started", (e) => { + if (e.detail.moduleName === "@im_livechat/embed/core/livechat_service") { + const { LivechatService, ODOO_VERSION_KEY } = e.detail.module; + // Livechat sessions are deleted if they come from a different odoo version than + // the current one. Since each test is run with a different browser the version + // is never set. This patch sets the version to the current one. + patch(LivechatService.prototype, { + async initialize() { + try { + const response = await fetch("/web/webclient/version_info", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({}), + }); + const { server_version: serverVersion } = (await response.json()).result; + localStorage.setItem(ODOO_VERSION_KEY, serverVersion); + } catch { + // Could be intentional (tour ended successfully while version still loading) + } + + return super.initialize(...arguments); + }, + }); + } +}) const chatRequest = [ { diff --git a/addons/website_sale/static/src/js/components/wysiwyg_adapter/wysiwyg_adapter.js b/addons/website_sale/static/src/js/components/wysiwyg_adapter/wysiwyg_adapter.js index 0878f37b52e..37f53c29ef4 100644 --- a/addons/website_sale/static/src/js/components/wysiwyg_adapter/wysiwyg_adapter.js +++ b/addons/website_sale/static/src/js/components/wysiwyg_adapter/wysiwyg_adapter.js @@ -2,10 +2,205 @@ import { WysiwygAdapterComponent } from '@website/components/wysiwyg_adapter/wysiwyg_adapter'; import { patch } from "@web/core/utils/patch"; +import { Markup } from "@web/legacy/js/core/utils"; -// TODO this whole patch actually seems unnecessary. The bug it solved seems -// to stay solved if this is removed. To investigate. patch(WysiwygAdapterComponent.prototype, { + /** + * @override + */ + async init() { + await super.init(...arguments); + + let ribbons = []; + if (this._isProductListPage()) { + ribbons = await this.orm.searchRead( + 'product.ribbon', + [], + ['id', 'html', 'bg_color', 'text_color', 'html_class'], + ); + } + this.ribbons = Object.fromEntries(ribbons.map(ribbon => { + ribbon.html = Markup(ribbon.html); + return [ribbon.id, ribbon]; + })); + this.originalRibbons = Object.assign({}, this.ribbons); + this.productTemplatesRibbons = []; + this.deletedRibbonClasses = ''; + }, + /** + * @override + */ + async _saveViewBlocks() { + await this._saveRibbons(); + return super._saveViewBlocks(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Saves the ribbons in the database. + * + * @private + */ + async _saveRibbons() { + if (!this._isProductListPage()) { + return; + } + const originalIds = Object.keys(this.originalRibbons).map(id => parseInt(id)); + const currentIds = Object.keys(this.ribbons).map(id => parseInt(id)); + + const ribbons = Object.values(this.ribbons); + const created = ribbons.filter(ribbon => !originalIds.includes(ribbon.id)); + const deletedIds = originalIds.filter(id => !currentIds.includes(id)); + const modified = ribbons.filter(ribbon => { + if (created.includes(ribbon)) { + return false; + } + const original = this.originalRibbons[ribbon.id]; + return Object.entries(ribbon).some(([key, value]) => value !== original[key]); + }); + + const proms = []; + let createdRibbonIds; + if (created.length > 0) { + proms.push(this.orm.create( + 'product.ribbon', + created.map(ribbon => { + ribbon = Object.assign({}, ribbon); + delete ribbon.id; + return ribbon; + }), + ).then(ids => createdRibbonIds = ids)); + } + + modified.forEach(ribbon => proms.push(this.orm.write( + 'product.ribbon', + [ribbon.id], + ribbon, + ))); + + if (deletedIds.length > 0) { + proms.push(this.orm.unlink( + 'product.ribbon', + deletedIds, + )); + } + await Promise.all(proms); + const localToServer = Object.assign( + this.ribbons, + Object.fromEntries(created.map((ribbon, index) => [ribbon.id, {id: createdRibbonIds[index]}])), + {'false': {id: false}}, + ); + + // Building the final template to ribbon-id map + const finalTemplateRibbons = this.productTemplatesRibbons.reduce((acc, {templateId, ribbonId}) => { + acc[templateId] = ribbonId; + return acc; + }, {}); + // Inverting the relationship so that we have all templates that have the same ribbon to reduce RPCs + const ribbonTemplates = Object.entries(finalTemplateRibbons).reduce((acc, [templateId, ribbonId]) => { + if (!acc[ribbonId]) { + acc[ribbonId] = []; + } + acc[ribbonId].push(parseInt(templateId)); + return acc; + }, {}); + const setProductTemplateRibbons = Object.entries(ribbonTemplates) + // If the ribbonId that the template had no longer exists, remove the ribbon (id = false) + .map(([ribbonId, templateIds]) => { + const id = currentIds.includes(parseInt(ribbonId)) ? ribbonId : false; + return [id, templateIds]; + }).map(([ribbonId, templateIds]) => this.orm.write( + 'product.template', + templateIds, + {'website_ribbon_id': localToServer[ribbonId].id}, + )); + return Promise.all(setProductTemplateRibbons); + }, + /** + * Checks whether the current page is the product list. + * + * @private + */ + _isProductListPage() { + return this.options.editable && this.options.editable.find('#products_grid').length !== 0; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Returns a copy of this.ribbons through a callback. + * + * @private + */ + _onGetRibbons(ev) { + ev.data.callback(Object.assign({}, this.ribbons)); + }, + /** + * Returns all ribbon classes, current and deleted, so they can be removed. + * + * @private + */ + _onGetRibbonClasses(ev) { + const classes = Object.values(this.ribbons).reduce((classes, ribbon) => { + return classes + ` ${ribbon.html_class}`; + }, '') + this.deletedRibbonClasses; + ev.data.callback(classes); + }, + /** + * Deletes a ribbon. + * + * @private + */ + _onDeleteRibbon(ev) { + this.deletedRibbonClasses += ` ${this.ribbons[ev.data.id].html_class}`; + delete this.ribbons[ev.data.id]; + }, + /** + * Sets a ribbon; + * + * @private + */ + _onSetRibbon(ev) { + const {ribbon} = ev.data; + const previousRibbon = this.ribbons[ribbon.id]; + if (previousRibbon) { + this.deletedRibbonClasses += ` ${previousRibbon.html_class}`; + } + this.ribbons[ribbon.id] = ribbon; + }, + /** + * Sets which ribbon is used by a product template. + * + * @private + */ + _onSetProductRibbon(ev) { + const {templateId, ribbonId} = ev.data; + this.productTemplatesRibbons.push({templateId, ribbonId}); + }, + /** + * @override + */ + _trigger_up(ev) { + const methods = { + get_ribbons: this._onGetRibbons.bind(this), + get_ribbon_classes: this._onGetRibbonClasses.bind(this), + delete_ribbon: this._onDeleteRibbon.bind(this), + set_ribbon: this._onSetRibbon.bind(this), + set_product_ribbon: this._onSetProductRibbon.bind(this), + } + if (methods[ev.name]) { + return methods[ev.name](ev); + } else { + return super._trigger_up(...arguments); + } + }, + // TODO this whole patch actually seems unnecessary. The bug it solved seems + // to stay solved if this is removed. To investigate. /** * @override */ diff --git a/addons/website_sale/static/src/js/website_sale.editor.js b/addons/website_sale/static/src/js/website_sale.editor.js index c2d91d84a56..66c25ba86d8 100644 --- a/addons/website_sale/static/src/js/website_sale.editor.js +++ b/addons/website_sale/static/src/js/website_sale.editor.js @@ -1,212 +1,12 @@ /** @odoo-module **/ import options from "@web_editor/js/editor/snippets.options"; -import { WysiwygAdapterComponent } from '@website/components/wysiwyg_adapter/wysiwyg_adapter'; import { MediaDialog } from "@web_editor/components/media_dialog/media_dialog"; import { _t } from "@web/core/l10n/translation"; -import { Markup } from "@web/legacy/js/core/utils"; import Dialog from "@web/legacy/js/core/dialog"; import "@website/js/editor/snippets.options"; -import { patch } from "@web/core/utils/patch"; import { renderToElement } from "@web/core/utils/render"; -patch(WysiwygAdapterComponent.prototype, { - /** - * @override - */ - async init() { - await super.init(...arguments); - - let ribbons = []; - if (this._isProductListPage()) { - ribbons = await this.orm.searchRead( - 'product.ribbon', - [], - ['id', 'html', 'bg_color', 'text_color', 'html_class'], - ); - } - this.ribbons = Object.fromEntries(ribbons.map(ribbon => { - ribbon.html = Markup(ribbon.html); - return [ribbon.id, ribbon]; - })); - this.originalRibbons = Object.assign({}, this.ribbons); - this.productTemplatesRibbons = []; - this.deletedRibbonClasses = ''; - }, - /** - * @override - */ - async _saveViewBlocks() { - await this._saveRibbons(); - return super._saveViewBlocks(...arguments); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Saves the ribbons in the database. - * - * @private - */ - async _saveRibbons() { - if (!this._isProductListPage()) { - return; - } - const originalIds = Object.keys(this.originalRibbons).map(id => parseInt(id)); - const currentIds = Object.keys(this.ribbons).map(id => parseInt(id)); - - const ribbons = Object.values(this.ribbons); - const created = ribbons.filter(ribbon => !originalIds.includes(ribbon.id)); - const deletedIds = originalIds.filter(id => !currentIds.includes(id)); - const modified = ribbons.filter(ribbon => { - if (created.includes(ribbon)) { - return false; - } - const original = this.originalRibbons[ribbon.id]; - return Object.entries(ribbon).some(([key, value]) => value !== original[key]); - }); - - const proms = []; - let createdRibbonIds; - if (created.length > 0) { - proms.push(this.orm.create( - 'product.ribbon', - created.map(ribbon => { - ribbon = Object.assign({}, ribbon); - delete ribbon.id; - return ribbon; - }), - ).then(ids => createdRibbonIds = ids)); - } - - modified.forEach(ribbon => proms.push(this.orm.write( - 'product.ribbon', - [ribbon.id], - ribbon, - ))); - - if (deletedIds.length > 0) { - proms.push(this.orm.unlink( - 'product.ribbon', - deletedIds, - )); - } - await Promise.all(proms); - const localToServer = Object.assign( - this.ribbons, - Object.fromEntries(created.map((ribbon, index) => [ribbon.id, {id: createdRibbonIds[index]}])), - {'false': {id: false}}, - ); - - // Building the final template to ribbon-id map - const finalTemplateRibbons = this.productTemplatesRibbons.reduce((acc, {templateId, ribbonId}) => { - acc[templateId] = ribbonId; - return acc; - }, {}); - // Inverting the relationship so that we have all templates that have the same ribbon to reduce RPCs - const ribbonTemplates = Object.entries(finalTemplateRibbons).reduce((acc, [templateId, ribbonId]) => { - if (!acc[ribbonId]) { - acc[ribbonId] = []; - } - acc[ribbonId].push(parseInt(templateId)); - return acc; - }, {}); - const setProductTemplateRibbons = Object.entries(ribbonTemplates) - // If the ribbonId that the template had no longer exists, remove the ribbon (id = false) - .map(([ribbonId, templateIds]) => { - const id = currentIds.includes(parseInt(ribbonId)) ? ribbonId : false; - return [id, templateIds]; - }).map(([ribbonId, templateIds]) => this.orm.write( - 'product.template', - templateIds, - {'website_ribbon_id': localToServer[ribbonId].id}, - )); - return Promise.all(setProductTemplateRibbons); - }, - /** - * Checks whether the current page is the product list. - * - * @private - */ - _isProductListPage() { - return this.options.editable && this.options.editable.find('#products_grid').length !== 0; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Returns a copy of this.ribbons through a callback. - * - * @private - */ - _onGetRibbons(ev) { - ev.data.callback(Object.assign({}, this.ribbons)); - }, - /** - * Returns all ribbon classes, current and deleted, so they can be removed. - * - * @private - */ - _onGetRibbonClasses(ev) { - const classes = Object.values(this.ribbons).reduce((classes, ribbon) => { - return classes + ` ${ribbon.html_class}`; - }, '') + this.deletedRibbonClasses; - ev.data.callback(classes); - }, - /** - * Deletes a ribbon. - * - * @private - */ - _onDeleteRibbon(ev) { - this.deletedRibbonClasses += ` ${this.ribbons[ev.data.id].html_class}`; - delete this.ribbons[ev.data.id]; - }, - /** - * Sets a ribbon; - * - * @private - */ - _onSetRibbon(ev) { - const {ribbon} = ev.data; - const previousRibbon = this.ribbons[ribbon.id]; - if (previousRibbon) { - this.deletedRibbonClasses += ` ${previousRibbon.html_class}`; - } - this.ribbons[ribbon.id] = ribbon; - }, - /** - * Sets which ribbon is used by a product template. - * - * @private - */ - _onSetProductRibbon(ev) { - const {templateId, ribbonId} = ev.data; - this.productTemplatesRibbons.push({templateId, ribbonId}); - }, - /** - * @override - */ - _trigger_up(ev) { - const methods = { - get_ribbons: this._onGetRibbons.bind(this), - get_ribbon_classes: this._onGetRibbonClasses.bind(this), - delete_ribbon: this._onDeleteRibbon.bind(this), - set_ribbon: this._onSetRibbon.bind(this), - set_product_ribbon: this._onSetProductRibbon.bind(this), - } - if (methods[ev.name]) { - return methods[ev.name](ev); - } else { - return super._trigger_up(...arguments); - } - } -}); - options.registry.WebsiteSaleGridLayout = options.Class.extend({ /** diff --git a/addons/website_sale/static/tests/tours/website_sale_google_analytics.js b/addons/website_sale/static/tests/tours/website_sale_google_analytics.js index 08aceeeff83..4902bfdca28 100644 --- a/addons/website_sale/static/tests/tours/website_sale_google_analytics.js +++ b/addons/website_sale/static/tests/tours/website_sale_google_analytics.js @@ -1,20 +1,25 @@ /** @odoo-module **/ import { registry } from "@web/core/registry"; -import websiteSaleTracking from "@website_sale/js/website_sale_tracking"; import tourUtils from "@website_sale/js/tours/tour_utils"; +odoo.loader.bus.addEventListener("module-started", (e) => { + if (e.detail.moduleName === "@website_sale/js/website_sale_tracking") { + //import websiteSaleTracking from "@website_sale/js/website_sale_tracking"; + e.detail.module[Symbol.for("default")].include({ + // Purposely don't call super to avoid call to third party (GA) during tests + _onViewItem(event, data) { + $('body').attr('view-event-id', data.item_id); + }, + _onAddToCart(event, data) { + $('body').attr('cart-event-id', data.item_id); + }, + }); + } +}); + let itemId; -websiteSaleTracking.include({ - // Purposely don't call super to avoid call to third party (GA) during tests - _onViewItem(event, data) { - $('body').attr('view-event-id', data.item_id); - }, - _onAddToCart(event, data) { - $('body').attr('cart-event-id', data.item_id); - }, -}); registry.category("web_tour.tours").add('google_analytics_view_item', { test: true, diff --git a/addons/website_slides/__manifest__.py b/addons/website_slides/__manifest__.py index 9290fdec5f9..629ed5a07ef 100644 --- a/addons/website_slides/__manifest__.py +++ b/addons/website_slides/__manifest__.py @@ -149,7 +149,7 @@ Featuring 'website/static/src/libs/zoomodoo/zoomodoo.scss', 'web/static/src/legacy/js/promise_extension.js', - 'web/static/src/boot.js', + 'web/static/src/module_loader.js', 'web/static/src/session.js', 'web/static/src/legacy/js/core/cookie_utils.js', diff --git a/addons/website_slides/static/tests/tours/slides_course_member_yt.js b/addons/website_slides/static/tests/tours/slides_course_member_yt.js index 1e5d58a2788..b93fad0c483 100644 --- a/addons/website_slides/static/tests/tours/slides_course_member_yt.js +++ b/addons/website_slides/static/tests/tours/slides_course_member_yt.js @@ -1,23 +1,26 @@ /** @odoo-module **/ import { registry } from "@web/core/registry"; -import FullScreen from '@website_slides/js/slides_course_fullscreen_player'; -/** - * Alter this method for test purposes. - * This will make the video start at 10 minutes. - * As it lasts 10min24s, it will mark it as completed immediately. - */ -FullScreen.include({ - _renderSlide: function () { +function patchFullScreen(){ + /** + * Alter this method for test purposes. + * This will make the video start at 10 minutes. + * As it lasts 10min24s, it will mark it as completed immediately. + */ + const FullScreen = odoo.loader.modules.get('@website_slides/js/slides_course_fullscreen_player')[Symbol.for("default")]; + FullScreen.include({ + _renderSlide: function () { - var slide = this.get('slide'); - slide.embedUrl += '&start=260'; - this.set('slide', slide); + var slide = this.get('slide'); + slide.embedUrl += '&start=260'; + this.set('slide', slide); + + return this._super.call(this, arguments); + } + }); +} - return this._super.call(this, arguments); - } -}); /** * Global use case: @@ -32,6 +35,13 @@ registry.category("web_tour.tours").add('course_member_youtube', { url: '/slides', test: true, steps: () => [ +{ + content: "Patching FullScreen", + trigger: 'body', + run: function() { + patchFullScreen() + } +}, // eLearning: go on /all, find free course and join it { trigger: 'a.o_wslides_home_all_slides'