From 78e44a23c95649ab4f9e2aa7fb626fc5174d2a24 Mon Sep 17 00:00:00 2001 From: Julien Mougenot Date: Tue, 22 Dec 2020 16:59:46 +0000 Subject: [PATCH] [FIX] web,mail: Call on_attach_callback when appending content Before this commit, the form renderer would call "on_attach_callback" only when the whole _render process was over. The problem was that this process is asynchronous and this callback should be invoked as soon as the widgets are appended. As such, any asynchronous operation is performed before appending the content in a new function "__renderView". Task 2346540 closes odoo/odoo#65991 X-original-commit: 953fb092636b9919aaa6c6a6cfaffd003ba951d5 Signed-off-by: Aaron Bohy (aab) --- .../widgets/form_renderer/form_renderer.js | 2 +- .../src/js/views/basic/basic_renderer.js | 4 +- .../static/src/js/views/form/form_renderer.js | 10 ++- addons/web/static/tests/views/form_tests.js | 74 +++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer.js b/addons/mail/static/src/widgets/form_renderer/form_renderer.js index e4c3161c4da..cf147656d63 100644 --- a/addons/mail/static/src/widgets/form_renderer/form_renderer.js +++ b/addons/mail/static/src/widgets/form_renderer/form_renderer.js @@ -145,7 +145,7 @@ FormRenderer.include({ * * @override */ - async _renderView() { + async __renderView() { await this._super(...arguments); if (this._hasChatter()) { if (!this._chatterContainerComponent) { diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js index 260de94c7a9..bfc4ee4a0ea 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -684,7 +684,7 @@ var BasicRenderer = AbstractRenderer.extend(WidgetAdapterMixin, { if (this._isInDom) { for (const handle in this.allFieldWidgets) { this.allFieldWidgets[handle].forEach(widget => { - if (widget.on_attach_callback) { + if (!utils.isComponent(widget.constructor) && widget.on_attach_callback) { widget.on_attach_callback(); } }); @@ -694,6 +694,8 @@ var BasicRenderer = AbstractRenderer.extend(WidgetAdapterMixin, { widget.on_attach_callback(); } }); + // call on_attach_callback on child components (including field components) + WidgetAdapterMixin.on_attach_callback.call(this); } }, /** diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js index b0da8c5aaf5..e1e9c5200a9 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -1062,7 +1062,7 @@ var FormRenderer = BasicRenderer.extend({ var $form = this._renderNode(this.arch).addClass(this.className); delete this.defs; - return Promise.all(defs).then(function () { + return Promise.all(defs).then(() => this.__renderView()).then(function () { self._updateView($form.contents()); if (self.state.res_id in self.alertFields) { self.displayTranslationAlert(); @@ -1075,6 +1075,14 @@ var FormRenderer = BasicRenderer.extend({ $form.remove(); }); }, + /** + * Meant to be overridden if asynchronous work needs to be done when + * rendering the view. This is called right before attaching the new view + * content. + * @private + * @returns {Promise} + */ + async __renderView() {}, /** * This method is overridden to activate the first notebook page if the * current active page is invisible due to modifiers. This is done after diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index cd00a3588d4..0e55d3223bb 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -1,12 +1,14 @@ odoo.define('web.form_tests', function (require) { "use strict"; +const AbstractField = require("web.AbstractField"); var AbstractStorageService = require('web.AbstractStorageService'); var BasicModel = require('web.BasicModel'); var concurrency = require('web.concurrency'); var core = require('web.core'); var fieldRegistry = require('web.field_registry'); const fieldRegistryOwl = require('web.field_registry_owl'); +const FormRenderer = require('web.FormRenderer'); var FormView = require('web.FormView'); var mixins = require('web.mixins'); var NotificationService = require('web.NotificationService'); @@ -10851,6 +10853,78 @@ QUnit.module('Views', { form.destroy(); }); + + QUnit.test("attach callbacks with long processing in __renderView", async function (assert) { + /** + * The main use case of this test is discuss, in which the FormRenderer + * __renderView method is overridden to perform asynchronous tasks (the + * update of the chatter Component) resulting in a delay between the + * appending of the new form content into its element and the + * "on_attach_callback" calls. This is the purpose of "__renderView" + * which is meant to do all the async work before the content is appended. + */ + assert.expect(11); + + let testPromise = Promise.resolve(); + + const Renderer = FormRenderer.extend({ + on_attach_callback() { + assert.step("form.on_attach_callback"); + this._super(...arguments); + }, + async __renderView() { + const _super = this._super.bind(this); + await testPromise; + return _super(); + }, + }); + + // Setup custom field widget + fieldRegistry.add("customwidget", AbstractField.extend({ + className: "custom-widget", + on_attach_callback() { + assert.step("widget.on_attach_callback"); + }, + })); + + const form = await createView({ + arch: `
`, + data: this.data, + model: 'partner', + res_id: 1, + View: FormView.extend({ + config: Object.assign({}, FormView.prototype.config, { Renderer }), + }), + }); + + assert.containsOnce(form, ".custom-widget"); + assert.verifySteps([ + "form.on_attach_callback", // Form attached + "widget.on_attach_callback", // Initial widget attached + ]); + + const initialWidget = form.$(".custom-widget")[0]; + testPromise = testUtils.makeTestPromise(); + + await testUtils.form.clickEdit(form); + + assert.containsOnce(form, ".custom-widget"); + assert.strictEqual(initialWidget, form.$(".custom-widget")[0], "Widgets have yet to be replaced"); + assert.verifySteps([]); + + testPromise.resolve(); + await testUtils.nextTick(); + + assert.containsOnce(form, ".custom-widget"); + assert.notStrictEqual(initialWidget, form.$(".custom-widget")[0], "Widgets have been replaced"); + assert.verifySteps([ + "widget.on_attach_callback", // New widget attached + ]); + + form.destroy(); + + delete fieldRegistry.map.customwidget; + }); }); });