From 3db0ad01a10b6e123fd2c67d5e685f64e2740f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 16 Mar 2023 14:32:03 +0000 Subject: [PATCH] [REF] web, *: rework webclient cache invalidation system The web client has a mechanism to invalidate the action and the view cache: in the basic model (and the relationalmodel), some code is looking for updates to some specific models (such as ir.actions) and trigger a `CLEAR-CACHE` event. This event is then listened by the action and view services to properly clear the caches. This mechanism was also used to reload the page after editing a company, or reloading the currencies after editing some currency. With this commit, we modify the orm service to trigger an event after each rpc. This event can then be used by the action/view service, and also by the currency/company services to perform their specific cleanup. This work is one step in the future refactoring of the relational model. closes odoo/odoo#115655 Related: odoo/enterprise#38814 Signed-off-by: Aaron Bohy (aab) --- .../src/components/tax_totals/tax_totals.js | 4 +- .../views/crm_kanban/crm_column_progress.js | 3 +- addons/mail/static/src/web/message_patch.js | 4 +- .../project_right_side_panel.js | 4 +- addons/stock/static/src/js/basic_model.js | 16 --- .../static/src/stock_warehouse_service.js | 19 +++ addons/web/static/src/core/currency.js | 11 ++ .../static/src/core/network/rpc_service.js | 24 +--- addons/web/static/src/core/orm_service.js | 2 + .../web/static/src/legacy/js/core/session.js | 17 --- .../src/legacy/js/public/public_root.js | 2 - .../src/legacy/js/views/basic/basic_model.js | 76 ------------ addons/web/static/src/legacy/legacy_setup.js | 3 - addons/web/static/src/legacy/utils.js | 16 --- .../web/static/src/views/fields/formatters.js | 4 +- .../views/fields/monetary/monetary_field.js | 8 +- .../src/views/kanban/kanban_renderer.js | 7 +- .../web/static/src/views/relational_model.js | 37 ------ addons/web/static/src/views/view_service.js | 13 +- .../src/webclient/actions/action_service.js | 12 +- .../static/src/webclient/company_service.js | 13 +- .../static/src/webclient/currency_service.js | 29 +++++ .../loading_indicator/loading_indicator.js | 14 ++- .../tests/core/network/rpc_service_tests.js | 16 +-- .../web/static/tests/helpers/mock_services.js | 30 ++++- addons/web/static/tests/setup.js | 11 +- .../tests/views/fields/formatters_tests.js | 3 +- .../views/fields/monetary_field_tests.js | 47 +++---- .../tests/views/form/form_view_tests.js | 115 +----------------- .../web/static/tests/views/list_view_tests.js | 43 ------- .../static/tests/views/view_service_tests.js | 34 ++++++ .../webclient/actions/window_action_tests.js | 7 ++ .../tests/webclient/company_service_tests.js | 32 +++++ .../tests/webclient/currency_service_tests.js | 31 +++++ .../webclient/loading_indicator_tests.js | 42 ++++--- 35 files changed, 318 insertions(+), 431 deletions(-) delete mode 100644 addons/stock/static/src/js/basic_model.js create mode 100644 addons/stock/static/src/stock_warehouse_service.js create mode 100644 addons/web/static/src/core/currency.js create mode 100644 addons/web/static/src/webclient/currency_service.js create mode 100644 addons/web/static/tests/webclient/company_service_tests.js create mode 100644 addons/web/static/tests/webclient/currency_service_tests.js diff --git a/addons/account/static/src/components/tax_totals/tax_totals.js b/addons/account/static/src/components/tax_totals/tax_totals.js index 499ec58d78f..27413903ccc 100644 --- a/addons/account/static/src/components/tax_totals/tax_totals.js +++ b/addons/account/static/src/components/tax_totals/tax_totals.js @@ -4,7 +4,7 @@ import { formatFloat, formatMonetary } from "@web/views/fields/formatters"; import { parseFloat } from "@web/views/fields/parsers"; import { standardFieldProps } from "@web/views/fields/standard_field_props"; import { registry } from "@web/core/registry"; -import { session } from "@web/session"; +import { getCurrency } from "@web/core/currency"; const { Component, onPatched, onWillUpdateProps, useRef, useState } = owl; @@ -123,7 +123,7 @@ export class TaxTotalsComponent extends Component { } get currency() { - return session.currencies[this.currencyId]; + return getCurrency(this.currencyId); } invalidate() { diff --git a/addons/crm/static/src/views/crm_kanban/crm_column_progress.js b/addons/crm/static/src/views/crm_kanban/crm_column_progress.js index 99f8fc718a0..2452bf91624 100644 --- a/addons/crm/static/src/views/crm_kanban/crm_column_progress.js +++ b/addons/crm/static/src/views/crm_kanban/crm_column_progress.js @@ -4,6 +4,7 @@ import { onWillStart } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; import { ColumnProgress } from "@web/views/view_components/column_progress"; import { session } from "@web/session"; +import { getCurrency } from "@web/core/currency"; export class CrmColumnProgress extends ColumnProgress { static props = { @@ -29,7 +30,7 @@ export class CrmColumnProgress extends ColumnProgress { const title = rrField.string || this.env._t("Count"); let currency = false; if (value && rrField.currency_field) { - currency = session.currencies[session.company_currency_id]; + currency = getCurrency(session.company_currency_id); } return { value, currency, title }; } diff --git a/addons/mail/static/src/web/message_patch.js b/addons/mail/static/src/web/message_patch.js index 267da3edd01..61273c005fd 100644 --- a/addons/mail/static/src/web/message_patch.js +++ b/addons/mail/static/src/web/message_patch.js @@ -6,8 +6,8 @@ import { useService } from "@web/core/utils/hooks"; import { format } from "web.field_utils"; import { deserializeDateTime } from "@web/core/l10n/dates"; import { registry } from "@web/core/registry"; -import { session } from "@web/session"; import { _t } from "@web/core/l10n/translation"; +import { getCurrency } from "@web/core/currency"; const formatters = registry.category("formatters"); @@ -93,7 +93,7 @@ patch(Message.prototype, "mail/web", { case "monetary": return format.monetary(trackingValue.value, undefined, { currency: trackingValue.currencyId - ? session.currencies[trackingValue.currencyId] + ? getCurrency(trackingValue.currencyId) : undefined, forceString: true, }); diff --git a/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.js b/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.js index 50f41722f68..21cd096b121 100644 --- a/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.js +++ b/addons/project/static/src/components/project_right_side_panel/project_right_side_panel.js @@ -2,13 +2,13 @@ import { useService } from '@web/core/utils/hooks'; import { formatFloat } from '@web/views/fields/formatters'; -import { session } from '@web/session'; import { ViewButton } from '@web/views/view_button/view_button'; import { FormViewDialog } from '@web/views/view_dialogs/form_view_dialog'; import { ProjectRightSidePanelSection } from './components/project_right_side_panel_section'; import { ProjectMilestone } from './components/project_milestone'; import { ProjectProfitability } from './components/project_profitability'; +import { getCurrency } from '@web/core/currency'; const { Component, onWillStart, useState } = owl; @@ -75,7 +75,7 @@ export class ProjectRightSidePanel extends Component { 'digits': [false, 0], 'noSymbol': true, }); - const currency = session.currencies[this.currencyId]; + const currency = getCurrency(this.currencyId); if (!currency) { return valueFormatted; } diff --git a/addons/stock/static/src/js/basic_model.js b/addons/stock/static/src/js/basic_model.js deleted file mode 100644 index 6a5653c6314..00000000000 --- a/addons/stock/static/src/js/basic_model.js +++ /dev/null @@ -1,16 +0,0 @@ -odoo.define('stock.BasicModel', function (require) { -"use strict"; - -var BasicModel = require('web.BasicModel'); -var localStorage = require('web.local_storage'); - -BasicModel.include({ - - _invalidateCache: function (dataPoint) { - this._super.apply(this, arguments); - if (dataPoint.model === 'stock.warehouse' && !localStorage.getItem('running_tour')) { - this.do_action('reload_context'); - } - } -}); -}); diff --git a/addons/stock/static/src/stock_warehouse_service.js b/addons/stock/static/src/stock_warehouse_service.js new file mode 100644 index 00000000000..112cc979a42 --- /dev/null +++ b/addons/stock/static/src/stock_warehouse_service.js @@ -0,0 +1,19 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { browser } from "@web/core/browser/browser"; +import { UPDATE_METHODS } from "@web/core/orm_service"; + +registry.category("services").add("stock_warehouse", { + dependencies: ["action"], + start(env, { action }) { + env.bus.addEventListener("RPC:RESPONSE", (ev) => { + const { model, method } = ev.detail.data.params; + if (model === "stock.warehouse") { + if (UPDATE_METHODS.includes(method) && !browser.localStorage.getItem("running_tour")) { + action.doAction("reload_context"); + } + } + }); + }, +}); diff --git a/addons/web/static/src/core/currency.js b/addons/web/static/src/core/currency.js new file mode 100644 index 00000000000..5e57b650b17 --- /dev/null +++ b/addons/web/static/src/core/currency.js @@ -0,0 +1,11 @@ +/** @odoo-module **/ + +import { session } from "@web/session"; + +export const currencies = session.currencies || {}; +// to make sure code is reading currencies from here +delete session.currencies; + +export function getCurrency(id) { + return currencies[id]; +} diff --git a/addons/web/static/src/core/network/rpc_service.js b/addons/web/static/src/core/network/rpc_service.js index 16781e6c73f..f47183485dc 100644 --- a/addons/web/static/src/core/network/rpc_service.js +++ b/addons/web/static/src/core/network/rpc_service.js @@ -56,16 +56,12 @@ export function jsonrpc(env, rpcId, url, params, settings = {}) { let rejectFn; const promise = new Promise((resolve, reject) => { rejectFn = reject; - if (!settings.silent) { - bus.trigger("RPC:REQUEST", data.id); - } + bus.trigger("RPC:REQUEST", { data, settings }); // handle success request.addEventListener("load", () => { if (request.status === 502) { // If Odoo is behind another server (eg.: nginx) - if (!settings.silent) { - bus.trigger("RPC:RESPONSE", data.id); - } + bus.trigger("RPC:RESPONSE", { data, settings }); reject(new ConnectionLostError(url)); return; } @@ -75,15 +71,11 @@ export function jsonrpc(env, rpcId, url, params, settings = {}) { } catch { // the response isn't json parsable, which probably means that the rpc request could // not be handled by the server, e.g. PoolError('The Connection Pool Is Full') - if (!settings.silent) { - bus.trigger("RPC:RESPONSE", data.id); - } + bus.trigger("RPC:RESPONSE", { data, settings }); return reject(new ConnectionLostError(url)); } const { error: responseError, result: responseResult } = params; - if (!settings.silent) { - bus.trigger("RPC:RESPONSE", data.id); - } + bus.trigger("RPC:RESPONSE", { data, settings }); if (!responseError) { return resolve(responseResult); } @@ -92,9 +84,7 @@ export function jsonrpc(env, rpcId, url, params, settings = {}) { }); // handle failure request.addEventListener("error", () => { - if (!settings.silent) { - bus.trigger("RPC:RESPONSE", data.id); - } + bus.trigger("RPC:RESPONSE", { data, settings }); reject(new ConnectionLostError(url)); }); // configure and send request @@ -110,9 +100,7 @@ export function jsonrpc(env, rpcId, url, params, settings = {}) { if (request.abort) { request.abort(); } - if (!settings.silent) { - bus.trigger("RPC:RESPONSE", data.id); - } + bus.trigger("RPC:RESPONSE", { data, settings }); if (rejectError) { rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort")); } diff --git a/addons/web/static/src/core/orm_service.js b/addons/web/static/src/core/orm_service.js index 3fbcbec6ac4..bb2cf6973b0 100644 --- a/addons/web/static/src/core/orm_service.js +++ b/addons/web/static/src/core/orm_service.js @@ -84,6 +84,8 @@ function validateArray(name, array) { } } +export const UPDATE_METHODS = ["unlink", "create", "write", "action_archive", "action_unarchive"]; + export class ORM { constructor(rpc, user) { /** @protected */ diff --git a/addons/web/static/src/legacy/js/core/session.js b/addons/web/static/src/legacy/js/core/session.js index 74093dcb59c..4b78538fbb6 100644 --- a/addons/web/static/src/legacy/js/core/session.js +++ b/addons/web/static/src/legacy/js/core/session.js @@ -301,23 +301,6 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { } }, - /** - * Reload the currencies (initially given in session_info). This is meant to - * be called when changes are made on 'res.currency' records (e.g. when - * (de-)activating a currency). For the sake of simplicity, we reload all - * session_info. - * - * FIXME: this whole currencies handling should be moved out of session. - * - * @returns {$.promise} - */ - reloadCurrencies: function () { - var self = this; - return this.rpc('/web/session/get_session_info').then(function (result) { - self.currencies = result.currencies; - }); - }, - setCompanies: function (main_company_id, company_ids) { var hash = $.bbq.getState(); hash.cids = company_ids.sort(function(a, b) { diff --git a/addons/web/static/src/legacy/js/public/public_root.js b/addons/web/static/src/legacy/js/public/public_root.js index 58bb299c19c..b33f03f080f 100644 --- a/addons/web/static/src/legacy/js/public/public_root.js +++ b/addons/web/static/src/legacy/js/public/public_root.js @@ -12,7 +12,6 @@ import lazyloader from "web.public.lazyloader"; import { makeLegacyNotificationService, - makeLegacyRpcService, makeLegacySessionService, makeLegacyDialogMappingService, mapLegacyEnvToWowlEnv, @@ -369,7 +368,6 @@ export async function createPublicRoot(RootWidget) { AbstractService.prototype.deployServices(legacyEnv); // add a bunch of mapping services that will redirect service calls from the legacy env // to the wowl env - serviceRegistry.add("legacy_rpc", makeLegacyRpcService(legacyEnv)); serviceRegistry.add("legacy_session", makeLegacySessionService(legacyEnv, session)); serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv)); serviceRegistry.add("legacy_dialog_mapping", makeLegacyDialogMappingService(legacyEnv)); diff --git a/addons/web/static/src/legacy/js/views/basic/basic_model.js b/addons/web/static/src/legacy/js/views/basic/basic_model.js index 211b0d7d10e..53ced035546 100644 --- a/addons/web/static/src/legacy/js/views/basic/basic_model.js +++ b/addons/web/static/src/legacy/js/views/basic/basic_model.js @@ -91,7 +91,6 @@ var Domain = require('web.Domain'); var session = require('web.session'); var utils = require('web.utils'); var viewUtils = require('web.viewUtils'); -var localStorage = require('web.local_storage'); var _t = core._t; @@ -144,15 +143,6 @@ var BasicModel = AbstractModel.extend({ // constants OPEN_GROUP_LIMIT: 10, // after this limit, groups are automatically folded - // list of models for which the DataManager's cache should be cleared on - // create, update and delete operations - noCacheModels: [ - 'ir.actions.act_window', - 'ir.filters', - 'ir.ui.view', - 'res.currency', - ], - /** * @override */ @@ -398,8 +388,6 @@ var BasicModel = AbstractModel.extend({ record.count--; } }); - // optionally clear the DataManager's cache - self._invalidateCache(records[0]); }); }, /** @@ -1192,9 +1180,6 @@ var BasicModel = AbstractModel.extend({ // Erase changes as they have been applied record._changes = {}; - // Optionally clear the DataManager's cache - self._invalidateCache(record); - self.unfreezeOrder(record.id); // Update the data directly or reload them @@ -1298,39 +1283,6 @@ var BasicModel = AbstractModel.extend({ const has_x_active = fields.includes('x_active'); return has_x_active?'x_active':undefined }, - /** - * Toggle the active value of given records (to archive/unarchive them) - * - * @param {Array} recordIDs local ids of the records to (un)archive - * @param {boolean} value false to archive, true to unarchive (value of the active field) - * @param {string} parentID id of the parent resource to reload - * @returns {Promise} resolves to the parent id - */ - toggleActive: function (recordIDs, parentID) { - var self = this; - var parent = this.localData[parentID]; - var resIDs = _.map(recordIDs, function (recordID) { - return self.localData[recordID].res_id; - }); - return this._rpc({ - model: parent.model, - method: 'toggle_active', - args: [resIDs], - }) - .then(function (action) { - // optionally clear the DataManager's cache - self._invalidateCache(parent); - if (!_.isEmpty(action)) { - return self.do_action(action, { - on_close: function () { - return self.trigger_up('reload'); - } - }); - } else { - return self.reload(parentID); - } - }); - }, /** * Archive the given records * @@ -1347,8 +1299,6 @@ var BasicModel = AbstractModel.extend({ args: [resIDs], }) .then(function (action) { - // optionally clear the DataManager's cache - self._invalidateCache(parent); if (!_.isEmpty(action)) { return new Promise(function (resolve, reject) { self.do_action(action, { @@ -1390,7 +1340,6 @@ var BasicModel = AbstractModel.extend({ }) .then(function (action) { // optionally clear the DataManager's cache - self._invalidateCache(parent); if (!_.isEmpty(action)) { return new Promise(function (resolve, reject) { self.do_action(action, { @@ -4044,31 +3993,6 @@ var BasicModel = AbstractModel.extend({ _getUngroupedListDomain(list) { return list.domain || []; }, - /** - * Invalidates the DataManager's cache if the main model (i.e. the model of - * its root parent) of the given dataPoint is a model in 'noCacheModels'. - * - * Reloads the currencies if the main model is 'res.currency'. - * Reloads the webclient if we modify a res.company, to (un)activate the - * multi-company environment if we are not in a tour test. - * - * @private - * @param {Object} dataPoint - */ - _invalidateCache: function (dataPoint) { - while (dataPoint.parentID) { - dataPoint = this.localData[dataPoint.parentID]; - } - if (dataPoint.model === 'res.currency') { - session.reloadCurrencies(); - } - if (dataPoint.model === 'res.company' && !localStorage.getItem('running_tour')) { - this.do_action('reload_context'); - } - if (_.contains(this.noCacheModels, dataPoint.model)) { - core.bus.trigger('clear_cache'); - } - }, /** * Returns true if the field is protected against changes, looking for a * readonly modifier unless there is a force_save modifier (checking first diff --git a/addons/web/static/src/legacy/legacy_setup.js b/addons/web/static/src/legacy/legacy_setup.js index 4395832a3f6..4b310f10cd6 100644 --- a/addons/web/static/src/legacy/legacy_setup.js +++ b/addons/web/static/src/legacy/legacy_setup.js @@ -3,7 +3,6 @@ import { registry } from "../core/registry"; import { makeLegacyNotificationService, - makeLegacyRpcService, makeLegacySessionService, makeLegacyDialogMappingService, makeLegacyCrashManagerService, @@ -34,8 +33,6 @@ export const legacySetupProm = new Promise((resolve) => { serviceRegistry.add("legacy_action_manager", legacyActionManagerService); // add a service to redirect rpc events triggered on the bus in the // legacy env on the bus in the wowl env - const legacyRpcService = makeLegacyRpcService(legacyEnv); - serviceRegistry.add("legacy_rpc", legacyRpcService); const legacySessionService = makeLegacySessionService(legacyEnv, session); serviceRegistry.add("legacy_session", legacySessionService); const legacyWebClientService = makeLegacyWebClientService(legacyEnv); diff --git a/addons/web/static/src/legacy/utils.js b/addons/web/static/src/legacy/utils.js index 0f312a717d3..f10d67e5754 100644 --- a/addons/web/static/src/legacy/utils.js +++ b/addons/web/static/src/legacy/utils.js @@ -15,22 +15,6 @@ import { useComponent } from "@odoo/owl"; export const wowlServicesSymbol = Symbol("wowlServices"); -export function makeLegacyRpcService(legacyEnv) { - return { - start(env) { - legacyEnv.bus.on("rpc_request", null, (rpcId) => { - env.bus.trigger("RPC:REQUEST", rpcId); - }); - legacyEnv.bus.on("rpc_response", null, (rpcId) => { - env.bus.trigger("RPC:RESPONSE", rpcId); - }); - legacyEnv.bus.on("rpc_response_failed", null, (rpcId) => { - env.bus.trigger("RPC:RESPONSE", rpcId); - }); - }, - }; -} - /** * Returns a service that maps legacy dialogs * to new environment services behavior. diff --git a/addons/web/static/src/views/fields/formatters.js b/addons/web/static/src/views/fields/formatters.js index d4d6b0896e6..b16e1e3b33a 100644 --- a/addons/web/static/src/views/fields/formatters.js +++ b/addons/web/static/src/views/fields/formatters.js @@ -6,9 +6,9 @@ import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; import { escape, intersperse, nbsp, sprintf } from "@web/core/utils/strings"; import { isBinarySize } from "@web/core/utils/binary"; -import { session } from "@web/session"; import { markup } from "@odoo/owl"; +import { getCurrency } from "@web/core/currency"; // ----------------------------------------------------------------------------- // Helpers @@ -366,7 +366,7 @@ export function formatMonetary(value, options = {}) { const dataValue = options.data[currencyField]; currencyId = Array.isArray(dataValue) ? dataValue[0] : dataValue; } - const currency = session.currencies[currencyId]; + const currency = getCurrency(currencyId); const digits = options.digits || (currency && currency.digits); let formattedValue; diff --git a/addons/web/static/src/views/fields/monetary/monetary_field.js b/addons/web/static/src/views/fields/monetary/monetary_field.js index 9649b6d4fbc..0bbf1b1bba7 100644 --- a/addons/web/static/src/views/fields/monetary/monetary_field.js +++ b/addons/web/static/src/views/fields/monetary/monetary_field.js @@ -7,9 +7,9 @@ import { parseMonetary } from "../parsers"; import { useInputField } from "../input_field_hook"; import { useNumpadDecimal } from "../numpad_decimal_hook"; import { standardFieldProps } from "../standard_field_props"; -import { session } from "@web/session"; import { Component } from "@odoo/owl"; +import { getCurrency } from "@web/core/currency"; export class MonetaryField extends Component { static template = "web.MonetaryField"; @@ -44,8 +44,8 @@ export class MonetaryField extends Component { return currency && currency[0]; } get currency() { - if (!isNaN(this.currencyId) && this.currencyId in session.currencies) { - return session.currencies[this.currencyId]; + if (!isNaN(this.currencyId)) { + return getCurrency(this.currencyId) || null; } return null; } @@ -61,7 +61,7 @@ export class MonetaryField extends Component { if (!this.currency) { return null; } - return session.currencies[this.currencyId].digits; + return getCurrency(this.currencyId).digits; } get formattedValue() { diff --git a/addons/web/static/src/views/kanban/kanban_renderer.js b/addons/web/static/src/views/kanban/kanban_renderer.js index 773a0f70f55..e4377c26bc9 100644 --- a/addons/web/static/src/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/views/kanban/kanban_renderer.js @@ -9,7 +9,6 @@ import { registry } from "@web/core/registry"; import { useBus, useService } from "@web/core/utils/hooks"; import { useSortable } from "@web/core/utils/sortable"; import { sprintf } from "@web/core/utils/strings"; -import { session } from "@web/session"; import { archParseBoolean, isNull, isRelational } from "@web/views/utils"; import { ColumnProgress } from "@web/views/view_components/column_progress"; import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog"; @@ -350,11 +349,7 @@ export class KanbanRenderer extends Component { const { sumField } = this.props.list.model.progressAttributes; const value = group.getAggregates(sumField && sumField.name); const title = sumField ? sumField.string : this.env._t("Count"); - let currency = false; - if (sumField && value && sumField.currency_field) { - currency = session.currencies[session.company_currency_id]; - } - return { value, currency, title }; + return { value, title }; } generateGhostColumns() { diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index 13631e68105..d01eeb74228 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -298,32 +298,6 @@ class DataPoint { */ setup() {} - /** - * TODO WOWL: adapt this comment to new system - * Also, is datapoint the best place ? Could be in record. - * - * Invalidates the DataManager's cache if the main model (i.e. the model of - * its root parent) of the given dataPoint is a model in 'noCacheModels'. - * - * Reloads the currencies if the main model is 'res.currency'. - * Reloads the webclient if we modify a res.company, to (un)activate the - * multi-company environment if we are not in a tour test. - * - */ - invalidateCache() { - if (this.resModel === "res.currency") { - // TODO WOWL: this needs to be ported from basic model for the list view to have it. - // session.reloadCurrencies(); - // There is a test in form view but it uses the basic model for now. - } - if (this.resModel === "res.company") { - this.model.action.doAction("reload_context"); - } - if (this.model.noCacheModels.includes(this.resModel)) { - this.model.env.bus.trigger("CLEAR-CACHES"); - } - } - /** * @param {Object} [activeFields={}] */ @@ -566,7 +540,6 @@ export class Record extends DataPoint { await toggleArchive(this.model, this.resModel, [this.resId], true, this.context); await this.load(); this.model.notify(); - this.invalidateCache(); } async askChanges() { @@ -641,7 +614,6 @@ export class Record extends DataPoint { this._changes = {}; this.preloadedData = {}; } - this.invalidateCache(); } discard() { @@ -982,7 +954,6 @@ export class Record extends DataPoint { await toggleArchive(this.model, this.resModel, [this.resId], false, this.context); await this.load(); this.model.notify(); - this.invalidateCache(); } async update(changes, options) { @@ -1492,7 +1463,6 @@ export class Record extends DataPoint { this.data.id = this.resId; this.resIds.push(this.resId); this._changes = {}; - this.invalidateCache(); } else if (keys.length > 0) { try { await this.model.orm.write(this.resModel, [this.resId], changes, { context }); @@ -1503,7 +1473,6 @@ export class Record extends DataPoint { throw e; } this._changes = {}; - this.invalidateCache(); } this.isInQuickCreation = false; @@ -1655,7 +1624,6 @@ class DynamicList extends DataPoint { const resIds = await this.getResIds(isSelected); await toggleArchive(this.model, this.resModel, resIds, true, this.context); await this.model.load(); - this.invalidateCache(); return resIds; } @@ -1724,7 +1692,6 @@ class DynamicList extends DataPoint { const resIds = await this.getResIds(isSelected); await toggleArchive(this.model, this.resModel, resIds, false, this.context); await this.model.load(); - this.invalidateCache(); return resIds; } @@ -1770,7 +1737,6 @@ class DynamicList extends DataPoint { try { const context = this.context; await this.model.orm.write(this.resModel, resIds, changes, { context }); - this.invalidateCache(); validSelection.forEach((record) => { record.selected = false; }); @@ -3579,9 +3545,6 @@ export class RelationalModel extends Model { this.root = null; this.nextId = 1; - - // list of models for which the DataManager's cache should be cleared on create, update and delete operations - this.noCacheModels = ["ir.actions.act_window", "ir.filters", "ir.ui.view", "res.currency"]; } /** diff --git a/addons/web/static/src/views/view_service.js b/addons/web/static/src/views/view_service.js index 752781801b6..4c00ee5fa03 100644 --- a/addons/web/static/src/views/view_service.js +++ b/addons/web/static/src/views/view_service.js @@ -3,6 +3,7 @@ import { deepCopy } from "@web/core/utils/objects"; import { registry } from "@web/core/registry"; import { generateLegacyLoadViewsResult } from "@web/legacy/legacy_load_views"; +import { UPDATE_METHODS } from "@web/core/orm_service"; /** * @typedef {Object} IrFilter @@ -51,11 +52,21 @@ export const viewService = { start(env, { orm }) { let cache = {}; - env.bus.addEventListener("CLEAR-CACHES", () => { + function clearCache() { cache = {}; const processedArchs = registry.category("__processed_archs__"); processedArchs.content = {}; processedArchs.trigger("UPDATE"); + } + + env.bus.addEventListener("CLEAR-CACHES", clearCache); + env.bus.addEventListener("RPC:RESPONSE", (ev) => { + const { model, method } = ev.detail.data.params; + if (["ir.ui.view", "ir.filters"].includes(model)) { + if (UPDATE_METHODS.includes(method)) { + clearCache(); + } + } }); /** diff --git a/addons/web/static/src/webclient/actions/action_service.js b/addons/web/static/src/webclient/actions/action_service.js index b1ef19452a2..bb06fbd018b 100644 --- a/addons/web/static/src/webclient/actions/action_service.js +++ b/addons/web/static/src/webclient/actions/action_service.js @@ -14,6 +14,7 @@ import { View, ViewNotFoundError } from "@web/views/view"; import { ActionDialog } from "./action_dialog"; import { CallbackRecorder } from "./action_hook"; import { ReportAction } from "./reports/report_action"; +import { UPDATE_METHODS } from "@web/core/orm_service"; import { Component, @@ -111,6 +112,12 @@ function makeActionManager(env) { env.bus.addEventListener("CLEAR-CACHES", () => { actionCache = {}; }); + env.bus.addEventListener("RPC:RESPONSE", (ev) => { + const { model, method } = ev.detail.data.params; + if (model === "ir.actions.act_window" && UPDATE_METHODS.includes(method)) { + actionCache = {}; + } + }); // --------------------------------------------------------------------------- // misc @@ -836,7 +843,10 @@ function makeActionManager(env) { }); } if (action.close) { - return doAction({ type: "ir.actions.act_window_close" }, { onClose: options.onClose }); + return doAction( + { type: "ir.actions.act_window_close" }, + { onClose: options.onClose } + ); } else if (options.onClose) { options.onClose(); } diff --git a/addons/web/static/src/webclient/company_service.js b/addons/web/static/src/webclient/company_service.js index 7d3c4e8833f..a868ca77063 100644 --- a/addons/web/static/src/webclient/company_service.js +++ b/addons/web/static/src/webclient/company_service.js @@ -4,6 +4,7 @@ import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; import { symmetricalDifference } from "../core/utils/arrays"; import { session } from "@web/session"; +import { UPDATE_METHODS } from "@web/core/orm_service"; function parseCompanyIds(cidsFromHash) { const cids = []; @@ -30,8 +31,8 @@ function computeAllowedCompanyIds(cids) { } export const companyService = { - dependencies: ["user", "router", "cookie"], - start(env, { user, router, cookie }) { + dependencies: ["user", "router", "cookie", "action"], + start(env, { user, router, cookie, action }) { let cids; if ("cids" in router.current.hash) { cids = parseCompanyIds(router.current.hash.cids); @@ -47,6 +48,14 @@ export const companyService = { user.updateContext({ allowed_company_ids: allowedCompanyIds }); const availableCompanies = session.user_companies.allowed_companies; + env.bus.addEventListener("RPC:RESPONSE", (ev) => { + const { model, method } = ev.detail.data.params; + if (model === "res.company" && UPDATE_METHODS.includes(method)) { + if (!browser.localStorage.getItem("running_tour")) { + action.doAction("reload_context"); + } + } + }); return { availableCompanies, get allowedCompanyIds() { diff --git a/addons/web/static/src/webclient/currency_service.js b/addons/web/static/src/webclient/currency_service.js new file mode 100644 index 00000000000..301bf1f31c8 --- /dev/null +++ b/addons/web/static/src/webclient/currency_service.js @@ -0,0 +1,29 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { currencies } from "@web/core/currency"; +import { UPDATE_METHODS } from "@web/core/orm_service"; + +export const currencyService = { + dependencies: ["rpc"], + start(env, { rpc }) { + /** + * Reload the currencies (initially given in session_info) + */ + async function reloadCurrencies() { + const result = await rpc("/web/session/get_session_info"); + for (const k in currencies) { + delete currencies[k]; + } + Object.assign(currencies, result); + } + env.bus.addEventListener("RPC:RESPONSE", (ev) => { + const { model, method } = ev.detail.data.params; + if (model === "res.currency" && UPDATE_METHODS.includes(method)) { + reloadCurrencies(); + } + }); + }, +}; + +registry.category("services").add("currency", currencyService); diff --git a/addons/web/static/src/webclient/loading_indicator/loading_indicator.js b/addons/web/static/src/webclient/loading_indicator/loading_indicator.js index eebe2707d06..fdd63fa9d9f 100644 --- a/addons/web/static/src/webclient/loading_indicator/loading_indicator.js +++ b/addons/web/static/src/webclient/loading_indicator/loading_indicator.js @@ -36,7 +36,10 @@ export class LoadingIndicator extends Component { }); } - requestCall({ detail: rpcId }) { + requestCall({ detail }) { + if (detail.settings.silent) { + return; + } if (this.state.count === 0) { browser.clearTimeout(this.startShowTimer); this.startShowTimer = browser.setTimeout(() => { @@ -49,12 +52,15 @@ export class LoadingIndicator extends Component { } }, 250); } - this.rpcIds.add(rpcId); + this.rpcIds.add(detail.data.id); this.state.count++; } - responseCall({ detail: rpcId }) { - this.rpcIds.delete(rpcId); + responseCall({ detail }) { + if (detail.settings.silent) { + return; + } + this.rpcIds.delete(detail.data.id); this.state.count = this.rpcIds.size; if (this.state.count === 0) { browser.clearTimeout(this.startShowTimer); diff --git a/addons/web/static/tests/core/network/rpc_service_tests.js b/addons/web/static/tests/core/network/rpc_service_tests.js index 4f68df4a617..afb957cce40 100644 --- a/addons/web/static/tests/core/network/rpc_service_tests.js +++ b/addons/web/static/tests/core/network/rpc_service_tests.js @@ -190,20 +190,22 @@ QUnit.test("check trigger RPC:REQUEST and RPC:RESPONSE for a simple rpc", async }); const rpcIdsRequest = []; const rpcIdsResponse = []; - env.bus.addEventListener("RPC:REQUEST", (rpcId) => { - rpcIdsRequest.push(rpcId); - assert.step("RPC:REQUEST"); + env.bus.addEventListener("RPC:REQUEST", (ev) => { + rpcIdsRequest.push(ev.detail.data.id); + const silent = ev.detail.settings.silent; + assert.step("RPC:REQUEST" + (silent ? "(silent)" : "")); }); - env.bus.addEventListener("RPC:RESPONSE", (rpcId) => { - rpcIdsResponse.push(rpcId); - assert.step("RPC:RESPONSE"); + env.bus.addEventListener("RPC:RESPONSE", (ev) => { + rpcIdsResponse.push(ev.detail.data.id); + const silent = ev.detail.settings.silent; + assert.step("RPC:RESPONSE" + (silent ? "(silent)" : "")); }); await env.services.rpc("/test/"); assert.strictEqual(rpcIdsRequest.toString(), rpcIdsResponse.toString()); assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE"]); await env.services.rpc("/test/", {}, { silent: true }); - assert.verifySteps([]); + assert.verifySteps(["RPC:REQUEST(silent)", "RPC:RESPONSE(silent)"]); unpatch(browser, "mock.xhr"); }); diff --git a/addons/web/static/tests/helpers/mock_services.js b/addons/web/static/tests/helpers/mock_services.js index f3b86ac953e..4ff1e15df6c 100644 --- a/addons/web/static/tests/helpers/mock_services.js +++ b/addons/web/static/tests/helpers/mock_services.js @@ -14,6 +14,7 @@ import { uiService } from "@web/core/ui/ui_service"; import { ConnectionAbortedError } from "../../src/core/network/rpc_service"; import { Component, status } from "@odoo/owl"; +import { ormService } from "@web/core/orm_service"; // ----------------------------------------------------------------------------- // Mock Services @@ -59,14 +60,25 @@ function buildMockRPC(mockRPC) { export function makeFakeRPCService(mockRPC) { return { name: "rpc", - start() { + start(env) { const rpcService = buildMockRPC(mockRPC); - return function () { + let nextId = 1; + return function (route, params = {}, settings = {}) { let rejectFn; + const data = { + id: nextId++, + jsonrpc: "2.0", + method: "call", + params: params, + }; + env.bus.trigger("RPC:REQUEST", { data, settings }); const rpcProm = new Promise((resolve, reject) => { rejectFn = reject; rpcService(...arguments) - .then(resolve) + .then((result) => { + env.bus.trigger("RPC:RESPONSE", { data, settings }); + resolve(result); + }) .catch(reject); }); rpcProm.abort = (rejectError = true) => { @@ -307,6 +319,16 @@ export function makeFakeHTTPService(getResponse, postResponse) { }; } +function makeFakeActionService() { + return { + start() { + return { + doAction() {}, + }; + }, + }; +} + export const mocks = { color_scheme: () => fakeColorSchemeService, company: () => fakeCompanyService, @@ -321,4 +343,6 @@ export const mocks = { ui: () => uiService, user: () => userService, dialog: makeFakeDialogService, + orm: () => ormService, + action: makeFakeActionService, }; diff --git a/addons/web/static/tests/setup.js b/addons/web/static/tests/setup.js index 6c18634c9eb..472557461ed 100644 --- a/addons/web/static/tests/setup.js +++ b/addons/web/static/tests/setup.js @@ -19,6 +19,7 @@ transitionConfig.disabled = true; import { patch } from "@web/core/utils/patch"; import { App, whenReady } from "@odoo/owl"; +import { currencies } from "@web/core/currency"; const { prepareRegistriesWithCleanup } = utils; @@ -247,10 +248,6 @@ function patchSessionInfo() { load_menus: "161803", translations: "314159", }, - currencies: { - 1: { name: "USD", digits: [69, 2], position: "before", symbol: "$" }, - 2: { name: "EUR", digits: [69, 2], position: "after", symbol: "€" }, - }, user_context: { lang: "en", uid: 7, @@ -271,7 +268,11 @@ function patchSessionInfo() { }, db: "test", server_version: "1.0", - server_version_info: [1, 0, 0, "final", 0, ''], + server_version_info: [1, 0, 0, "final", 0, ""], + }); + patchWithCleanup(currencies, { + 1: { name: "USD", digits: [69, 2], position: "before", symbol: "$" }, + 2: { name: "EUR", digits: [69, 2], position: "after", symbol: "€" }, }); } diff --git a/addons/web/static/tests/views/fields/formatters_tests.js b/addons/web/static/tests/views/fields/formatters_tests.js index 2d930c471b6..97fcfc5a87e 100644 --- a/addons/web/static/tests/views/fields/formatters_tests.js +++ b/addons/web/static/tests/views/fields/formatters_tests.js @@ -2,6 +2,7 @@ import { defaultLocalization } from "@web/../tests/helpers/mock_services"; import { patchWithCleanup } from "@web/../tests/helpers/utils"; +import { currencies } from "@web/core/currency"; import { localization } from "@web/core/l10n/localization"; import { session } from "@web/session"; import { @@ -246,7 +247,7 @@ QUnit.module("Fields", (hooks) => { }); QUnit.test("formatMonetary", function (assert) { - patchWithCleanup(session.currencies, { + patchWithCleanup(currencies, { 10: { digits: [69, 2], position: "after", diff --git a/addons/web/static/tests/views/fields/monetary_field_tests.js b/addons/web/static/tests/views/fields/monetary_field_tests.js index 610f7dded3f..32a0d12fe81 100644 --- a/addons/web/static/tests/views/fields/monetary_field_tests.js +++ b/addons/web/static/tests/views/fields/monetary_field_tests.js @@ -10,7 +10,7 @@ import { patchWithCleanup, triggerEvent, } from "@web/../tests/helpers/utils"; -import { session } from "@web/session"; +import { currencies } from "@web/core/currency"; let serverData; let target; @@ -252,15 +252,12 @@ QUnit.module("Fields", (hooks) => { QUnit.test("with currency digits != 2 - float field", async function (assert) { // need to also add it to the session (as currencies are loaded there) - patchWithCleanup(session, { - currencies: { - ...session.currencies, - 3: { - name: "VEF", - symbol: "Bs.F", - position: "after", - digits: [0, 4], - }, + patchWithCleanup(currencies, { + 3: { + name: "VEF", + symbol: "Bs.F", + position: "after", + digits: [0, 4], }, }); @@ -313,15 +310,12 @@ QUnit.module("Fields", (hooks) => { QUnit.test("with currency digits != 2 - monetary field", async function (assert) { // need to also add it to the session (as currencies are loaded there) - patchWithCleanup(session, { - currencies: { - ...session.currencies, - 3: { - name: "VEF", - symbol: "Bs.F", - position: "after", - digits: [0, 4], - }, + patchWithCleanup(currencies, { + 3: { + name: "VEF", + symbol: "Bs.F", + position: "after", + digits: [0, 4], }, }); @@ -865,15 +859,12 @@ QUnit.module("Fields", (hooks) => { }, ]; - patchWithCleanup(session, { - currencies: { - ...session.currencies, - 1: { - name: "USD", - symbol: "$", - position: "before", - digits: [0, 4], - }, + patchWithCleanup(currencies, { + 1: { + name: "USD", + symbol: "$", + position: "before", + digits: [0, 4], }, }); diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index 66ae6beb7e2..d5cb0097082 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -29,9 +29,7 @@ import { tooltipService } from "@web/core/tooltip/tooltip_service"; import { CharField } from "@web/views/fields/char/char_field"; import { FormController } from "@web/views/form/form_controller"; import { session } from "@web/session"; -import legacySession from "web.session"; import { scrollerService } from "@web/core/scroller_service"; -import BasicModel from "web.BasicModel"; import { localization } from "@web/core/l10n/localization"; import { SIZES } from "@web/core/ui/ui_service"; import { errorService } from "@web/core/errors/error_service"; @@ -43,6 +41,7 @@ const serviceRegistry = registry.category("services"); const widgetRegistry = registry.category("view_widgets"); import { Component, xml, EventBus } from "@odoo/owl"; +import { companyService } from "@web/webclient/company_service"; let target; let serverData; @@ -10385,116 +10384,6 @@ QUnit.module("Views", (hooks) => { ]); }); - QUnit.test('edition in form view on a "noCache" model', async function (assert) { - patchWithCleanup(BasicModel.prototype, { - noCacheModels: BasicModel.prototype.noCacheModels.concat(["partner"]), - }); - - const form = await makeView({ - type: "form", - resModel: "partner", - serverData, - arch: `
`, - resId: 1, - mockRPC(route, args) { - if (args.method === "write") { - assert.step("write"); - } - }, - }); - - form.env.bus.on("CLEAR-CACHES", target, assert.step.bind(assert, "clear_cache")); - - await editInput(target, "[name=display_name] input", "new value"); - await clickSave(target); - - assert.verifySteps(["write", "clear_cache"]); - }); - - QUnit.test('creation in form view on a "noCache" model', async function (assert) { - patchWithCleanup(BasicModel.prototype, { - noCacheModels: BasicModel.prototype.noCacheModels.concat(["partner"]), - }); - - const form = await makeView({ - type: "form", - resModel: "partner", - serverData, - arch: `
`, - resId: 1, - mockRPC(route, args) { - if (args.method === "write") { - assert.step("write"); - } - }, - }); - - form.env.bus.on("CLEAR-CACHES", target, assert.step.bind(assert, "clear_cache")); - - await editInput(target, "[name=display_name] input", "new value"); - await clickSave(target); - - assert.verifySteps(["write", "clear_cache"]); - }); - - QUnit.test('deletion in form view on a "noCache" model', async function (assert) { - patchWithCleanup(BasicModel.prototype, { - noCacheModels: BasicModel.prototype.noCacheModels.concat(["partner"]), - }); - - const form = await makeView({ - type: "form", - serverData, - resModel: "partner", - arch: `
`, - mockRPC(route, args) { - if (args.method === "unlink") { - assert.step("unlink"); - } - }, - resId: 1, - actionMenus: {}, - }); - form.env.bus.on("CLEAR-CACHES", target, assert.step.bind(assert, "clear_cache")); - - await toggleActionMenu(target); - await toggleMenuItem(target, "Delete"); - await click(target.querySelector(".modal-footer .btn-primary")); - - assert.verifySteps(["unlink", "clear_cache"]); - }); - - QUnit.test( - "reload currencies when writing on records of model res.currency", - async function (assert) { - serverData.models["res.currency"] = { - fields: {}, - records: [{ id: 1, display_name: "some currency" }], - }; - - patchWithCleanup(legacySession, { - reloadCurrencies: function () { - assert.step("reload currencies"); - }, - }); - - await makeView({ - type: "form", - resModel: "res.currency", - serverData, - arch: '
', - resId: 1, - mockRPC(route, args) { - assert.step(args.method); - }, - }); - - await editInput(target.querySelector("[name=display_name]"), "input", "new value"); - await clickSave(target); - assert.verifySteps(["get_views", "read", "write", "reload currencies", "read"]); - } - ); - QUnit.test("keep editing after call_button fail", async function (assert) { assert.expect(4); @@ -11233,6 +11122,7 @@ QUnit.module("Views", (hooks) => { }); }, }; + serviceRegistry.add("company", companyService, { force: true }); serviceRegistry.add("action", fakeActionService, { force: true }); await makeView({ @@ -11270,6 +11160,7 @@ QUnit.module("Views", (hooks) => { }); }, }; + serviceRegistry.add("company", companyService, { force: true }); serviceRegistry.add("action", fakeActionService, { force: true }); serverData.models["res.company"].records = [ diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 76c95e0db11..5281a38d022 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -12253,49 +12253,6 @@ QUnit.module("Views", (hooks) => { ); }); - QUnit.test('list view on a "noCache" model', async function (assert) { - const list = await makeView({ - type: "list", - resModel: "foo", - serverData, - arch: ``, - mockRPC(route, args) { - if (["name_create", "unlink", "write"].includes(args.method)) { - assert.step(args.method); - } - }, - actionMenus: {}, - }); - - list.model.noCacheModels = list.model.noCacheModels.concat(["foo"]); - list.env.bus.on("CLEAR-CACHES", target, assert.step.bind(assert, "clear_cache")); - - // create a new record - await click(target.querySelector(".o_list_button_add")); - await editInput(target, ".o_selected_row .o_field_widget input", "some value"); - await clickSave(target); - - // edit an existing record - await click(target.querySelector(".o_data_cell")); - await editInput(target, ".o_selected_row .o_field_widget input", "new value"); - await click(target.querySelector(".o_list_button_save")); - - // delete a record - await click(target.querySelector(".o_data_row .o_list_record_selector input")); - await toggleActionMenu(target); - await toggleMenuItem(target, "Delete"); - await click(target.querySelector(".modal-footer .btn-primary")); - - assert.verifySteps([ - "name_create", - "clear_cache", - "write", - "clear_cache", - "unlink", - "clear_cache", - ]); - }); - QUnit.test( "list view move to previous page when all records from last page deleted", async function (assert) { diff --git a/addons/web/static/tests/views/view_service_tests.js b/addons/web/static/tests/views/view_service_tests.js index d9a9a4182e5..ca3c91c9f91 100644 --- a/addons/web/static/tests/views/view_service_tests.js +++ b/addons/web/static/tests/views/view_service_tests.js @@ -18,6 +18,9 @@ QUnit.module("View service", (hooks) => { fields: {}, records: [], }, + "ir.ui.view": { + fields: {}, records: [] + }, }; const fakeUiService = { @@ -109,6 +112,37 @@ QUnit.module("View service", (hooks) => { assert.verifySteps(["get_views", "get_views"]); }); + QUnit.test("clear cache when updating ir.ui.view", async (assert) => { + const mockRPC = (route, args) => { + if (route.includes("get_views")) { + assert.step("get_views"); + } + }; + const loadView = () => + env.services.views.loadViews( + { + resModel: "take.five", + views: [[99, "list"]], + context: { default_field_value: 1 }, + }, + {} + ); + + await makeMockServer(serverData, mockRPC); + const env = await makeTestEnv(); + + await loadView(); + assert.verifySteps(["get_views"]); + await loadView(); + assert.verifySteps([]); // cache works => no actual rpc + await env.services.orm.unlink("ir.ui.view", [3]); + await loadView(); + assert.verifySteps(["get_views"]); // cache was invalidated + await env.services.orm.unlink("take.five", [3]); + await loadView(); + assert.verifySteps([]); // cache was not invalidated + }); + QUnit.test("loadViews stores fields in cache", async (assert) => { assert.expect(2); diff --git a/addons/web/static/tests/webclient/actions/window_action_tests.js b/addons/web/static/tests/webclient/actions/window_action_tests.js index 7a58dd3df68..71d05edee1a 100644 --- a/addons/web/static/tests/webclient/actions/window_action_tests.js +++ b/addons/web/static/tests/webclient/actions/window_action_tests.js @@ -2427,6 +2427,7 @@ QUnit.module("ActionManager", (hooks) => { const mockRPC = async (route, args) => { assert.step(args.method || route); }; + serverData.models["ir.actions.act_window"] = { fields: {}, records: [] }; const webClient = await createWebClient({ serverData, mockRPC }); assert.verifySteps(["/web/webclient/load_menus"]); @@ -2438,6 +2439,12 @@ QUnit.module("ActionManager", (hooks) => { assert.containsOnce(target, ".o_kanban_view"); assert.verifySteps(["web_search_read"]); + + await webClient.env.services.orm.unlink("ir.actions.act_window", [333]); + assert.verifySteps(["unlink"]); + await doAction(webClient, 1); + // cache was cleared => reload the action + assert.verifySteps(["/web/action/load", "web_search_read"]); }); QUnit.test("pushState also changes the title of the tab", async (assert) => { diff --git a/addons/web/static/tests/webclient/company_service_tests.js b/addons/web/static/tests/webclient/company_service_tests.js new file mode 100644 index 00000000000..fdb23b0d65c --- /dev/null +++ b/addons/web/static/tests/webclient/company_service_tests.js @@ -0,0 +1,32 @@ +/** @odoo-module **/ + +import { ormService } from "@web/core/orm_service"; +import { registry } from "@web/core/registry"; +import { companyService } from "@web/webclient/company_service"; +import { makeTestEnv } from "../helpers/mock_env"; + +const serviceRegistry = registry.category("services"); + +QUnit.module("company service"); + +QUnit.test("reload webclient when updating a res.company", async (assert) => { + serviceRegistry.add("company", companyService); + serviceRegistry.add("orm", ormService); + serviceRegistry.add("action", { + start(env) { + return { + doAction(action) { + assert.step(action); + }, + }; + }, + }); + const env = await makeTestEnv(); + assert.verifySteps([]); + await env.services.orm.read("res.company", [32]); + assert.verifySteps([]); + await env.services.orm.unlink("res.company", [32]); + assert.verifySteps(["reload_context"]); + await env.services.orm.unlink("notacompany", [32]); + assert.verifySteps([]); +}); diff --git a/addons/web/static/tests/webclient/currency_service_tests.js b/addons/web/static/tests/webclient/currency_service_tests.js new file mode 100644 index 00000000000..0b681ab2fc4 --- /dev/null +++ b/addons/web/static/tests/webclient/currency_service_tests.js @@ -0,0 +1,31 @@ +/** @odoo-module **/ + +import { ormService } from "@web/core/orm_service"; +import { registry } from "@web/core/registry"; +import { currencyService } from "@web/webclient/currency_service"; +import { makeTestEnv } from "../helpers/mock_env"; +import { makeFakeRPCService } from "../helpers/mock_services"; + +const serviceRegistry = registry.category("services"); + +QUnit.module("currency service"); + +QUnit.test("reload currencies when updating a res.currency", async (assert) => { + serviceRegistry.add("currency", currencyService); + serviceRegistry.add("orm", ormService); + const fakeRpc = makeFakeRPCService((route) => { + assert.step(route); + }); + serviceRegistry.add("rpc", fakeRpc); + const env = await makeTestEnv(); + assert.verifySteps([]); + await env.services.orm.read("res.currency", [32]); + assert.verifySteps(["/web/dataset/call_kw/res.currency/read"]); + await env.services.orm.unlink("res.currency", [32]); + assert.verifySteps([ + "/web/dataset/call_kw/res.currency/unlink", + "/web/session/get_session_info", + ]); + await env.services.orm.unlink("notcurrency", [32]); + assert.verifySteps(["/web/dataset/call_kw/notcurrency/unlink"]); +}); diff --git a/addons/web/static/tests/webclient/loading_indicator_tests.js b/addons/web/static/tests/webclient/loading_indicator_tests.js index 195ced9b5a6..1dba387afc5 100644 --- a/addons/web/static/tests/webclient/loading_indicator_tests.js +++ b/addons/web/static/tests/webclient/loading_indicator_tests.js @@ -31,12 +31,14 @@ QUnit.module("LoadingIndicator", { }, }); +const payload = (id) => ({ data: { id }, settings: {} }); + QUnit.test("displays the loading indicator in non debug mode", async (assert) => { const env = await makeTestEnv(); await mount(LoadingIndicator, target, { env }); let loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed"); - env.bus.trigger("RPC:REQUEST", 1); + env.bus.trigger("RPC:REQUEST", payload(1)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.notStrictEqual(loadingIndicator, null, "the loading indicator should be displayed"); @@ -45,7 +47,7 @@ QUnit.test("displays the loading indicator in non debug mode", async (assert) => "Loading", "the loading indicator should display 'Loading'" ); - env.bus.trigger("RPC:RESPONSE", 1); + env.bus.trigger("RPC:RESPONSE", payload(1)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed"); @@ -57,7 +59,7 @@ QUnit.test("displays the loading indicator for one rpc in debug mode", async (as await mount(LoadingIndicator, target, { env }); let loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed"); - env.bus.trigger("RPC:REQUEST", 1); + env.bus.trigger("RPC:REQUEST", payload(1)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.notStrictEqual(loadingIndicator, null, "the loading indicator should be displayed"); @@ -66,7 +68,7 @@ QUnit.test("displays the loading indicator for one rpc in debug mode", async (as "Loading (1)", "the loading indicator should indicate 1 request in progress" ); - env.bus.trigger("RPC:RESPONSE", 1); + env.bus.trigger("RPC:RESPONSE", payload(1)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed"); @@ -78,8 +80,8 @@ QUnit.test("displays the loading indicator for multi rpc in debug mode", async ( await mount(LoadingIndicator, target, { env }); let loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed"); - env.bus.trigger("RPC:REQUEST", 1); - env.bus.trigger("RPC:REQUEST", 2); + env.bus.trigger("RPC:REQUEST", payload(1)); + env.bus.trigger("RPC:REQUEST", payload(2)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.notStrictEqual(loadingIndicator, null, "the loading indicator should be displayed"); @@ -88,7 +90,7 @@ QUnit.test("displays the loading indicator for multi rpc in debug mode", async ( "Loading (2)", "the loading indicator should indicate 2 requests in progress." ); - env.bus.trigger("RPC:REQUEST", 3); + env.bus.trigger("RPC:REQUEST", payload(3)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual( @@ -96,7 +98,7 @@ QUnit.test("displays the loading indicator for multi rpc in debug mode", async ( "Loading (3)", "the loading indicator should indicate 3 requests in progress." ); - env.bus.trigger("RPC:RESPONSE", 1); + env.bus.trigger("RPC:RESPONSE", payload(1)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual( @@ -104,7 +106,7 @@ QUnit.test("displays the loading indicator for multi rpc in debug mode", async ( "Loading (2)", "the loading indicator should indicate 2 requests in progress." ); - env.bus.trigger("RPC:REQUEST", 4); + env.bus.trigger("RPC:REQUEST", payload(4)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual( @@ -112,8 +114,8 @@ QUnit.test("displays the loading indicator for multi rpc in debug mode", async ( "Loading (3)", "the loading indicator should indicate 3 requests in progress." ); - env.bus.trigger("RPC:RESPONSE", 2); - env.bus.trigger("RPC:RESPONSE", 3); + env.bus.trigger("RPC:RESPONSE", payload(2)); + env.bus.trigger("RPC:RESPONSE", payload(3)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual( @@ -121,7 +123,7 @@ QUnit.test("displays the loading indicator for multi rpc in debug mode", async ( "Loading (1)", "the loading indicator should indicate 1 request in progress." ); - env.bus.trigger("RPC:RESPONSE", 4); + env.bus.trigger("RPC:RESPONSE", payload(4)); await nextTick(); loadingIndicator = target.querySelector(".o_loading_indicator"); assert.strictEqual(loadingIndicator, null, "the loading indicator should not be displayed"); @@ -144,9 +146,9 @@ QUnit.test("loading indicator blocks UI", async (assert) => { assert.step("unblock"); }); await mount(LoadingIndicator, target, { env }); - env.bus.trigger("RPC:REQUEST", 1); + env.bus.trigger("RPC:REQUEST", payload(1)); await nextTick(); - env.bus.trigger("RPC:RESPONSE", 1); + env.bus.trigger("RPC:RESPONSE", payload(1)); await nextTick(); assert.verifySteps(["set timeout 250", "set timeout 3000", "block", "unblock"]); unpatch(originalBrowser, "mock.settimeout"); @@ -163,12 +165,12 @@ QUnit.test("loading indicator doesn't unblock ui if it didn't block it", async ( assert.step("unblock"); }); await mount(LoadingIndicator, target, { env }); - env.bus.trigger("RPC:REQUEST", 1); + env.bus.trigger("RPC:REQUEST", payload(1)); execRegisteredTimeouts(); - env.bus.trigger("RPC:RESPONSE", 1); + env.bus.trigger("RPC:RESPONSE", payload(1)); assert.verifySteps(["block", "unblock"]); - env.bus.trigger("RPC:REQUEST", 2); - env.bus.trigger("RPC:RESPONSE", 2); + env.bus.trigger("RPC:REQUEST", payload(2)); + env.bus.trigger("RPC:RESPONSE", payload(2)); execRegisteredTimeouts(); assert.verifySteps([]); }); @@ -185,14 +187,14 @@ QUnit.test("loading indicator is not displayed immediately", async (assert) => { assert.step("unblock"); }); await mount(LoadingIndicator, target, { env }); - env.bus.trigger("RPC:REQUEST", 1); + env.bus.trigger("RPC:REQUEST", payload(1)); await nextTick(); assert.containsNone(target, ".o_loading_indicator"); await advanceTime(400); await nextTick(); assert.containsOnce(target, ".o_loading_indicator"); - env.bus.trigger("RPC:RESPONSE", 1); + env.bus.trigger("RPC:RESPONSE", payload(1)); await nextTick(); assert.containsNone(target, ".o_loading_indicator"); });