Before this commit, performing a search in a m2o can block your ui
despite the result already being displayed.
Why?
When you perform your search in a m2o, a set of RPC queries are made
with the different search values (name_search). If the last search value
(the one that will be used by the m2o in its autocomplete) has already
been resolved, but one of the other RPCs has still not been resolved,
then the screen will remain blocked until all the rpc's have been resolved.
Solution:
Since we are only interested in the last rpc, we will cancel all the
others. So with each new rpc, we will cancel the previous one.
How to reproduce:
- Go to a form view with a m2o field
- Edit this field +- slowly (several rpc will be done with different values)
- Receive the result of the search. (The autocomplete is displayed with
the right values)
Before this commit:
If one of the other rpc's is not yet resolved, the ui will block
until all rpc's are resolved.
After this commit:
The ui will not block.
closes odoo/odoo#105269
X-original-commit: 9917d841fa38ccc1e6d67875a665494dc22ef92f
Signed-off-by: Samuel Degueldre <sad@odoo.com>
334 lines
9.2 KiB
JavaScript
334 lines
9.2 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
import { browser } from "@web/core/browser/browser";
|
|
import { routerService } from "@web/core/browser/router_service";
|
|
import { localization } from "@web/core/l10n/localization";
|
|
import { _t } from "@web/core/l10n/translation";
|
|
import { rpcService } from "@web/core/network/rpc_service";
|
|
import { userService } from "@web/core/user_service";
|
|
import { effectService } from "@web/core/effects/effect_service";
|
|
import { objectToUrlEncodedString } from "@web/core/utils/urls";
|
|
import { registerCleanup } from "./cleanup";
|
|
import { patchWithCleanup } from "./utils";
|
|
import { uiService } from "@web/core/ui/ui_service";
|
|
import { ConnectionAbortedError } from "../../src/core/network/rpc_service";
|
|
|
|
import { Component, status } from "@odoo/owl";
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Mock Services
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export const defaultLocalization = {
|
|
dateFormat: "MM/dd/yyyy",
|
|
timeFormat: "HH:mm:ss",
|
|
dateTimeFormat: "MM/dd/yyyy HH:mm:ss",
|
|
decimalPoint: ".",
|
|
direction: "ltr",
|
|
grouping: [],
|
|
multiLang: false,
|
|
thousandsSep: ",",
|
|
weekStart: 7,
|
|
};
|
|
|
|
/**
|
|
* @param {Partial<typeof defaultLocalization>} [config]
|
|
*/
|
|
export function makeFakeLocalizationService(config = {}) {
|
|
patchWithCleanup(localization, Object.assign({}, defaultLocalization, config));
|
|
|
|
return {
|
|
name: "localization",
|
|
start: async (env) => {
|
|
env._t = _t;
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildMockRPC(mockRPC) {
|
|
return async function (...args) {
|
|
if (this instanceof Component && status(this) === "destroyed") {
|
|
return new Promise(() => {});
|
|
}
|
|
if (mockRPC) {
|
|
return mockRPC(...args);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function makeFakeRPCService(mockRPC) {
|
|
return {
|
|
name: "rpc",
|
|
start() {
|
|
const rpcService = buildMockRPC(mockRPC);
|
|
return function () {
|
|
let rejectFn;
|
|
const rpcProm = new Promise((resolve, reject) => {
|
|
rejectFn = reject;
|
|
rpcService(...arguments)
|
|
.then(resolve)
|
|
.catch(reject);
|
|
});
|
|
rpcProm.abort = (rejectError = true) => {
|
|
if (rejectError) {
|
|
rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
|
|
}
|
|
};
|
|
return rpcProm;
|
|
};
|
|
},
|
|
specializeForComponent: rpcService.specializeForComponent,
|
|
};
|
|
}
|
|
|
|
export function makeMockXHR(response, sendCb, def) {
|
|
const MockXHR = function () {
|
|
return {
|
|
_loadListener: null,
|
|
url: "",
|
|
addEventListener(type, listener) {
|
|
if (type === "load") {
|
|
this._loadListener = listener;
|
|
} else if (type === "error") {
|
|
this._errorListener = listener;
|
|
}
|
|
},
|
|
set onload(listener) {
|
|
this._loadListener = listener;
|
|
},
|
|
set onerror(listener) {
|
|
this._errorListener = listener;
|
|
},
|
|
open(method, url) {
|
|
this.url = url;
|
|
},
|
|
getResponseHeader() {},
|
|
setRequestHeader() {},
|
|
async send(data) {
|
|
let listener = this._loadListener;
|
|
if (sendCb) {
|
|
if (typeof data === "string") {
|
|
try {
|
|
data = JSON.parse(data);
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
try {
|
|
await sendCb.call(this, data);
|
|
} catch {
|
|
listener = this._errorListener;
|
|
}
|
|
}
|
|
if (def) {
|
|
await def;
|
|
}
|
|
listener.call(this);
|
|
},
|
|
response: JSON.stringify(response || ""),
|
|
};
|
|
};
|
|
return MockXHR;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Low level API mocking
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export function makeMockFetch(mockRPC) {
|
|
const _rpc = buildMockRPC(mockRPC);
|
|
return async (input, params) => {
|
|
let route = typeof input === "string" ? input : input.url;
|
|
if (route.includes("load_menus")) {
|
|
const routeArray = route.split("/");
|
|
params = {
|
|
hash: routeArray.pop(),
|
|
};
|
|
route = routeArray.join("/");
|
|
}
|
|
let res;
|
|
let status;
|
|
try {
|
|
res = await _rpc(route, params);
|
|
status = 200;
|
|
} catch {
|
|
status = 500;
|
|
}
|
|
const blob = new Blob([JSON.stringify(res || {})], { type: "application/json" });
|
|
return new Response(blob, { status });
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {Object} [params={}]
|
|
* @param {Object} [params.onRedirect] hook on the "redirect" method
|
|
* @returns {typeof routerService}
|
|
*/
|
|
export function makeFakeRouterService(params = {}) {
|
|
return {
|
|
start({ bus }) {
|
|
const router = routerService.start(...arguments);
|
|
bus.addEventListener("test:hashchange", (ev) => {
|
|
const hash = ev.detail;
|
|
browser.location.hash = objectToUrlEncodedString(hash);
|
|
});
|
|
registerCleanup(router.cancelPushes);
|
|
patchWithCleanup(router, {
|
|
async redirect() {
|
|
await this._super(...arguments);
|
|
if (params.onRedirect) {
|
|
params.onRedirect(...arguments);
|
|
}
|
|
},
|
|
});
|
|
return router;
|
|
},
|
|
};
|
|
}
|
|
|
|
export const fakeCommandService = {
|
|
start() {
|
|
return {
|
|
add() {
|
|
return () => {};
|
|
},
|
|
getCommands() {
|
|
return [];
|
|
},
|
|
openPalette() {},
|
|
};
|
|
},
|
|
};
|
|
|
|
export const fakeCookieService = {
|
|
start() {
|
|
const cookie = {};
|
|
return {
|
|
get current() {
|
|
return cookie;
|
|
},
|
|
setCookie(key, value) {
|
|
if (value !== undefined) {
|
|
cookie[key] = value;
|
|
}
|
|
},
|
|
deleteCookie(key) {
|
|
delete cookie[key];
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
export const fakeTitleService = {
|
|
start() {
|
|
let current = {};
|
|
return {
|
|
get current() {
|
|
return JSON.stringify(current);
|
|
},
|
|
getParts() {
|
|
return current;
|
|
},
|
|
setParts(parts) {
|
|
current = Object.assign({}, current, parts);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
export const fakeColorSchemeService = {
|
|
start() {
|
|
return {
|
|
switchToColorScheme() {},
|
|
};
|
|
},
|
|
};
|
|
|
|
export function makeFakeNotificationService(mock) {
|
|
return {
|
|
start() {
|
|
function add() {
|
|
if (mock) {
|
|
return mock(...arguments);
|
|
}
|
|
}
|
|
return {
|
|
add,
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
export function makeFakeDialogService(addDialog) {
|
|
return {
|
|
start() {
|
|
return {
|
|
add: addDialog || (() => () => {}),
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
export function makeFakeUserService(hasGroup = () => false) {
|
|
return {
|
|
...userService,
|
|
start() {
|
|
const fakeUserService = userService.start(...arguments);
|
|
fakeUserService.hasGroup = hasGroup;
|
|
return fakeUserService;
|
|
},
|
|
};
|
|
}
|
|
|
|
export const fakeCompanyService = {
|
|
start() {
|
|
return {
|
|
availableCompanies: {},
|
|
allowedCompanyIds: [],
|
|
currentCompany: {},
|
|
setCompanies: () => {},
|
|
};
|
|
},
|
|
};
|
|
|
|
export function makeFakeHTTPService(getResponse, postResponse) {
|
|
getResponse =
|
|
getResponse ||
|
|
((route, readMethod) => {
|
|
return readMethod === "json" ? {} : "";
|
|
});
|
|
postResponse =
|
|
postResponse ||
|
|
((route, params, readMethod) => {
|
|
return readMethod === "json" ? {} : "";
|
|
});
|
|
return {
|
|
start() {
|
|
return {
|
|
async get(...args) {
|
|
return getResponse(...args);
|
|
},
|
|
async post(...args) {
|
|
return postResponse(...args);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
export const mocks = {
|
|
color_scheme: () => fakeColorSchemeService,
|
|
company: () => fakeCompanyService,
|
|
command: () => fakeCommandService,
|
|
cookie: () => fakeCookieService,
|
|
effect: () => effectService, // BOI The real service ? Is this what we want ?
|
|
localization: makeFakeLocalizationService,
|
|
notification: makeFakeNotificationService,
|
|
router: makeFakeRouterService,
|
|
rpc: makeFakeRPCService,
|
|
title: () => fakeTitleService,
|
|
ui: () => uiService,
|
|
user: () => userService,
|
|
dialog: makeFakeDialogService,
|
|
};
|