From 0bfc05c2a48f8cc69292c2aed796128814eb6bc9 Mon Sep 17 00:00:00 2001 From: "Hubert Van de Walle (huvw)" <huvw@odoo.com> Date: Fri, 15 Oct 2021 15:21:37 +0000 Subject: [PATCH] [FIX] web: basic_renderer: delay destroy after render when rerendering widget Steps to follow Edit the account.move view (with studio for example) Set the lines readonly property to [["partner_id","=",False]] Create a new move Add a partner Add a product Remove the partner -> A traceback appears: widget.$el is undefined Cause of the issue widget.$el is used after the widget has been destroyed This commit corrects it, by waiting for new widgets to be "mounted" before destroying old ones. The code already exists in 14.0 (3fd7b2009ec1f16c56c6d0ae87004011c90e204c). opw-2557142 closes odoo/odoo#78625 Signed-off-by: Lucas Perais (lpe) <lpe@odoo.com> --- .../src/js/views/basic/basic_renderer.js | 19 +++++-- addons/web/static/tests/views/form_tests.js | 54 +++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) 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 d051ba08edf0..f1b24d9c5b3d 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -749,13 +749,22 @@ var BasicRenderer = AbstractRenderer.extend({ _rerenderFieldWidget: function (widget, record, options) { // Render the new field widget var $el = this._renderFieldWidget(widget.__node, record, options); - widget.$el.replaceWith($el); - // Destroy the old widget and position the new one at the old one's - var oldIndex = this._destroyFieldWidget(record.id, widget); + // get the new widget that has just been pushed in allFieldWidgets var recordWidgets = this.allFieldWidgets[record.id]; - var newWidget = recordWidgets.pop(); - recordWidgets.splice(oldIndex, 0, newWidget); + var newWidget = recordWidgets[recordWidgets.length - 1]; + const def = this.defs[this.defs.length - 1]; // this is the widget's def, resolved when it is ready + const $div = $('<div>'); + $div.append($el); // $el will be replaced when widget is ready (see _renderFieldWidget) + def.then(() => { + widget.$el.replaceWith($div.children()); + + // Destroy the old widget and position the new one at the old one's + // (it has been temporarily inserted at the end of the list) + recordWidgets.splice(recordWidgets.indexOf(newWidget), 1); + var oldIndex = this._destroyFieldWidget(record.id, widget); + recordWidgets.splice(oldIndex, 0, newWidget); + }) }, /** * Unregisters an element of the modifiers data associated to the given diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index ea9fbb5b1002..d6a2f7b76f34 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -1036,6 +1036,60 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('readonly attrs on lines are re-evaluated on field change 2', async function (assert) { + assert.expect(4); + + this.data.partner.records[0].product_ids = [37]; + this.data.partner.records[0].trululu = false; + this.data.partner.onchanges = { + trululu(record) { + // when trululu changes, push another record in product_ids. + // only push a second record once. + if (record.product_ids.map(command => command[1]).includes(41)) { + return; + } + // copy the list to force it as different from the original + record.product_ids = record.product_ids.slice(); + record.product_ids.push([4,41,false]); + } + }; + + this.data.product.records[0].name = 'test'; + // This one is necessary to have a valid, rendered widget + this.data.product.fields.int_field = { type:"integer", string: "intField" }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: ` + <form> + <field name="trululu"/> + <field name="product_ids" attrs="{'readonly': [['trululu', '=', False]]}"> + <tree editable="top"><field name="int_field" widget="handle" /><field name="name"/></tree> + </field> + </form> + `, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + for (let value of [true, false, true, false]) { + if (value) { + await testUtils.fields.many2one.clickOpenDropdown('trululu') + await testUtils.fields.many2one.clickHighlightedItem('trululu') + assert.notOk($('.o_field_one2many[name="product_ids"]').hasClass("o_readonly_modifier"), 'lines should not be readonly') + } else { + await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="trululu"] input'), '', ['keyup']) + assert.ok($('.o_field_one2many[name="product_ids"]').hasClass("o_readonly_modifier"), 'lines should be readonly') + } + } + + form.destroy(); + }); + QUnit.test('empty fields have o_form_empty class in readonly mode', async function (assert) { assert.expect(8); -- GitLab