[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) <aab@odoo.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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<string>} 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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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: "€" },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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: `<form><sheet><field name="display_name"/></sheet></form>`,
|
||||
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: `<form><sheet><field name="display_name"/></sheet></form>`,
|
||||
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: `<form><sheet><field name="display_name"/></sheet></form>`,
|
||||
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: '<form><field name="display_name"/></form>',
|
||||
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 = [
|
||||
|
||||
@@ -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: `<tree editable="top"><field name="display_name"/></tree>`,
|
||||
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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
@@ -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"]);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user