diff --git a/addons/web/static/src/views/kanban/kanban_model.js b/addons/web/static/src/views/kanban/kanban_model.js index e578d023a7f..21f146a711d 100644 --- a/addons/web/static/src/views/kanban/kanban_model.js +++ b/addons/web/static/src/views/kanban/kanban_model.js @@ -21,27 +21,33 @@ const { EventBus, markRaw } = owl; const FALSE = Symbol("false"); +class TransactionInProgress extends Error {} + +class NoTransactionInProgress extends Error {} + function makeTransactionManager() { const bus = new EventBus(); const transactions = {}; return { start: (id) => { if (transactions[id]) { - throw new Error(`Transaction in progress: commit or abort to start a new one.`); + throw new TransactionInProgress( + `Transaction in progress: commit or abort to start a new one.` + ); } transactions[id] = true; bus.trigger("START"); }, commit: (id) => { if (!transactions[id]) { - throw new Error(`No transaction in progress.`); + throw new NoTransactionInProgress(`No transaction in progress.`); } delete transactions[id]; bus.trigger("COMMIT"); }, abort: (id) => { if (!transactions[id]) { - throw new Error(`No transaction in progress.`); + throw new NoTransactionInProgress(`No transaction in progress.`); } delete transactions[id]; bus.trigger("ABORT"); @@ -406,7 +412,6 @@ export class KanbanDynamicGroupList extends DynamicGroupList { * @param {string} dataGroupId * @param {string} refId * @param {string} targetGroupId - * @returns {Promise} */ async moveRecord(dataRecordId, dataGroupId, refId, targetGroupId) { const sourceGroup = this.groups.find((g) => g.id === dataGroupId); @@ -418,7 +423,14 @@ export class KanbanDynamicGroupList extends DynamicGroupList { const record = sourceGroup.list.records.find((r) => r.id === dataRecordId); - this.model.transaction.start(dataRecordId); + try { + this.model.transaction.start(dataRecordId); + } catch (err) { + if (err instanceof TransactionInProgress) { + return; + } + throw err; + } // Move from one group to another try { @@ -445,8 +457,6 @@ export class KanbanDynamicGroupList extends DynamicGroupList { this.model.notify(); throw err; } - - return record; } // ------------------------------------------------------------------------ diff --git a/addons/web/static/tests/views/kanban_view_tests.js b/addons/web/static/tests/views/kanban_view_tests.js index ff3d707cdd2..da21cfcc166 100644 --- a/addons/web/static/tests/views/kanban_view_tests.js +++ b/addons/web/static/tests/views/kanban_view_tests.js @@ -10331,4 +10331,180 @@ QUnit.module("Views", (hooks) => { assert.hasClass(document.activeElement, "o_searchview_input"); } ); + + QUnit.test("no leak of TransactionInProgress (grouped case)", async (assert) => { + let def; + await makeView({ + type: "kanban", + resModel: "partner", + serverData, + arch: ` + + + + +
+ +
+
+
+
+ `, + groupBy: ["state"], + async mockRPC(route) { + if (route === "/web/dataset/resequence") { + assert.step("resequence"); + await Promise.resolve(def); + } + }, + }); + + def = makeDeferred(); + + assert.containsOnce(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); + assert.strictEqual( + target.querySelector(".o_kanban_group:nth-child(1) .o_kanban_record").innerText, + "yop" + ); + assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); + assert.strictEqual( + target.querySelector(".o_kanban_group:nth-child(2) .o_kanban_record").innerText, + "blip" + ); + assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); + + assert.verifySteps([]); + + // move "yop" from first to second column + await dragAndDrop( + ".o_kanban_group:nth-child(1) .o_kanban_record", + ".o_kanban_group:nth-child(2)" + ); + + assert.containsNone(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); + assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); + assert.deepEqual( + [...target.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map( + (el) => el.innerText + ), + ["blip", "yop"] + ); + assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); + + assert.verifySteps(["resequence"]); + + // try to move "yop" from second to third column + await dragAndDrop( + ".o_kanban_group:nth-child(2) .o_kanban_record:nth-child(3)", // move yop + ".o_kanban_group:nth-child(3)" + ); + + assert.containsNone(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); + assert.containsN(target, ".o_kanban_group:nth-child(2) .o_kanban_record", 2); + assert.deepEqual( + [...target.querySelectorAll(".o_kanban_group:nth-child(2) .o_kanban_record")].map( + (el) => el.innerText + ), + ["blip", "yop"] + ); + assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 2); + + assert.verifySteps([]); + + def.resolve(); + await nextTick(); + + // try again to move "yop" from second to third column + await dragAndDrop( + ".o_kanban_group:nth-child(2) .o_kanban_record:nth-child(3)", // move yop + ".o_kanban_group:nth-child(3)" + ); + + assert.containsNone(target, ".o_kanban_group:nth-child(1) .o_kanban_record"); + assert.containsOnce(target, ".o_kanban_group:nth-child(2) .o_kanban_record"); + assert.containsN(target, ".o_kanban_group:nth-child(3) .o_kanban_record", 3); + assert.deepEqual( + [...target.querySelectorAll(".o_kanban_group:nth-child(3) .o_kanban_record")].map( + (el) => el.innerText + ), + ["gnap", "blip", "yop"] + ); + + assert.verifySteps(["resequence"]); + }); + + QUnit.test("no leak of TransactionInProgress (not grouped case)", async (assert) => { + let def; + await makeView({ + type: "kanban", + resModel: "partner", + serverData, + arch: ` + + + + +
+ +
+
+
+
+ `, + async mockRPC(route) { + if (route === "/web/dataset/resequence") { + assert.step("resequence"); + await Promise.resolve(def); + } + }, + }); + + def = makeDeferred(); + + assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 4); + assert.deepEqual( + [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( + (el) => el.innerText + ), + ["yop", "blip", "gnap", "blip"] + ); + + assert.verifySteps([]); + + // move "yop" to second place + await dragAndDrop(".o_kanban_record", ".o_kanban_record:nth-child(2)"); + + assert.deepEqual( + [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( + (el) => el.innerText + ), + ["yop", "blip", "gnap", "blip"] + ); + assert.verifySteps(["resequence"]); + + // try again + await dragAndDrop(".o_kanban_record", ".o_kanban_record:nth-child(2)"); + + assert.verifySteps([]); + + def.resolve(); + await nextTick(); + + assert.deepEqual( + [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( + (el) => el.innerText + ), + ["blip", "yop", "gnap", "blip"] + ); + + await dragAndDrop(".o_kanban_record:nth-child(2)", ".o_kanban_record:nth-child(3)"); + + assert.deepEqual( + [...target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")].map( + (el) => el.innerText + ), + ["blip", "gnap", "yop", "blip"] + ); + assert.verifySteps(["resequence"]); + }); });