[IMP] *: rewrite module loader
This commit rewrite the module loader to simplify it and to improve error handling. before this commit, you could have silent errors or "lucky" imports (import a module based on the fact that his asset will surely be load and if not, the error would be silent anyway). e.g.: The tours cannot import modules extern to tours' bundle anymore. Or load module before your libs in assets. TASK ID: 3266021 closes odoo/odoo#128502 Related: odoo/enterprise#44100 Signed-off-by: Bastien Pierre (ipb) <ipb@odoo.com>
This commit is contained in:
committed by
Bastien PIERRE
parent
1d33b7a9b8
commit
e0fdb6a0b4
@@ -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/*',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
__session_info__: <t t-out="json.dumps(session_info)"/>,
|
||||
csrf_token: "<t t-out="request.csrf_token(None)"/>",
|
||||
debug: "<t t-out="debug"/>",
|
||||
discuss_data: <t t-out="json.dumps(data)"/>
|
||||
};
|
||||
{
|
||||
const { user_context, cache_hashes } = odoo.__session_info__;
|
||||
@@ -25,13 +26,6 @@
|
||||
direction: <t t-out="env['res.lang']._lang_get(lang or env.user.lang).direction or 'ltr'" />;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
odoo.define('mail.discuss_public_template', [], function() {
|
||||
return {
|
||||
data: <t t-out="json.dumps(data)"/>,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body class="d-flex flex-column h-100">
|
||||
<t t-out="body or ''"/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
odoo.loadMenusPromise = Promise.resolve();
|
||||
</script>
|
||||
|
||||
<t t-call="web.conditional_assets_tests"/>
|
||||
<t t-call="web.conditional_assets_tests">
|
||||
<t t-set="ignore_missing_deps" t-value="True"/>
|
||||
</t>
|
||||
<t t-call-assets="point_of_sale.assets_prod" />
|
||||
</head>
|
||||
<body class="pos">
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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) });
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(), [
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<number>} 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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
*------------------------------------------------------------------------------
|
||||
* Odoo Web Boostrap Code
|
||||
*------------------------------------------------------------------------------
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
class ModuleLoader {
|
||||
/** @type {Map<string,{fn: Function, deps: string[]}>} mapping name => deps/fn */
|
||||
factories = new Map();
|
||||
/** @type {Set<string>} names of modules waiting to be started */
|
||||
jobs = new Set();
|
||||
/** @type {Set<string>} names of failed modules */
|
||||
failed = new Set();
|
||||
|
||||
/** @type {Map<string,any>} 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;
|
||||
})();
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,8 +4,13 @@
|
||||
<!-- Call this template instead of "web.assets_tests" to have the proper conditional check -->
|
||||
<template id="conditional_assets_tests" name="Tests Assets Bundle">
|
||||
<t t-if="'tests' in debug or test_mode_enabled">
|
||||
<!-- To allow all tests assets and tours to be in the same bundle, the assets_tests bundle ignores missing dependencies -->
|
||||
<t t-call-assets="web.__assets_tests_call__" defer_load="True" />
|
||||
<t t-if="ignore_missing_deps">
|
||||
<!-- FIXME: This is only to ignore the errors for the lazy loading. To allow all tests assets and tours to be in the same bundle, the assets_tests bundle ignores missing dependencies -->
|
||||
<t t-call-assets="web.__assets_tests_call__" defer_load="True" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call-assets="web.assets_tests" defer_load="True" />
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
@@ -47,7 +52,9 @@
|
||||
}
|
||||
</script>
|
||||
<t t-call-assets="web.assets_frontend_minimal" t-css="false" defer_load="True"/>
|
||||
<t t-call="web.conditional_assets_tests"/>
|
||||
<t t-call="web.conditional_assets_tests">
|
||||
<t t-set="ignore_missing_deps" t-value="True"/>
|
||||
</t>
|
||||
<t t-call-assets="web.assets_frontend_lazy" t-css="false" lazy_load="True"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-out='0']" position="replace">
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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, {});
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
-1
@@ -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, {
|
||||
@@ -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]`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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({
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user