From 6ee17243a6cd6f2b22da805a8b9b44b3254f282d Mon Sep 17 00:00:00 2001
From: "Hubert Van de Walle (huvw)" <huvw@odoo.com>
Date: Wed, 28 Sep 2022 08:58:29 +0000
Subject: [PATCH] [FIX] web: list view: don't rerender a row before the
 previous render is done

Setup:

  - Add a checkbox to the sublist view of quotations with studio and save
  - In the chrome devtools, select a custom network throttling profile
    with 1s of latency

Steps to reproduce:

  First example:

  - Go to a quotation with at least two lines
  - Enter edit mode
  - Change the quantity of the first line
  - Double click on the price field of the second line
  -> A traceback appears: `Cannot set properties of null (setting 'props')`

  Second example:
  - Go to a quotation with at least two lines
  - Enter edit mode
  - Change the quantity of the first line
  - Click on the price field of the second line
  - Click on Save
  -> The same traceback is there

Cause of the issue:

  The checkbox field is implemented in owl and thus the
  owl_compatibility layer is used.
  Mounted is not called on the checkbox if a new rendering is being done
  cf: https://github.com/odoo/odoo/pull/75950
  Currently, it is possible to set a row mode before the previous call
  and rendering is done.

Solution:

  Use a mutex for each record to prevent simultaneous render

opw-2937444

closes odoo/odoo#101419

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
---
 .../js/views/list/list_editable_renderer.js   | 11 ++++-
 addons/web/static/tests/views/list_tests.js   | 48 +++++++++++++++++++
 2 files changed, 58 insertions(+), 1 deletion(-)

diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 8fdfa7551ad5..fe9496628560 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -16,6 +16,7 @@ var dom = require('web.dom');
 var ListRenderer = require('web.ListRenderer');
 var utils = require('web.utils');
 const { WidgetAdapterMixin } = require('web.OwlCompatibility');
+const concurrency = require('web.concurrency');
 
 var _t = core._t;
 
@@ -97,6 +98,8 @@ ListRenderer.include({
         this.currentFieldIndex = null;
         this.isResizing = false;
         this.eventListeners = [];
+
+        this.rowModeChangeMutex = new concurrency.Mutex();
     },
     /**
      * @override
@@ -404,7 +407,12 @@ ListRenderer.include({
      * @returns {Promise}
      */
     setRowMode: function (recordID, mode) {
-        var self = this;
+        // Use a mutex because we don't want to rerender a row before the previous render is finished
+        return this.rowModeChangeMutex.exec(this._setRowMode.bind(this, recordID, mode));
+    },
+
+    _setRowMode: async function (recordID, mode) {
+        const self = this;
         var record = self._getRecord(recordID);
         if (!record) {
             return Promise.resolve();
@@ -471,6 +479,7 @@ ListRenderer.include({
             core.bus.trigger('DOM_updated');
         });
     },
+
     /**
      * This method is called whenever we click/move outside of a row that was
      * in edit mode. This is the moment we save all accumulated changes on that
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index bc51720e2837..9f1376d3480a 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -11811,6 +11811,54 @@ QUnit.module('Views', {
         list.destroy();
     });
 
+    QUnit.test('quickly setting row mode with owl_compatibility', async function (assert) {
+        assert.expect(3);
+
+        this.data.bar.fields.bool = { string: 'bool', type: 'boolean' };
+        this.data.foo.records[0].o2m = [1, 2, 3];
+
+        const form = await createView({
+            View: FormView,
+            model: 'foo',
+            data: this.data,
+            res_id: 1,
+            viewOptions: { mode: 'edit' },
+            arch: `<form>
+                <sheet>
+                    <notebook>
+                        <page>
+                            <field name="o2m">
+                                <tree editable="bottom">
+                                    <field name="bool"/>
+                                    <field name="display_name"/>
+                                </tree>
+                            </field>
+                        </page>
+                    </notebook>
+                </sheet>
+            </form>`,
+        });
+
+        await testUtils.dom.click(form.$('tbody tr:eq(0) td:eq(1)'));
+        await testUtils.fields.editInput(form.$('input[name=display_name]'), 'another value');
+
+        // Double-click on a field of the second row
+        testUtils.dom.click(form.$('tbody tr:eq(1) td:eq(1)'));
+        testUtils.dom.click(form.$('tbody tr:eq(1) td:eq(1)'));
+        await testUtils.nextTick();
+        await testUtils.nextTick();
+
+        assert.strictEqual(
+            form.el.querySelector('tr:nth-child(1) > td.o_data_cell.o_field_cell.o_list_char').textContent,
+            'another value');
+
+        const rows = form.$('.o_data_row .o_list_char');
+        assert.containsNone(rows[0], 'input'); // this means that the first row is saved
+        assert.containsOnce(rows[1], 'input'); // this means that the second row is in edit mode
+
+        form.destroy();
+    });
+
 });
 
 });
-- 
GitLab