From 86824d8e0ced85ffe66f1f3efcbb767d7c7f2220 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Mon, 7 Nov 2022 08:51:25 +0000 Subject: [PATCH] [FIX] web: search in a m2o block the ui 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 --- .../web/static/src/core/network/rpc_service.js | 13 +++++++++++-- addons/web/static/src/core/utils/hooks.js | 16 +++++++++++----- .../static/src/views/fields/relational_utils.js | 6 +++++- .../tests/core/network/rpc_service_tests.js | 7 +++++++ addons/web/static/tests/helpers/mock_services.js | 7 +++++-- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/addons/web/static/src/core/network/rpc_service.js b/addons/web/static/src/core/network/rpc_service.js index f898c75dc8d..e85c4f2ae70 100644 --- a/addons/web/static/src/core/network/rpc_service.js +++ b/addons/web/static/src/core/network/rpc_service.js @@ -99,11 +99,20 @@ export function jsonrpc(env, rpcId, url, params, settings = {}) { request.setRequestHeader("Content-Type", "application/json"); request.send(JSON.stringify(data)); }); - promise.abort = function () { + /** + * @param {Boolean} rejectError Returns an error if true. Allows you to cancel + * ignored rpc's in order to unblock the ui and not display an error. + */ + promise.abort = function (rejectError = true) { if (request.abort) { request.abort(); } - rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort")); + if (!settings.silent) { + bus.trigger("RPC:RESPONSE", data.id); + } + if (rejectError) { + rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort")); + } }; return promise; } diff --git a/addons/web/static/src/core/utils/hooks.js b/addons/web/static/src/core/utils/hooks.js index 078bc4caf62..548581aa056 100644 --- a/addons/web/static/src/core/utils/hooks.js +++ b/addons/web/static/src/core/utils/hooks.js @@ -164,14 +164,20 @@ export function useListener(eventName, querySelector, handler, options = {}) { // ----------------------------------------------------------------------------- // useService // ----------------------------------------------------------------------------- - function _protectMethod(component, fn) { - return async function (...args) { + return function (...args) { if (status(component) === "destroyed") { - throw new Error("Component is destroyed"); + return Promise.reject(new Error("Component is destroyed")); } - const result = await fn.call(this, ...args); - return status(component) === "destroyed" ? new Promise(() => {}) : result; + + const prom = Promise.resolve(fn.call(this, ...args)); + const protectedProm = prom.then((result) => + status(component) === "destroyed" ? new Promise(() => {}) : result + ); + return Object.assign(protectedProm, { + abort: prom.abort, + cancel: prom.cancel, + }); }; } diff --git a/addons/web/static/src/views/fields/relational_utils.js b/addons/web/static/src/views/fields/relational_utils.js index 8376039509b..fc89730b97e 100644 --- a/addons/web/static/src/views/fields/relational_utils.js +++ b/addons/web/static/src/views/fields/relational_utils.js @@ -213,13 +213,17 @@ export class Many2XAutocomplete extends Component { } async loadOptionsSource(request) { - const records = await this.orm.call(this.props.resModel, "name_search", [], { + if (this.lastProm) { + this.lastProm.abort(false); + } + this.lastProm = this.orm.call(this.props.resModel, "name_search", [], { name: request, operator: "ilike", args: this.props.getDomain(), limit: this.props.searchLimit + 1, context: this.props.context, }); + const records = await this.lastProm; const options = records.map((result) => ({ value: result[0], 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 364f35790aa..19152acfd15 100644 --- a/addons/web/static/tests/core/network/rpc_service_tests.js +++ b/addons/web/static/tests/core/network/rpc_service_tests.js @@ -247,10 +247,17 @@ QUnit.test("check connection aborted", async (assert) => { const MockXHR = makeMockXHR({}, () => {}, def); patchWithCleanup(browser, { XMLHttpRequest: MockXHR }, { pure: true }); const env = await makeTestEnv({ serviceRegistry }); + env.bus.addEventListener("RPC:REQUEST", () => { + assert.step("RPC:REQUEST"); + }); + env.bus.addEventListener("RPC:RESPONSE", () => { + assert.step("RPC:RESPONSE"); + }); const connection = env.services.rpc(); connection.abort(); assert.rejects(connection, ConnectionAbortedError); + assert.verifySteps(["RPC:REQUEST", "RPC:RESPONSE"]); }); QUnit.test( diff --git a/addons/web/static/tests/helpers/mock_services.js b/addons/web/static/tests/helpers/mock_services.js index ad5f6923161..e3a890f55e8 100644 --- a/addons/web/static/tests/helpers/mock_services.js +++ b/addons/web/static/tests/helpers/mock_services.js @@ -69,8 +69,11 @@ export function makeFakeRPCService(mockRPC) { .then(resolve) .catch(reject); }); - rpcProm.abort = () => - rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort")); + rpcProm.abort = (rejectError = true) => { + if (rejectError) { + rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort")); + } + }; return rpcProm; }; },