From 14bffd983e3b6c6bcd3aa74299c2f4f65100b60c Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 31 May 2021 07:04:45 +0200 Subject: [PATCH] [REF] *: adapt code to new owl webclient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adapts the community codebase to the rewriting of the /web application in owl. Co-authored-by: Aaron Bohy Co-authored-by: Bruno Boi Co-authored-by: Géry Debongnie Co-authored-by: Samuel Degueldre Co-authored-by: Mathieu Duckerts-Antoine Co-authored-by: Simon Genin (ges) Co-authored-by: Francois (fge) Co-authored-by: Michael Mattiello (mcm) Co-authored-by: Julien Mougenot Co-authored-by: Lucas Perais (lpe) Co-authored-by: Jorge Pinna Puissant --- addons/account/__manifest__.py | 4 +- .../static/src/js/import_action.js | 22 +- addons/bus/__manifest__.py | 5 +- .../js/services/assets_watchdog_service.js | 92 +++ addons/bus/static/src/js/web_client_bus.js | 109 ---- .../bus/static/tests/assets_watchdog_tests.js | 86 +++ addons/bus/static/tests/bus_tests_tour.js | 25 - addons/bus/tests/test_assetsbundle.py | 3 +- addons/calendar/__manifest__.py | 1 + .../calendar/static/src/js/base_calendar.js | 106 ---- .../services/calendar_notification_service.js | 108 ++++ .../static/src/xml/notification_calendar.xml | 19 - .../tests/calendar_notification_tests.js | 233 ++++++++ addons/hr_attendance/__manifest__.py | 2 +- .../static/src/legacy/public_livechat.js | 2 +- .../attachment_viewer/attachment_viewer.js | 2 +- addons/mail/static/src/js/document_viewer.js | 2 +- .../mail/static/src/js/tools/debug_manager.js | 61 +- .../mail/static/src/models/discuss/discuss.js | 2 +- addons/mail/static/src/utils/test_utils.js | 38 +- addons/mail/static/src/xml/thread.xml | 8 - addons/mail/static/tests/activity_tests.js | 202 ++++--- .../static/tests/tools/debug_manager_tests.js | 118 ++-- .../mass_mailing/static/src/js/unsubscribe.js | 2 +- .../static/src/js/web_company_autocomplete.js | 32 +- addons/point_of_sale/__manifest__.py | 37 ++ .../static/src/js/chrome_adapter.js | 48 ++ .../static/src/js/debug_manager.js | 33 +- addons/point_of_sale/static/src/js/main.js | 73 +-- .../static/src/xml/debug_manager.xml | 10 - .../static/tests/tours/point_of_sale.js | 2 +- .../point_of_sale/views/pos_assets_index.xml | 3 +- .../point_of_sale/views/pos_assets_qunit.xml | 40 +- addons/pos_restaurant/__manifest__.py | 4 +- .../views/pos_restaurant_templates.xml | 10 - .../tests/product_pricelist_report_test.js | 61 +- .../static/tests/tours/sale_timesheet_tour.js | 8 +- ...stock_traceability_report_backend_tests.js | 69 +-- addons/web_tour/__manifest__.py | 6 +- .../static/src/debug/debug_manager.js | 47 ++ .../static/src/debug/tour_dialog_component.js | 43 ++ .../src/debug/tour_dialog_component.xml | 61 ++ .../web_tour/static/src/js/debug_manager.js | 81 --- addons/web_tour/static/src/js/tour_manager.js | 3 +- .../web_tour/static/src/js/tour_step_utils.js | 2 +- .../static/src/services/tour_service.js | 93 +++ .../web_tour/static/src/xml/debug_manager.xml | 54 -- addons/website/__manifest__.py | 2 +- .../static/tests/tours/dashboard_tour.js | 43 +- addons/website_blog/static/src/js/options.js | 2 +- .../base/static/tests/base_settings_tests.js | 539 ++++++++++-------- odoo/addons/test_lint/tests/test_eslint.py | 1 + odoo/addons/test_lint/tests/test_markers.py | 2 + .../static/tests/tours/main_flow.js | 35 +- .../test_new_api/static/tests/tours/x2many.js | 8 +- 55 files changed, 1592 insertions(+), 1112 deletions(-) create mode 100644 addons/bus/static/src/js/services/assets_watchdog_service.js delete mode 100644 addons/bus/static/src/js/web_client_bus.js create mode 100644 addons/bus/static/tests/assets_watchdog_tests.js delete mode 100644 addons/bus/static/tests/bus_tests_tour.js create mode 100644 addons/calendar/static/src/js/services/calendar_notification_service.js delete mode 100644 addons/calendar/static/src/xml/notification_calendar.xml create mode 100644 addons/calendar/static/tests/calendar_notification_tests.js create mode 100644 addons/point_of_sale/static/src/js/chrome_adapter.js delete mode 100644 addons/point_of_sale/static/src/xml/debug_manager.xml delete mode 100644 addons/pos_restaurant/views/pos_restaurant_templates.xml create mode 100644 addons/web_tour/static/src/debug/debug_manager.js create mode 100644 addons/web_tour/static/src/debug/tour_dialog_component.js create mode 100644 addons/web_tour/static/src/debug/tour_dialog_component.xml delete mode 100644 addons/web_tour/static/src/js/debug_manager.js create mode 100644 addons/web_tour/static/src/services/tour_service.js delete mode 100644 addons/web_tour/static/src/xml/debug_manager.xml diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index e64b6f69656..f6aab5d6fdc 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -115,8 +115,8 @@ You could use this simplified accounting in case you work with an (external) acc 'account/static/tests/tours/**/*', ], 'web.qunit_suite_tests': [ - ('after', 'web/static/tests/views/kanban_tests.js', 'account/static/tests/account_payment_field_tests.js'), - ('after', 'web/static/tests/views/kanban_tests.js', 'account/static/tests/section_and_note_tests.js'), + ('after', 'web/static/tests/legacy/views/kanban_tests.js', 'account/static/tests/account_payment_field_tests.js'), + ('after', 'web/static/tests/legacy/views/kanban_tests.js', 'account/static/tests/section_and_note_tests.js'), ], 'web.assets_qweb': [ 'account/static/src/xml/**/*', diff --git a/addons/base_import/static/src/js/import_action.js b/addons/base_import/static/src/js/import_action.js index 7cdbc968781..f5374be0571 100644 --- a/addons/base_import/static/src/js/import_action.js +++ b/addons/base_import/static/src/js/import_action.js @@ -6,8 +6,6 @@ var config = require('web.config'); var core = require('web.core'); var session = require('web.session'); var time = require('web.time'); -var AbstractWebClient = require('web.AbstractWebClient'); -var Loading = require('web.Loading'); var QWeb = core.qweb; var _t = core._t; @@ -800,9 +798,6 @@ var DataImport = AbstractAction.extend({ {tracking_disable: tracking_disable} ); var self = this; - this.trigger_up('with_client', {callback: function () { - this.loading.ignore_events = true; - }}); $.blockUI({message: QWeb.render('Throbber')}); $(document.body).addClass('o_ui_blocked'); var opts = this.import_options(); @@ -851,9 +846,6 @@ var DataImport = AbstractAction.extend({ }).finally(function () { $(document.body).removeClass('o_ui_blocked'); $.unblockUI(); - self.trigger_up('with_client', {callback: function () { - delete this.loading.ignore_events; - }}); }); }, /** * @@ -877,6 +869,8 @@ var DataImport = AbstractAction.extend({ method: 'execute_import', args: args.concat([opts]), kwargs: kwargs + }, { + shadow: true, }).then(function (results) { _.each(results.messages, offset_by(opts.skip)); if (!kwargs.dryrun && !results.ids) { @@ -1172,18 +1166,6 @@ StateMachine.create({ ], }); -Loading.include({ - on_rpc_event: function () { - if (this.ignore_events) { - return - } - this._super.apply(this, arguments); - } -}); -AbstractWebClient.prototype.custom_events['with_client'] = function (ev) { - ev.data.callback.call(this); -}; - function offset_by(by) { return function offset_message(msg) { if (msg.rows) { diff --git a/addons/bus/__manifest__.py b/addons/bus/__manifest__.py index 9dd82600664..880e2d8dda5 100644 --- a/addons/bus/__manifest__.py +++ b/addons/bus/__manifest__.py @@ -19,10 +19,7 @@ 'bus/static/src/js/services/bus_service.js', ], 'web.qunit_suite_tests': [ - 'bus/static/tests/bus_tests.js', - ], - 'web.assets_tests': [ - 'bus/static/tests/bus_tests_tour.js', + 'bus/static/tests/*.js', ], } } diff --git a/addons/bus/static/src/js/services/assets_watchdog_service.js b/addons/bus/static/src/js/services/assets_watchdog_service.js new file mode 100644 index 00000000000..114988a2afa --- /dev/null +++ b/addons/bus/static/src/js/services/assets_watchdog_service.js @@ -0,0 +1,92 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; + +export const assetsWatchdogService = { + dependencies: ["notification"], + + start(env, { notification }) { + const assets = {}; + let assetsChangedNotificationId = null; + let bundleNotifTimerID = null; + + env.bus.on("WEB_CLIENT_READY", null, async () => { + const legacyEnv = owl.Component.env; + + document.querySelectorAll("*[data-asset-bundle]").forEach((el) => { + assets[el.getAttribute("data-asset-bundle")] = el.getAttribute( + "data-asset-version" + ); + }); + + legacyEnv.services.bus_service.onNotification(this, onNotification); + legacyEnv.services.bus_service.addChannel("bundle_changed"); + }); + + /** + * Displays one notification on user's screen when assets have changed + */ + function displayBundleChangedNotification() { + if (!assetsChangedNotificationId) { + // Wrap the notification inside a delay. + // The server may be overwhelmed with recomputing assets + // We wait until things settle down + browser.clearTimeout(bundleNotifTimerID); + bundleNotifTimerID = browser.setTimeout(() => { + assetsChangedNotificationId = notification.create( + env._t("The page appears to be out of date."), + { + title: env._t("Refresh"), + type: "warning", + sticky: true, + buttons: [ + { + name: env._t("Refresh"), + primary: true, + onClick: () => { + browser.location.reload(); + }, + }, + ], + onClose: () => { + assetsChangedNotificationId = null; + }, + } + ); + }, getBundleNotificationDelay()); + } + } + + /** + * Computes a random delay to avoid hammering the server + * when bundles change with all the users reloading + * at the same time + * + * @return {number} delay in milliseconds + */ + function getBundleNotificationDelay() { + return 10000 + Math.floor(Math.random() * 50) * 1000; + } + + /** + * Reacts to bus's notification + * + * @param {Array} notifications: list of received notifications + */ + function onNotification(notifications) { + for (const notif of notifications) { + if (notif[0][1] === "bundle_changed") { + const bundleXmlId = notif[1][0]; + const bundleVersion = notif[1][1]; + if (bundleXmlId in assets && bundleVersion !== assets[bundleXmlId]) { + displayBundleChangedNotification(); + break; + } + } + } + } + }, +}; + +registry.category("services").add("assetsWatchdog", assetsWatchdogService); diff --git a/addons/bus/static/src/js/web_client_bus.js b/addons/bus/static/src/js/web_client_bus.js deleted file mode 100644 index 759ec2dc945..00000000000 --- a/addons/bus/static/src/js/web_client_bus.js +++ /dev/null @@ -1,109 +0,0 @@ -odoo.define('bus.WebClient', function (require) { - "use strict"; - - const core = require('web.core'); - const WebClient = require('web.WebClient'); - - const _t = core._t; - - WebClient.include({ - - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- - - /** - * Detects the presence of assets in DOM's HEAD - * - * @override - */ - async start() { - this._assetsChangedNotificationId = null; - this._assets = {}; - await this._super(...arguments); - }, - /** - * Assigns handler to bus notification - * - * @override - */ - show_application() { - const shown = this._super(...arguments); - document.querySelectorAll('*[data-asset-bundle]').forEach(el => { - this._assets[el.getAttribute('data-asset-bundle')] = el.getAttribute('data-asset-version'); - }); - this.call('bus_service', 'onNotification', this, this._onNotification); - this.call('bus_service', 'addChannel', 'bundle_changed'); - return shown; - }, - - //---------------------------------------------------------------------- - // Private - //---------------------------------------------------------------------- - - /** - * Displays one notification on user's screen when assets have changed - * - * @private - */ - _displayBundleChangedNotification() { - if (!this._assetsChangedNotificationId) { - // Wrap the notification inside a delay. - // The server may be overwhelmed with recomputing assets - // We wait until things settle down - clearTimeout(this._bundleNotifTimerID); - this._bundleNotifTimerID = setTimeout(() => { - this._assetsChangedNotificationId = this.call('notification', 'notify', { - title: _t('Refresh'), - message: _t('The page appears to be out of date.'), - sticky: true, - onClose: () => { - this._assetsChangedNotificationId = null; - }, - buttons: [{ - text: _t('Refresh'), - primary: true, - click: () => { - window.location.reload(true); - } - }], - }); - }, this._getBundleNotificationDelay()); - } - }, - /** - * Computes a random delay to avoid hammering the server - * when bundles change with all the users reloading - * at the same time - * - * @private - * @return {number} delay in milliseconds - */ - _getBundleNotificationDelay() { - return 10000 + Math.floor(Math.random()*50) * 1000; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Reacts to bus's notification - * - * @private - * @param {Array} notifications: list of received notifications - */ - _onNotification(notifications) { - for (const notif of notifications) { - if (notif[0][1] === 'bundle_changed') { - const bundleXmlId = notif[1][0]; - const bundleVersion = notif[1][1]; - if (bundleXmlId in this._assets && bundleVersion !== this._assets[bundleXmlId]) { - this._displayBundleChangedNotification(); - break; - } - } - } - } - }); -}); diff --git a/addons/bus/static/tests/assets_watchdog_tests.js b/addons/bus/static/tests/assets_watchdog_tests.js new file mode 100644 index 00000000000..175a069176e --- /dev/null +++ b/addons/bus/static/tests/assets_watchdog_tests.js @@ -0,0 +1,86 @@ +/** @odoo-module */ + +import * as legacyRegistry from "web.Registry"; +import * as BusService from "bus.BusService"; +import * as RamStorage from "web.RamStorage"; +import * as AbstractStorageService from "web.AbstractStorageService"; + +import { + createWebClient, + getActionManagerTestConfig, +} from "@web/../tests/webclient/actions/helpers"; +import { assetsWatchdogService } from "@bus/js/services/assets_watchdog_service"; +import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; +import { NotificationContainer } from "@web/core/notifications/notification_container"; +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { clearRegistryWithCleanup } from "@web/../tests/helpers/mock_env"; + +const LocalStorageService = AbstractStorageService.extend({ + storage: new RamStorage(), +}); +const mainComponentRegistry = registry.category("main_components"); +const serviceRegistry = registry.category("services"); + +QUnit.module("Bus Assets WatchDog", (hooks) => { + let legacyServicesRegistry; + let testConfig; + hooks.beforeEach((assert) => { + legacyServicesRegistry = new legacyRegistry(); + legacyServicesRegistry.add("bus_service", BusService); + legacyServicesRegistry.add("local_storage", LocalStorageService); + + testConfig = getActionManagerTestConfig(); + serviceRegistry.add("assetsWatchdog", assetsWatchdogService); + + clearRegistryWithCleanup(mainComponentRegistry); + mainComponentRegistry.add("NotificationContainer", NotificationContainer); + patchWithCleanup(browser, { + setTimeout: (fn) => fn(), + clearTimeout: () => {}, + location: { + reload: () => assert.step("reloadPage"), + }, + }); + }); + + QUnit.test("can listen on bus and displays notifications in DOM", async (assert) => { + assert.expect(5); + + let pollNumber = 0; + const mockRPC = (route, args) => { + if (route === "/longpolling/poll") { + if (pollNumber > 0) { + return new Promise(() => {}); // let it hang to avoid further calls + } + pollNumber++; + assert.deepEqual(args.channels, ["bundle_changed"]); + return Promise.resolve([ + { + id: "prout", + channel: ["db_name", "bundle_changed"], + message: ["web.assets_backend", "newHash"], + }, + ]); + } + }; + + const webClient = await createWebClient({ + testConfig, + legacyParams: { serviceRegistry: legacyServicesRegistry }, + mockRPC, + }); + + await nextTick(); + + assert.containsOnce(webClient.el, ".o_notification_body"); + assert.strictEqual( + webClient.el.querySelector(".o_notification_body .o_notification_content").textContent, + "The page appears to be out of date." + ); + + // reload by clicking on the reload button + await click(webClient.el, ".o_notification_buttons .btn-primary"); + assert.verifySteps(["reloadPage"]); + }); +}); diff --git a/addons/bus/static/tests/bus_tests_tour.js b/addons/bus/static/tests/bus_tests_tour.js deleted file mode 100644 index 2248e0a1eb3..00000000000 --- a/addons/bus/static/tests/bus_tests_tour.js +++ /dev/null @@ -1,25 +0,0 @@ -odoo.define("bus.tour", function (require) { - "use strict"; - - const tour = require("web_tour.tour"); - - tour.register("bundle_changed_notification", { - test: true, - url: '/web', - }, [{ - trigger: '.o_web_client', - run() { - const webClient = odoo.__DEBUG__.services['web.web_client']; - const _delayFn = webClient._getBundleNotificationDelay; - webClient._getBundleNotificationDelay = () => 0; - this.call('bus_service', 'trigger', - 'notification', - [[['db_name', 'bundle_changed'], ['web.assets_backend', 'hash']]] - ); - webClient._getBundleNotificationDelay = _delayFn; - } - }, { - trigger: '.o_notification_title:contains(Refresh)', - }] - ); -}); diff --git a/addons/bus/tests/test_assetsbundle.py b/addons/bus/tests/test_assetsbundle.py index 2bbb89e8be0..4abc687d34e 100644 --- a/addons/bus/tests/test_assetsbundle.py +++ b/addons/bus/tests/test_assetsbundle.py @@ -36,7 +36,8 @@ class BusWebTests(odoo.tests.HttpCase): self.patch(type(self.env['bus.bus']), 'sendone', patched_sendone) - self.start_tour('/web', "bundle_changed_notification", login='admin', timeout=180) + self.authenticate('admin', 'admin') + self.url_open('/web') # One sendone for each asset bundle and for each CSS / JS self.assertEqual( diff --git a/addons/calendar/__manifest__.py b/addons/calendar/__manifest__.py index a34889b121b..f44f1abeca5 100644 --- a/addons/calendar/__manifest__.py +++ b/addons/calendar/__manifest__.py @@ -47,6 +47,7 @@ If you need to manage your meetings, you should install the CRM module. 'calendar/static/src/js/calendar_view.js', 'calendar/static/src/js/mail_activity.js', 'calendar/static/src/js/systray_activity_menu.js', + 'calendar/static/src/js/services/calendar_notification_service.js', ], 'web.qunit_suite_tests': [ 'calendar/static/tests/**/*', diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index a4de081c604..29220944a06 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -3,116 +3,10 @@ odoo.define('base_calendar.base_calendar', function (require) { var BasicModel = require('web.BasicModel'); var fieldRegistry = require('web.field_registry'); -var Notification = require('web.Notification'); var relationalFields = require('web.relational_fields'); -var session = require('web.session'); -var WebClient = require('web.WebClient'); const FieldMany2ManyTagsAvatar = relationalFields.FieldMany2ManyTagsAvatar; -var CalendarNotification = Notification.extend({ - template: "CalendarNotification", - xmlDependencies: (Notification.prototype.xmlDependencies || []) - .concat(['/calendar/static/src/xml/notification_calendar.xml']), - - init: function(parent, params) { - this._super(parent, params); - this.eid = params.eventID; - this.sticky = true; - - this.events = _.extend(this.events || {}, { - 'click .link2event': function() { - var self = this; - - this._rpc({ - route: '/web/action/load', - params: { - action_id: 'calendar.action_calendar_event_notify', - }, - }) - .then(function(r) { - r.res_id = self.eid; - return self.do_action(r); - }); - }, - - 'click .link2recall': function() { - this.close(); - }, - - 'click .link2showed': function() { - this._rpc({route: '/calendar/notify_ack'}) - .then(this.close.bind(this, false), this.close.bind(this, false)); - }, - }); - }, -}); - -WebClient.include({ - display_calendar_notif: function(notifications) { - var self = this; - var last_notif_timer = 0; - - // Clear previously set timeouts and destroy currently displayed calendar notifications - clearTimeout(this.get_next_calendar_notif_timeout); - _.each(this.calendar_notif_timeouts, clearTimeout); - this.calendar_notif_timeouts = {}; - - // For each notification, set a timeout to display it - _.each(notifications, function(notif) { - var key = notif.event_id + ',' + notif.alarm_id; - if (key in self.calendar_notif) { - return; - } - self.calendar_notif_timeouts[key] = setTimeout(function () { - var notificationID = self.call('notification', 'notify', { - Notification: CalendarNotification, - title: notif.title, - message: notif.message, - eventID: notif.event_id, - onClose: function () { - delete self.calendar_notif[key]; - }, - }); - self.calendar_notif[key] = notificationID; - }, notif.timer * 1000); - last_notif_timer = Math.max(last_notif_timer, notif.timer); - }); - - // Set a timeout to get the next notifications when the last one has been displayed - if (last_notif_timer > 0) { - this.get_next_calendar_notif_timeout = setTimeout(this.get_next_calendar_notif.bind(this), last_notif_timer * 1000); - } - }, - get_next_calendar_notif: function() { - session.rpc("/calendar/notify", {}, {shadow: true}) - .then(this.display_calendar_notif.bind(this)) - .guardedCatch(function(reason) { // - var err = reason.message; - var ev = reason.event; - if(err.code === -32098) { - // Prevent the CrashManager to display an error - // in case of an xhr error not due to a server error - ev.preventDefault(); - } - }); - }, - show_application: function() { - // An event is triggered on the bus each time a calendar event with alarm - // in which the current user is involved is created, edited or deleted - this.calendar_notif_timeouts = {}; - this.calendar_notif = {}; - this.call('bus_service', 'onNotification', this, function (notifications) { - _.each(notifications, (function (notification) { - if (notification[0][1] === 'calendar.alarm') { - this.display_calendar_notif(notification[1]); - } - }).bind(this)); - }); - return this._super.apply(this, arguments).then(this.get_next_calendar_notif.bind(this)); - }, -}); - BasicModel.include({ /** * @private diff --git a/addons/calendar/static/src/js/services/calendar_notification_service.js b/addons/calendar/static/src/js/services/calendar_notification_service.js new file mode 100644 index 00000000000..89686c287ba --- /dev/null +++ b/addons/calendar/static/src/js/services/calendar_notification_service.js @@ -0,0 +1,108 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { ConnectionLostError } from "@web/core/network/rpc_service"; +import { registry } from "@web/core/registry"; + +export const calendarNotificationService = { + dependencies: ["action", "notification", "rpc"], + + start(env, { action, notification, rpc }) { + let calendarNotifTimeouts = {}; + let nextCalendarNotifTimeout = null; + const calendarNotif = {}; + + env.bus.on("WEB_CLIENT_READY", null, async () => { + const legacyEnv = owl.Component.env; + legacyEnv.services.bus_service.onNotification(this, (notifications) => { + for (const notif of notifications) { + if (notif[0][1] === "calendar.alarm") { + displayCalendarNotification(notif[1]); + } + } + }); + legacyEnv.services.bus_service.startPolling(); + }); + + /** + * Displays the Calendar notification on user's screen + */ + function displayCalendarNotification(notifications) { + let lastNotifTimer = 0; + + // Clear previously set timeouts and destroy currently displayed calendar notifications + browser.clearTimeout(nextCalendarNotifTimeout); + Object.values(calendarNotifTimeouts).forEach((notif) => browser.clearTimeout(notif)); + calendarNotifTimeouts = {}; + + // For each notification, set a timeout to display it + notifications.forEach(function (notif) { + const key = notif.event_id + "," + notif.alarm_id; + if (key in calendarNotif) { + return; + } + calendarNotifTimeouts[key] = browser.setTimeout(function () { + const notificationID = notification.create(notif.message, { + title: notif.title, + type: "warning", + sticky: true, + onClose: () => { + delete calendarNotif[key]; + }, + buttons: [ + { + name: env._t("OK"), + primary: true, + onClick: async () => { + await rpc("/calendar/notify_ack"); + notification.close(calendarNotif[key]); + }, + }, + { + name: env._t("Details"), + onClick: async () => { + await action.doAction( + "calendar.action_calendar_event_notify", + { + resId: notif.event_id, + } + ); + notification.close(calendarNotif[key]); + }, + }, + { + name: env._t("Snooze"), + onClick: () => { + notification.close(calendarNotif[key]); + }, + }, + ], + }); + calendarNotif[key] = notificationID; + }, notif.timer * 1000); + lastNotifTimer = Math.max(lastNotifTimer, notif.timer); + }); + + // Set a timeout to get the next notifications when the last one has been displayed + if (lastNotifTimer > 0) { + nextCalendarNotifTimeout = browser.setTimeout( + getNextCalendarNotif, + lastNotifTimer * 1000 + ); + } + } + + async function getNextCalendarNotif() { + try { + const result = await rpc("/calendar/notify", {}, { silent: true }); + displayCalendarNotification(result); + } catch (error) { + if (!(error instanceof ConnectionLostError)) { + throw error; + } + } + } + }, +}; + +registry.category("services").add("calendarNotification", calendarNotificationService); diff --git a/addons/calendar/static/src/xml/notification_calendar.xml b/addons/calendar/static/src/xml/notification_calendar.xml deleted file mode 100644 index 3c51293f36c..00000000000 --- a/addons/calendar/static/src/xml/notification_calendar.xml +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/addons/calendar/static/tests/calendar_notification_tests.js b/addons/calendar/static/tests/calendar_notification_tests.js new file mode 100644 index 00000000000..9ac4238b81c --- /dev/null +++ b/addons/calendar/static/tests/calendar_notification_tests.js @@ -0,0 +1,233 @@ +/** @odoo-module */ + +import * as legacyRegistry from "web.Registry"; +import * as BusService from "bus.BusService"; +import * as RamStorage from "web.RamStorage"; +import * as AbstractStorageService from "web.AbstractStorageService"; + +import { + createWebClient, + getActionManagerTestConfig, +} from "@web/../tests/webclient/actions/helpers"; +import { calendarNotificationService } from "@calendar/js/services/calendar_notification_service"; +import { click, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; +import { NotificationContainer } from "@web/core/notifications/notification_container"; +import { browser } from "@web/core/browser/browser"; +import { clearRegistryWithCleanup } from "@web/../tests/helpers/mock_env"; +import { registry } from "@web/core/registry"; + +const LocalStorageService = AbstractStorageService.extend({ + storage: new RamStorage(), +}); +const mainComponentRegistry = registry.category("main_components"); +const serviceRegistry = registry.category("services"); + +QUnit.module("Calendar Notification", (hooks) => { + let legacyServicesRegistry; + let testConfig; + hooks.beforeEach(() => { + legacyServicesRegistry = new legacyRegistry(); + legacyServicesRegistry.add("bus_service", BusService); + legacyServicesRegistry.add("local_storage", LocalStorageService); + + testConfig = getActionManagerTestConfig(); + serviceRegistry.add("calendarNotification", calendarNotificationService); + + clearRegistryWithCleanup(mainComponentRegistry); + mainComponentRegistry.add("NotificationContainer", NotificationContainer); + + patchWithCleanup(browser, { + setTimeout: (fn) => fn(), + clearTimeout: () => {}, + }); + }); + + QUnit.test( + "can listen on bus and display notifications in DOM and click OK", + async (assert) => { + assert.expect(5); + + let pollNumber = 0; + const mockRPC = (route, args) => { + if (route === "/longpolling/poll") { + if (pollNumber > 0) { + return new Promise(() => {}); // let it hang to avoid further calls + } + pollNumber++; + return Promise.resolve([ + { + id: "prout", + channel: ["db_name", "calendar.alarm", 7], + message: [ + { + alarm_id: 1, + event_id: 2, + title: "Meeting", + message: "Very old meeting message", + timer: 20 * 60, + notify_at: "1978-04-14 12:45:00", + }, + ], + }, + ]); + } + if (route === "/calendar/notify") { + return Promise.resolve([]); + } + if (route === "/calendar/notify_ack") { + assert.step("notifyAck"); + return Promise.resolve(true); + } + }; + + const webClient = await createWebClient({ + testConfig, + legacyParams: { serviceRegistry: legacyServicesRegistry }, + mockRPC, + }); + + await nextTick(); + + assert.containsOnce(webClient.el, ".o_notification_body"); + assert.strictEqual( + webClient.el.querySelector(".o_notification_body .o_notification_content") + .textContent, + "Very old meeting message" + ); + + await click(webClient.el.querySelector(".o_notification_buttons .btn")); + assert.verifySteps(["notifyAck"]); + assert.containsNone(webClient.el, ".o_notification"); + } + ); + + QUnit.test( + "can listen on bus and display notifications in DOM and click Detail", + async (assert) => { + assert.expect(5); + + let pollNumber = 0; + const mockRPC = (route, args) => { + if (route === "/longpolling/poll") { + if (pollNumber > 0) { + return new Promise(() => {}); // let it hang to avoid further calls + } + pollNumber++; + return Promise.resolve([ + { + id: "prout", + channel: ["db_name", "calendar.alarm", 7], + message: [ + { + alarm_id: 1, + event_id: 2, + title: "Meeting", + message: "Very old meeting message", + timer: 20 * 60, + notify_at: "1978-04-14 12:45:00", + }, + ], + }, + ]); + } + if (route === "/calendar/notify") { + return Promise.resolve([]); + } + }; + + const fakeActionService = { + name: "action", + start() { + return { + doAction(actionId) { + assert.step(actionId); + return Promise.resolve(true); + }, + loadState(state, options) { + return Promise.resolve(true); + }, + }; + }, + }; + serviceRegistry.add("action", fakeActionService, { force: true }); + + const webClient = await createWebClient({ + testConfig, + legacyParams: { serviceRegistry: legacyServicesRegistry }, + mockRPC, + }); + + await nextTick(); + + assert.containsOnce(webClient.el, ".o_notification_body"); + assert.strictEqual( + webClient.el.querySelector(".o_notification_body .o_notification_content") + .textContent, + "Very old meeting message" + ); + + await click(webClient.el.querySelectorAll(".o_notification_buttons .btn")[1]); + assert.verifySteps(["calendar.action_calendar_event_notify"]); + assert.containsNone(webClient.el, ".o_notification"); + } + ); + + QUnit.test( + "can listen on bus and display notifications in DOM and click Snooze", + async (assert) => { + assert.expect(4); + + let pollNumber = 0; + const mockRPC = (route, args) => { + if (route === "/longpolling/poll") { + if (pollNumber > 0) { + return new Promise(() => {}); // let it hang to avoid further calls + } + pollNumber++; + return Promise.resolve([ + { + id: "prout", + channel: ["db_name", "calendar.alarm", 7], + message: [ + { + alarm_id: 1, + event_id: 2, + title: "Meeting", + message: "Very old meeting message", + timer: 20 * 60, + notify_at: "1978-04-14 12:45:00", + }, + ], + }, + ]); + } + if (route === "/calendar/notify") { + return Promise.resolve([]); + } + if (route === "/calendar/notify_ack") { + assert.step("notifyAck"); + return Promise.resolve(true); + } + }; + + const webClient = await createWebClient({ + testConfig, + legacyParams: { serviceRegistry: legacyServicesRegistry }, + mockRPC, + }); + + await nextTick(); + + assert.containsOnce(webClient.el, ".o_notification_body"); + assert.strictEqual( + webClient.el.querySelector(".o_notification_body .o_notification_content") + .textContent, + "Very old meeting message" + ); + + await click(webClient.el.querySelectorAll(".o_notification_buttons .btn")[2]); + assert.verifySteps([], "should only close the notification withtout calling a rpc"); + assert.containsNone(webClient.el, ".o_notification"); + } + ); +}); diff --git a/addons/hr_attendance/__manifest__.py b/addons/hr_attendance/__manifest__.py index c066f65da63..d4a21836620 100644 --- a/addons/hr_attendance/__manifest__.py +++ b/addons/hr_attendance/__manifest__.py @@ -42,7 +42,7 @@ actions(Check in/Check out) performed by them. 'hr_attendance/static/src/scss/hr_attendance.scss', ], 'web.qunit_suite_tests': [ - ('after', 'web/static/tests/views/kanban_tests.js', 'hr_attendance/static/tests/hr_attendance_tests.js'), + ('after', 'web/static/tests/legacy/views/kanban_tests.js', 'hr_attendance/static/tests/hr_attendance_tests.js'), ], 'web.assets_qweb': [ 'hr_attendance/static/src/xml/**/*', diff --git a/addons/im_livechat/static/src/legacy/public_livechat.js b/addons/im_livechat/static/src/legacy/public_livechat.js index 1bf1e7ce1ff..25445bde1ea 100644 --- a/addons/im_livechat/static/src/legacy/public_livechat.js +++ b/addons/im_livechat/static/src/legacy/public_livechat.js @@ -3530,7 +3530,7 @@ odoo.define('im_livechat.legacy.mail.DocumentViewer', function (require) { var core = require('web.core'); var Widget = require('web.Widget'); -var { hidePDFJSButtons } = require('@web/js/libs/pdfjs'); +var { hidePDFJSButtons } = require('@web/legacy/js/libs/pdfjs'); var QWeb = core.qweb; diff --git a/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js b/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js index 108006b14d3..769232a3d9d 100644 --- a/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js +++ b/addons/mail/static/src/components/attachment_viewer/attachment_viewer.js @@ -5,7 +5,7 @@ import useShouldUpdateBasedOnProps from '@mail/component_hooks/use_should_update import useStore from '@mail/component_hooks/use_store/use_store'; import { link } from '@mail/model/model_field_command'; -import { hidePDFJSButtons } from '@web/js/libs/pdfjs'; +import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs'; const { Component, QWeb } = owl; const { useRef } = owl.hooks; diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js index 5325921bc5d..3659c01bb12 100644 --- a/addons/mail/static/src/js/document_viewer.js +++ b/addons/mail/static/src/js/document_viewer.js @@ -2,7 +2,7 @@ import core from 'web.core'; import Widget from 'web.Widget'; -import { hidePDFJSButtons } from '@web/js/libs/pdfjs'; +import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs'; var QWeb = core.qweb; diff --git a/addons/mail/static/src/js/tools/debug_manager.js b/addons/mail/static/src/js/tools/debug_manager.js index ca5c1738ee0..0e1968391e6 100644 --- a/addons/mail/static/src/js/tools/debug_manager.js +++ b/addons/mail/static/src/js/tools/debug_manager.js @@ -1,30 +1,37 @@ /** @odoo-module **/ -import core from 'web.core'; -import DebugManager from 'web.DebugManager.Backend'; +import { registry } from "@web/core/registry"; -var _t = core._t; -/** - * adds a new method available for the debug manager, called by the "Manage Messages" button. - * - */ -DebugManager.include({ - getMailMessages: function () { - var selectedIDs = this._controller.getSelectedIds(); - if (!selectedIDs.length) { - console.warn(_t("No message available")); - return; - } - this.do_action({ - res_model: 'mail.message', - name: _t('Manage Messages'), - views: [[false, 'list'], [false, 'form']], - type: 'ir.actions.act_window', - domain: [['res_id', '=', selectedIDs[0]], ['model', '=', this._controller.modelName]], - context: { - default_res_model: this._controller.modelName, - default_res_id: selectedIDs[0], - }, - }); - }, -}); +export function manageMessages({ action, component, env }) { + const selectedIds = component.widget.getSelectedIds(); + if (!selectedIds.length) { + return null; // No record + } + const description = env._t("Manage Messages"); + return { + type: "item", + description, + callback: () => { + env.services.action.doAction({ + res_model: "mail.message", + name: description, + views: [ + [false, "list"], + [false, "form"], + ], + type: "ir.actions.act_window", + domain: [ + ["res_id", "=", selectedIds[0]], + ["model", "=", action.res_model], + ], + context: { + default_res_model: action.res_model, + default_res_id: selectedIds[0], + }, + }); + }, + sequence: 325, + }; +} + +registry.category("debug").category("form").add("mail.manageMessages", manageMessages); diff --git a/addons/mail/static/src/models/discuss/discuss.js b/addons/mail/static/src/models/discuss/discuss.js index 7ee0f26a168..4416b95b513 100644 --- a/addons/mail/static/src/models/discuss/discuss.js +++ b/addons/mail/static/src/models/discuss/discuss.js @@ -191,7 +191,7 @@ function factory(dependencies) { options: { active_id: this.threadToActiveId(this), clear_breadcrumbs: false, - on_reverse_breadcrumb: () => this.close(), + on_reverse_breadcrumb: () => this.close(), // this is useless, close is called by destroy anyway }, }); } diff --git a/addons/mail/static/src/utils/test_utils.js b/addons/mail/static/src/utils/test_utils.js index 9cc8d643266..0919be63146 100644 --- a/addons/mail/static/src/utils/test_utils.js +++ b/addons/mail/static/src/utils/test_utils.js @@ -18,12 +18,17 @@ import AbstractStorageService from 'web.AbstractStorageService'; import NotificationService from 'web.NotificationService'; import RamStorage from 'web.RamStorage'; import { - createActionManager, createView, makeTestPromise, mock, } from 'web.test_utils'; import Widget from 'web.Widget'; +import { + createWebClient, + getActionManagerTestConfig, +} from "@web/../tests/webclient/actions/helpers"; +import { ComponentAdapter } from "web.OwlCompatibility"; +import LegacyMockServer from "web.MockServer"; const { addMockEnvironment, @@ -546,7 +551,36 @@ async function start(param0 = {}) { } }); } else if (hasActionManager) { - widget = await createActionManager(kwargs); + let testConfig; + if (!kwargs.testConfig) { + testConfig = getActionManagerTestConfig(); + } else { + testConfig = kwargs.testConfig; + delete kwargs.testConfig; + } + + if (kwargs.actions) { + const actions = {}; + kwargs.actions.forEach((act) => { + actions[act.xml_id || act.id] = act; + }); + Object.assign(testConfig.serverData.actions, actions); + delete kwargs.actions; + } + + Object.assign(testConfig.serverData.views, kwargs.archs); + delete kwargs.archs; + + Object.assign(testConfig.serverData.models, kwargs.data); + delete kwargs.data; + + const mockRPC = kwargs.mockRPC; + delete kwargs.mockRPC; + + const legacyParams = kwargs; + legacyParams.withLegacyMockServer = true; + const widget = await createWebClient({ testConfig, mockRPC, legacyParams }); + legacyPatch(widget, { destroy() { destroyCallbacks.forEach(callback => callback({ widget })); diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index f7b6611c14b..da5ca316437 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -1,14 +1,6 @@ - - - - Manage Messages - - diff --git a/addons/mail/static/tests/activity_tests.js b/addons/mail/static/tests/activity_tests.js index 0eebafd0319..92af5da6c6b 100644 --- a/addons/mail/static/tests/activity_tests.js +++ b/addons/mail/static/tests/activity_tests.js @@ -2,8 +2,14 @@ import ActivityView from '@mail/js/views/activity/activity_view'; import testUtils from 'web.test_utils'; +import { createWebClient } from "@web/../tests/webclient/actions/helpers"; -var createActionManager = testUtils.createActionManager; +import { legacyExtraNextTick } from "@web/../tests/helpers/utils"; +import { doAction, getActionManagerTestConfig } from "@web/../tests/webclient/actions/helpers"; +import { registry } from "@web/core/registry"; +import { makeFakeUserService } from "@web/../tests/helpers/mock_services"; + +let testConfig; var createView = testUtils.createView; @@ -108,6 +114,8 @@ QUnit.module('activity view', { ], }, }; + testConfig = getActionManagerTestConfig(); + Object.assign(testConfig.serverData.models, this.data); } }); @@ -350,49 +358,68 @@ QUnit.test('activity view: activity widget', async function (assert) { activity.destroy(); }); -QUnit.test('activity view: no group_by_menu and no comparison_menu', async function (assert) { + +QUnit.test("activity view: no group_by_menu and no comparison_menu", async function (assert) { assert.expect(4); - var actionManager = await createActionManager({ - actions: [{ - id: 1, - name: 'Task Action', - res_model: 'task', - type: 'ir.actions.act_window', - views: [[false, 'activity']], - }], - archs: { - 'task,false,activity': '' + - '' + - '
' + - '' + - '
' + - '
' + - '
', - 'task,false,search': '', - }, - data: this.data, - session: { - user_context: {lang: 'zz_ZZ'}, - }, - mockRPC: function(route, args) { - if (args.method === 'get_activity_data') { - assert.deepEqual(args.kwargs.context, {lang: 'zz_ZZ'}, - 'The context should have been passed'); - } - return this._super.apply(this, arguments); - }, - }); + testConfig.serverData.actions[1] = { + id: 1, + name: "Task Action", + res_model: "task", + type: "ir.actions.act_window", + views: [[false, "activity"]], + }; - await actionManager.doAction(1); + testConfig.serverData.views = { + "task,false,activity": + '' + + "" + + '
' + + '' + + "
" + + "
" + + "
", + "task,false,search": "", + }; - assert.containsN(actionManager, '.o_search_options .o_dropdown button:visible', 2, - "only two elements should be available in view search"); - assert.isVisible(actionManager.$('.o_search_options .o_dropdown.o_filter_menu > button'), - "filter should be available in view search"); - assert.isVisible(actionManager.$('.o_search_options .o_dropdown.o_favorite_menu > button'), - "favorites should be available in view search"); - actionManager.destroy(); + const mockRPC = (route, args) => { + if (args.method === "get_activity_data") { + assert.strictEqual( + args.kwargs.context.lang, + "zz_ZZ", + "The context should have been passed" + ); + } + }; + + registry.category("services").add( + "user", + makeFakeUserService({ + session_info: { + user_context: { lang: "zz_ZZ" }, + }, + }), + { force: true } + ); + + const webClient = await createWebClient({ testConfig, mockRPC , legacyParams: {withLegacyMockServer: true}}); + + await doAction(webClient, 1); + + assert.containsN( + webClient, + ".o_search_options .o_dropdown button:visible", + 2, + "only two elements should be available in view search" + ); + assert.isVisible( + $(webClient.el).find(".o_search_options .o_dropdown.o_filter_menu > button"), + "filter should be available in view search" + ); + assert.isVisible( + $(webClient.el).find(".o_search_options .o_dropdown.o_favorite_menu > button"), + "favorites should be available in view search" + ); }); QUnit.test('activity view: search more to schedule an activity for a record of a respecting model', async function (assert) { @@ -458,63 +485,54 @@ QUnit.test('activity view: search more to schedule an activity for a record of a activity.destroy(); }); -QUnit.test('Activity view: discard an activity creation dialog', async function (assert) { +QUnit.test("Activity view: discard an activity creation dialog", async function (assert) { assert.expect(2); - var actionManager = await createActionManager({ - actions: [{ - id: 1, - name: 'Task Action', - res_model: 'task', - type: 'ir.actions.act_window', - views: [[false, 'activity']], - }], - archs: { - 'task,false,activity': ` - - -
- -
-
-
`, - 'task,false,search': '', - 'mail.activity,false,form': ` -
- -
-
- ` - }, - data: this.data, - intercepts: { - do_action(ev) { - actionManager.doAction(ev.data.action, ev.data.options); - } - }, - async mockRPC(route, args) { - if (args.method === 'check_access_rights') { - return true; - } - return this._super(...arguments); - }, - }); - await actionManager.doAction(1); + testConfig.serverData.actions[1] = { + id: 1, + name: "Task Action", + res_model: "task", + type: "ir.actions.act_window", + views: [[false, "activity"]], + }; - await testUtils.dom.click(actionManager.$('.o_activity_view .o_data_row .o_activity_empty_cell')[0]); - assert.containsOnce( - $, - '.modal.o_technical_modal.show', - "Activity Modal should be opened"); + testConfig.serverData.views = { + "task,false,activity": ` + + +
+ +
+
+
`, + "task,false,search": "", + "mail.activity,false,form": ` +
+ +
+
+ `, + }; - await testUtils.dom.click($('.modal.o_technical_modal.show button[special="cancel"]')); - assert.containsNone( - $, - '.modal.o_technical_modal.show', - "Activity Modal should be closed"); + const mockRPC = (route, args) => { + if (args.method === "check_access_rights") { + return true; + } + }; - actionManager.destroy(); + const webClient = await createWebClient({ testConfig, mockRPC, legacyParams: {withLegacyMockServer: true} }); + await doAction(webClient, 1); + + await testUtils.dom.click( + $(webClient.el).find(".o_activity_view .o_data_row .o_activity_empty_cell")[0] + ); + await legacyExtraNextTick(); + assert.containsOnce($, ".modal.o_technical_modal", "Activity Modal should be opened"); + + await testUtils.dom.click($('.modal.o_technical_modal button[special="cancel"]')); + await legacyExtraNextTick(); + assert.containsNone($, ".modal.o_technical_modal", "Activity Modal should be closed"); }); }); diff --git a/addons/mail/static/tests/tools/debug_manager_tests.js b/addons/mail/static/tests/tools/debug_manager_tests.js index f6f383b50df..24c35286a7d 100644 --- a/addons/mail/static/tests/tools/debug_manager_tests.js +++ b/addons/mail/static/tests/tools/debug_manager_tests.js @@ -1,62 +1,70 @@ /** @odoo-module **/ -import testUtils from 'web.test_utils'; +import { manageMessages } from "@mail/js/tools/debug_manager"; +import { click, legacyExtraNextTick } from "@web/../tests/helpers/utils"; +import { + createWebClient, + doAction, + getActionManagerTestConfig, +} from "@web/../tests/webclient/actions/helpers"; +import { debugService } from "@web/core/debug/debug_service"; +import { registry } from "@web/core/registry"; -var createDebugManager = testUtils.createDebugManager; +QUnit.module("DebugMenu"); -QUnit.module('Mail DebugManager', {}, function () { +QUnit.test("Manage Messages", async function (assert) { + assert.expect(6); - QUnit.test("Manage Messages", async function (assert) { - assert.expect(3); - - var debugManager = await createDebugManager({ - intercepts: { - do_action: function (event) { - assert.deepEqual(event.data.action, { - context: { - default_res_model: "testModel", - default_res_id: 5, - }, - res_model: 'mail.message', - name: "Manage Messages", - views: [[false, 'list'], [false, 'form']], - type: 'ir.actions.act_window', - domain: [['res_id', '=', 5], ['model', '=', 'testModel']], - }); - }, - }, - }); - - await debugManager.appendTo($('#qunit-fixture')); - - // Simulate update debug manager from web client - var action = { - views: [{ - displayName: "Form", - fieldsView: { - view_id: 1, - }, - type: "form", - }], - }; - var view = { - viewType: "form", - getSelectedIds: function () { - return [5]; - }, - modelName: 'testModel', - }; - await testUtils.nextTick(); - await debugManager.update('action', action, view); - - var $messageMenu = debugManager.$('a[data-action=getMailMessages]'); - assert.strictEqual($messageMenu.length, 1, "should have Manage Message menu item"); - assert.strictEqual($messageMenu.text().trim(), "Manage Messages", - "should have correct menu item text"); - - await testUtils.dom.click(debugManager.$('> a')); // open dropdown - await testUtils.dom.click($messageMenu); - - debugManager.destroy(); + const testConfig = Object.assign(getActionManagerTestConfig(), { + debug: "1", }); + + // Add fake "mail.message" model and arch + testConfig.serverData.models["mail.message"] = { + fields: { name: { string: "Name", type: "char" } }, + records: [], + }; + Object.assign(testConfig.serverData.views, { + "mail.message,false,list": ``, + "mail.message,false,form": `
`, + "mail.message,false,search": ``, + }); + + // Activate debug service with "Manage Message" item + registry.category("services").add("debug", debugService); + registry.category("debug").category("form").add("manageMessages", manageMessages); + + async function mockRPC(route, args) { + if (args.method === "check_access_rights") { + return true; + } + if (route === "/web/dataset/search_read" && args.model === "mail.message") { + assert.strictEqual(args.context.default_res_id, 5); + assert.strictEqual(args.context.default_res_model, "partner"); + assert.deepEqual(args.domain, ["&", ["res_id", "=", 5], ["model", "=", "partner"]]); + } + } + + const wc = await createWebClient({ testConfig, mockRPC }); + await doAction(wc, 3, { viewType: "form", resId: 5 }); + await legacyExtraNextTick(); + await click(wc.el, ".o_debug_manager .o_dropdown_toggler"); + + const dropdownItems = wc.el.querySelectorAll( + ".o_debug_manager .o_dropdown_menu .o_dropdown_item" + ); + assert.strictEqual(dropdownItems.length, 1); + assert.strictEqual( + dropdownItems[0].innerText.trim(), + "Manage Messages", + "should have correct menu item text" + ); + + await click(dropdownItems[0]); + await legacyExtraNextTick(); + + assert.strictEqual( + wc.el.querySelector(".breadcrumb-item.active").innerText.trim(), + "Manage Messages" + ); }); diff --git a/addons/mass_mailing/static/src/js/unsubscribe.js b/addons/mass_mailing/static/src/js/unsubscribe.js index fae7d16e651..ad60ca88a2b 100644 --- a/addons/mass_mailing/static/src/js/unsubscribe.js +++ b/addons/mass_mailing/static/src/js/unsubscribe.js @@ -14,7 +14,7 @@ odoo.define('mass_mailing.unsubscribe', function (require) { var token = (location.search.split('token' + '=')[1] || '').split('&')[0]; if (!$('.o_unsubscribe_form').length) { - return Promise.reject("DOM doesn't contain '.o_unsubscribe_form'"); + return; } session.load_translations().then(function () { if (email != '' && email != undefined){ diff --git a/addons/partner_autocomplete/static/src/js/web_company_autocomplete.js b/addons/partner_autocomplete/static/src/js/web_company_autocomplete.js index 53c5029adfe..803919fdf1d 100644 --- a/addons/partner_autocomplete/static/src/js/web_company_autocomplete.js +++ b/addons/partner_autocomplete/static/src/js/web_company_autocomplete.js @@ -1,26 +1,18 @@ -odoo.define('web.company_autocomplete', function (require) { -"use strict"; +/** @odoo-module **/ -const AbstractWebClient = require('web.AbstractWebClient'); -const session = require('web.session'); +import { registry } from "@web/core/registry"; -return AbstractWebClient.include({ +export const companyAutocompleteService = { + dependencies: ["orm", "user"], - start: function () { - if (session.iap_company_enrich) { - const current_company_id = session.user_companies.current_company; - this._rpc({ - model: 'res.company', - method: 'iap_enrich_auto', - args: [current_company_id], - }, { - shadow: true, - }); + start(env, { orm, user }) { + if (odoo.session_info.iap_company_enrich) { + const currentCompanyId = user.current_company.id; + orm.silent.call("res.company", "iap_enrich_auto", [currentCompanyId], {}); } - - return this._super.apply(this, arguments); }, +}; -}); - -}); +registry + .category("services") + .add("partner_autocomplete.companyAutocomplete", companyAutocompleteService); diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index e5ec6554ec4..7e7c9a77086 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -164,6 +164,43 @@ ], 'point_of_sale.pos_assets_backend': [ ('include', 'web.assets_backend'), + ('remove', 'web/static/src/webclient/menu_service.js'), + ('remove', 'web/static/src/core/errors/error_handlers.js'), + ('remove', 'web/static/src/legacy/legacy_rpc_error_handler.js'), + ], + 'point_of_sale.tests_assets': [ + 'web/static/lib/daterangepicker/daterangepicker.css', + 'web/static/lib/qunit/qunit-2.9.1.css', + 'web/static/lib/qunit/qunit-2.9.1.js', + 'web/static/tests/legacy/helpers/**/*', + ('remove', 'web/static/tests/legacy/helpers/test_utils_tests.js'), + + 'web/static/tests/legacy/legacy_setup.js', + + 'web/static/tests/helpers/**/*.js', + 'web/static/tests/qunit.js', + 'web/static/tests/main.js', + 'web/static/tests/setup.js', + + # These 2 lines below are taken from web.assets_frontend + # They're required for the web.frontend_legacy to work properly + # It is expected to add other lines coming from the web.assets_frontend + # if we need to add more and more legacy stuff that would require other scss or js. + ('include', 'web._assets_helpers'), + 'web/static/lib/bootstrap/scss/_variables.scss', + + ('include', 'web.frontend_legacy'), + ], + 'point_of_sale.qunit_suite_tests': [ + 'web/static/tests/legacy/component_extension_tests.js', + 'point_of_sale/static/tests/unit/**/*', + ('remove', 'point_of_sale/static/tests/unit/helpers/test_main.js') + ], + 'point_of_sale.assets_backend_prod_only': [ + 'point_of_sale/static/src/js/chrome_adapter.js', + 'point_of_sale/static/src/js/main.js', + 'web/static/src/start.js', + 'web/static/src/legacy/legacy_setup.js', ], 'web.assets_qweb': [ 'point_of_sale/static/src/xml/**/*', diff --git a/addons/point_of_sale/static/src/js/chrome_adapter.js b/addons/point_of_sale/static/src/js/chrome_adapter.js new file mode 100644 index 00000000000..f1433d2bf35 --- /dev/null +++ b/addons/point_of_sale/static/src/js/chrome_adapter.js @@ -0,0 +1,48 @@ +/** @odoo-module */ + +import { useService } from "@web/core/service_hook"; + +import Chrome from "point_of_sale.Chrome"; +import Registries from "point_of_sale.Registries"; +import { configureGui } from "point_of_sale.Gui"; +import { useBus } from "@web/core/bus_hook"; +import PosComponent from "point_of_sale.PosComponent"; +import PopupControllerMixin from "point_of_sale.PopupControllerMixin"; +import { useErrorHandlers } from "point_of_sale.custom_hooks"; +import { ConnectionLostError } from "@web/core/network/rpc_service"; + +function setupResponsivePlugin(env) { + const isMobile = () => window.innerWidth <= 768; + env.isMobile = isMobile(); + const updateEnv = owl.utils.debounce(() => { + if (env.isMobile !== isMobile()) { + env.isMobile = !env.isMobile; + env.qweb.forceUpdate(); + } + }, 15); + window.addEventListener("resize", updateEnv); +} + +export class ChromeAdapter extends PopupControllerMixin(PosComponent) { + setup() { + this.PosChrome = Registries.Component.get(Chrome); + this.legacyActionManager = useService("legacy_action_manager"); + useErrorHandlers(); + useBus(this.env.bus, "ERROR_DISPATCHED", (error) => { + if (error instanceof ConnectionLostError) { + error = { code: -9999 }; // just a negative number + } + this._handlePushOrderError(error); + }); + + this.env = owl.Component.env; + useBus(this.env.qweb, "update", () => this.render()); + const chrome = owl.hooks.useRef("chrome"); + owl.hooks.onMounted(async () => { + await chrome.comp.start(); + configureGui({ component: chrome.comp }); + setupResponsivePlugin(this.env); + }); + } +} +ChromeAdapter.template = owl.tags.xml``; diff --git a/addons/point_of_sale/static/src/js/debug_manager.js b/addons/point_of_sale/static/src/js/debug_manager.js index bcc9f60835f..b1a185e8417 100644 --- a/addons/point_of_sale/static/src/js/debug_manager.js +++ b/addons/point_of_sale/static/src/js/debug_manager.js @@ -1,20 +1,21 @@ -odoo.define('point_of_sale.DebugManager.Backend', function(require) { - 'use strict'; +/** @odoo-module */ - const { _t } = require('web.core'); - const DebugManager = require('web.DebugManager.Backend'); +import { registry } from "@web/core/registry"; - DebugManager.include({ - /** - * Runs the JS (desktop) tests - */ - perform_pos_js_tests() { - this.do_action({ - name: _t('JS Tests'), - target: 'new', - type: 'ir.actions.act_url', - url: '/pos/ui/tests?mod=*', +function runPoSJSTests({ env }) { + return { + type: "item", + description: env._t("Run Point of Sale JS Tests"), + callback: () => { + env.services.action.doAction({ + name: env._t("JS Tests"), + target: "new", + type: "ir.actions.act_url", + url: "/pos/ui/tests?mod=*", }); }, - }); -}); + sequence: 35, + }; +} + +registry.category("debug").add("point_of_sale.runPoSJSTests", runPoSJSTests); diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js index 346a61679f4..a851c9379c2 100644 --- a/addons/point_of_sale/static/src/js/main.js +++ b/addons/point_of_sale/static/src/js/main.js @@ -1,49 +1,34 @@ -odoo.define('web.web_client', function (require) { - 'use strict'; +/** @odoo-module */ - const AbstractService = require('web.AbstractService'); - const env = require('web.env'); - const WebClient = require('web.AbstractWebClient'); - const Chrome = require('point_of_sale.Chrome'); - const Registries = require('point_of_sale.Registries'); - const { configureGui } = require('point_of_sale.Gui'); +import { startWebClient } from "@web/start"; - owl.config.mode = env.isDebug() ? 'dev' : 'prod'; - owl.Component.env = env; +import { ChromeAdapter } from "@point_of_sale/js/chrome_adapter"; +import Registries from "point_of_sale.Registries"; +import { registry } from "@web/core/registry"; +// For consistency's sake, we should trigger"WEB_CLIENT_READY" on the bus when PosApp is mounted +// But we can't since mail and some other poll react on that cue, and we don't want those services started +class PosApp extends owl.Component { + setup() { + this.Components = registry.category("main_components").getEntries(); + } +} +PosApp.template = owl.tags.xml` + + +
+ + + +
+ +`; +PosApp.components = { ChromeAdapter }; + +function startPosApp() { Registries.Component.add(owl.misc.Portal); + Registries.Component.freeze(); + startWebClient(PosApp); +} - function setupResponsivePlugin(env) { - const isMobile = () => window.innerWidth <= 768; - env.isMobile = isMobile(); - const updateEnv = owl.utils.debounce(() => { - if (env.isMobile !== isMobile()) { - env.isMobile = !env.isMobile; - env.qweb.forceUpdate(); - } - }, 15); - window.addEventListener("resize", updateEnv); - } - - setupResponsivePlugin(owl.Component.env); - - async function startPosApp(webClient) { - Registries.Component.freeze(); - await env.session.is_bound; - env.qweb.addTemplates(env.session.owlTemplates); - env.bus = new owl.core.EventBus(); - await owl.utils.whenReady(); - await webClient.setElement(document.body); - await webClient.start(); - webClient.isStarted = true; - const chrome = new (Registries.Component.get(Chrome))(null, { webClient }); - await chrome.mount(document.querySelector('.o_action_manager')); - await chrome.start(); - configureGui({ component: chrome }); - } - - AbstractService.prototype.deployServices(env); - const webClient = new WebClient(); - startPosApp(webClient); - return webClient; -}); +startPosApp(); diff --git a/addons/point_of_sale/static/src/xml/debug_manager.xml b/addons/point_of_sale/static/src/xml/debug_manager.xml deleted file mode 100644 index 7ecc7cc1e6e..00000000000 --- a/addons/point_of_sale/static/src/xml/debug_manager.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - Run Point of Sale JS Tests - - - - diff --git a/addons/point_of_sale/static/tests/tours/point_of_sale.js b/addons/point_of_sale/static/tests/tours/point_of_sale.js index c9ba1a9ae4e..744a5fa56a6 100644 --- a/addons/point_of_sale/static/tests/tours/point_of_sale.js +++ b/addons/point_of_sale/static/tests/tours/point_of_sale.js @@ -50,7 +50,7 @@ odoo.define('point_of_sale.tour.pricelist', function (require) { var steps = [{ // Leave category displayed by default content: 'waiting for loading to finish', extra_trigger: 'body .pos:not(:has(.loader))', // Pos has finished loading - trigger: 'body:not(.oe_wait)', // WebClient has finished Loading + trigger: '.o_loading_indicator:not(.o_loading)', // WebClient has finished Loading run: function () { var product_wall_shelf = posmodel.db.search_product_in_category(0, 'Wall Shelf Unit')[0]; var product_small_shelf = posmodel.db.search_product_in_category(0, 'Small Shelf')[0]; diff --git a/addons/point_of_sale/views/pos_assets_index.xml b/addons/point_of_sale/views/pos_assets_index.xml index e131c52bfc1..361a3462655 100644 --- a/addons/point_of_sale/views/pos_assets_index.xml +++ b/addons/point_of_sale/views/pos_assets_index.xml @@ -32,11 +32,12 @@ 'login_number': login_number, 'debug': debug, })"/>; + odoo.loadTemplatesPromise = fetch(`/web/webclient/qweb/${odoo.session_info.cache_hashes.qweb}?bundle=web.assets_qweb`).then(doc => doc.text()); - + diff --git a/addons/point_of_sale/views/pos_assets_qunit.xml b/addons/point_of_sale/views/pos_assets_qunit.xml index d28669a3e7a..920446cdbd5 100644 --- a/addons/point_of_sale/views/pos_assets_qunit.xml +++ b/addons/point_of_sale/views/pos_assets_qunit.xml @@ -3,12 +3,8 @@ - -