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'