From 7fbe05cdd6e9d9ba67a7b3ff32fe1227ab939697 Mon Sep 17 00:00:00 2001 From: Mohammed Shekha Date: Fri, 19 Feb 2021 10:40:09 +0000 Subject: [PATCH] [FIX] web: prevent no content helper on delete/archive list records before this commit: when deleting or archiving or unarchive all records of last page in listview user is displayed with NoContentHelper while we still have other records in previous page, user should be moved to previous page if all records of last page are deleted. after this commit: if user deletes or archives or unarchives all records of last page in list view then user is moved to previous page. task-2446911 closes odoo/odoo#74091 X-original-commit: cead08fe6876239d1260b40948fcd74069496a2c Signed-off-by: Simon Genin (ges@odoo) --- .../src/legacy/js/views/basic/basic_model.js | 47 +++- .../static/tests/legacy/views/kanban_tests.js | 41 ++++ .../static/tests/legacy/views/list_tests.js | 210 +++++++++++++++++- 3 files changed, 289 insertions(+), 9 deletions(-) diff --git a/addons/web/static/src/legacy/js/views/basic/basic_model.js b/addons/web/static/src/legacy/js/views/basic/basic_model.js index 609df5652be..416c5055ce0 100644 --- a/addons/web/static/src/legacy/js/views/basic/basic_model.js +++ b/addons/web/static/src/legacy/js/views/basic/basic_model.js @@ -382,6 +382,11 @@ var BasicModel = AbstractModel.extend({ if (parent && parent.type === 'list') { parent.data = _.without(parent.data, record.id); delete self.localData[record.id]; + // Check if we are on last page and all records are deleted from current + // page i.e. if there is no state.data.length then go to previous page + if (!parent.data.length && parent.offset > 0) { + parent.offset = Math.max(parent.offset - parent.limit, 0); + } } else { record.res_ids.splice(record.offset, 1); record.offset = Math.min(record.offset, record.res_ids.length - 1); @@ -1330,14 +1335,27 @@ var BasicModel = AbstractModel.extend({ // optionally clear the DataManager's cache self._invalidateCache(parent); if (!_.isEmpty(action)) { - return self.do_action(action, { - on_close: function () { - return self.trigger_up('reload'); - } + return new Promise(function (resolve, reject) { + self.do_action(action, { + on_close: function (result) { + return self.trigger_up('reload', { + onSuccess: resolve, + }); + } + }); }); } else { return self.reload(parentID); } + }).then(function (datapoint) { + // if there are no records to display and we are not on first page(we check it + // by checking offset is greater than limit i.e. we are not on first page) + // reason for adding logic after reload to make sure there is no records after operation + if (parent && parent.type === 'list' && !parent.data.length && parent.offset > 0) { + parent.offset = Math.max(parent.offset - parent.limit, 0); + return self.reload(parentID); + } + return datapoint; }); }, /** @@ -1359,14 +1377,27 @@ var BasicModel = AbstractModel.extend({ // optionally clear the DataManager's cache self._invalidateCache(parent); if (!_.isEmpty(action)) { - return self.do_action(action, { - on_close: function () { - return self.trigger_up('reload'); - } + return new Promise(function (resolve, reject) { + self.do_action(action, { + on_close: function () { + return self.trigger_up('reload', { + onSuccess: resolve, + }); + } + }); }); } else { return self.reload(parentID); } + }).then(function (datapoint) { + // if there are no records to display and we are not on first page(we check it + // by checking offset is greater than limit i.e. we are not on first page) + // reason for adding logic after reload to make sure there is no records after operation + if (parent && parent.type === 'list' && !parent.data.length && parent.offset > 0) { + parent.offset = Math.max(parent.offset - parent.limit, 0); + return self.reload(parentID); + } + return datapoint; }); }, /** diff --git a/addons/web/static/tests/legacy/views/kanban_tests.js b/addons/web/static/tests/legacy/views/kanban_tests.js index 0bfbfc3599a..0101e1e1daa 100644 --- a/addons/web/static/tests/legacy/views/kanban_tests.js +++ b/addons/web/static/tests/legacy/views/kanban_tests.js @@ -444,6 +444,47 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('pager, ungrouped, deleting all records from last page should move to previous page', async function (assert) { + assert.expect(5); + + const kanban = await createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: + ` + + +
+ + +
+
+
+
`, + }); + + assert.strictEqual(cpHelpers.getPagerValue(kanban), "1-3", + "should have 3 records on current page"); + assert.strictEqual(cpHelpers.getPagerSize(kanban), "4", + "should have 4 records"); + + // move to next page + await cpHelpers.pagerNext(kanban); + assert.strictEqual(cpHelpers.getPagerValue(kanban), "4-4", + "should be on second page"); + + // delete a record + await testUtils.dom.click(kanban.$('.o_kanban_record:first a:first')); + await testUtils.dom.click($('.modal-footer button:first')); + assert.strictEqual(cpHelpers.getPagerValue(kanban), "1-3", + "should have 1 page only"); + assert.strictEqual(cpHelpers.getPagerSize(kanban), "3", + "should have 4 records"); + + kanban.destroy(); + }); + QUnit.test('create in grouped on m2o', async function (assert) { assert.expect(5); diff --git a/addons/web/static/tests/legacy/views/list_tests.js b/addons/web/static/tests/legacy/views/list_tests.js index 0aa773b1b72..4691da872aa 100644 --- a/addons/web/static/tests/legacy/views/list_tests.js +++ b/addons/web/static/tests/legacy/views/list_tests.js @@ -25,7 +25,7 @@ var _t = core._t; const cpHelpers = testUtils.controlPanel; var createView = testUtils.createView; -const { legacyExtraNextTick } = require("@web/../tests/helpers/utils"); +const { click, legacyExtraNextTick } = require("@web/../tests/helpers/utils"); const { createWebClient, doAction, loadState } = require('@web/../tests/webclient/helpers'); let serverData; @@ -3698,6 +3698,67 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('archive/unarchive handles returned action', async function (assert) { + assert.expect(6); + + // add active field on foo model and make all records active + serverData.models.foo.fields.active = { string: 'Active', type: 'boolean', default: true }; + + serverData.actions = { + 11: { + id: 11, + name: 'Action 11', + res_model: 'foo', + type: 'ir.actions.act_window', + views: [[3, 'list']], + search_view_id: [9, 'search'], + }, + }; + serverData.views = { + 'foo,3,list': '', + 'foo,9,search': ` + + + `, + 'bar,false,form': '
', + }; + + const mockRPC = (route, args) => { + if (route === '/web/dataset/call_kw/foo/action_archive') { + return Promise.resolve({ + 'type': 'ir.actions.act_window', + 'name': 'Archive Action', + 'res_model': 'bar', + 'view_mode': 'form', + 'target': 'new', + 'views': [[false, 'form']] + }); + } + }; + + const webClient = await createWebClient({ serverData, mockRPC }); + await doAction(webClient, 11); + + assert.containsNone(webClient, '.o_cp_action_menus', 'sidebar should be invisible'); + assert.containsN(webClient, 'tbody td.o_list_record_selector', 4, "should have 4 records"); + + await click(webClient.el.querySelector('tbody td.o_list_record_selector input')); + assert.containsOnce(webClient, '.o_cp_action_menus', 'sidebar should be visible'); + + await click(webClient.el.querySelector('.o_cp_action_menus .o_dropdown_toggler')); + const archiveItem = [...webClient.el.querySelectorAll('.o_cp_action_menus .o_dropdown_menu li > a')] + .filter((elem) => elem.textContent === 'Archive'); + await click(archiveItem[0]); + assert.strictEqual(document.querySelectorAll('.modal').length, 1, + 'a confirm modal should be displayed'); + + await click(document.querySelector('.modal .modal-footer .btn-primary')); + assert.strictEqual(document.querySelectorAll('.modal').length, 1, + 'archive action dialog should be displayed'); + assert.strictEqual(document.querySelector('.modal .modal-title').textContent.trim(), 'Archive Action', + "action wizard should have been opened"); + }); + QUnit.test('pager (ungrouped and grouped mode), default limit', async function (assert) { assert.expect(4); @@ -9159,6 +9220,153 @@ QUnit.module('Views', { assert.verifySteps(['clear_cache']); // triggered by the test environment on destroy }); + QUnit.test('list view move to previous page when all records from last page deleted', async function (assert) { + assert.expect(5); + + let checkSearchRead = false; + const list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '', + mockRPC: function (route, args) { + if (checkSearchRead && route === '/web/dataset/search_read') { + assert.strictEqual(args.limit, 3, "limit should 3"); + assert.notOk(args.offset, "offset should not be passed i.e. offset 0 by default"); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + hasActionMenus: true, + }, + }); + + assert.strictEqual(list.$('.o_pager_counter').text().trim(), '1-3 / 4', + "should have 2 pages and current page should be first page"); + + // move to next page + await testUtils.dom.click(list.$('.o_pager_next')); + assert.strictEqual(list.$('.o_pager_counter').text().trim(), '4-4 / 4', + "should be on second page"); + + // delete a record + await testUtils.dom.click(list.$('tbody .o_data_row:first td.o_list_record_selector:first input')); + checkSearchRead = true; + await testUtils.dom.click(list.$('.o_dropdown_toggler:contains(Action)')); + await testUtils.dom.click(list.$('a:contains(Delete)')); + await testUtils.dom.click($('body .modal button span:contains(Ok)')); + assert.strictEqual(list.$('.o_pager_counter').text().trim(), '1-3 / 3', + "should have 1 page only"); + + list.destroy(); + }); + + QUnit.test('grouped list view move to previous page of group when all records from last page deleted', async function (assert) { + assert.expect(7); + + let checkSearchRead = false; + const list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '', + mockRPC: function (route, args) { + if (checkSearchRead && route === '/web/dataset/search_read') { + assert.strictEqual(args.limit, 2, "limit should 2"); + assert.notOk(args.offset, "offset should not be passed i.e. offset 0 by default"); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + hasActionMenus: true, + }, + groupBy: ['m2o'], + }); + + assert.strictEqual(list.$('th:contains(Value 1 (3))').length, 1, + "Value 1 should contain 3 records"); + assert.strictEqual(list.$('th:contains(Value 2 (1))').length, 1, + "Value 2 should contain 1 record"); + + await testUtils.dom.click(list.$('th.o_group_name:nth(0)')); + assert.strictEqual(list.$('th.o_group_name:eq(0) .o_pager_counter').text().trim(), '1-2 / 3', + "should have 2 pages and current page should be first page"); + + // move to next page + await testUtils.dom.click(list.$('.o_group_header .o_pager_next')); + assert.strictEqual(list.$('th.o_group_name:eq(0) .o_pager_counter').text().trim(), '3-3 / 3', + "should be on second page"); + + // delete a record + await testUtils.dom.click(list.$('tbody .o_data_row:first td.o_list_record_selector:first input')); + checkSearchRead = true; + await testUtils.dom.click(list.$('.o_dropdown_toggler:contains(Action)')); + await testUtils.dom.click(list.$('a:contains(Delete)')); + await testUtils.dom.click($('body .modal button span:contains(Ok)')); + + assert.strictEqual(list.$('th.o_group_name:eq(0) .o_pager_counter').text().trim(), '', + "should be on first page now"); + + list.destroy(); + }); + + QUnit.test('list view move to previous page when all records from last page archive/unarchived', async function (assert) { + assert.expect(9); + + // add active field on foo model and make all records active + this.data.foo.fields.active = { string: 'Active', type: 'boolean', default: true }; + + const list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + viewOptions: { + hasActionMenus: true, + }, + mockRPC: function (route) { + if (route === '/web/dataset/call_kw/foo/action_archive') { + this.data.foo.records[3].active = false; + return Promise.resolve(); + } + return this._super.apply(this, arguments); + }, + }); + + assert.strictEqual(list.$('.o_pager_counter').text().trim(), '1-3 / 4', + "should have 2 pages and current page should be first page"); + assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 3, + "should have 3 records"); + + // move to next page + await testUtils.dom.click(list.$('.o_pager_next')); + assert.strictEqual(list.$('.o_pager_counter').text().trim(), '4-4 / 4', + "should be on second page"); + assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 1, + "should have 1 records"); + assert.containsNone(list, '.o_cp_action_menus', 'sidebar should not be available'); + + await testUtils.dom.click(list.$('tbody .o_data_row:first td.o_list_record_selector:first input')); + assert.containsOnce(list, '.o_cp_action_menus', 'sidebar should be available'); + + // archive all records of current page + await testUtils.dom.click(list.$('.o_cp_action_menus .o_dropdown_toggler:contains(Action)')); + await testUtils.dom.click(list.$('a:contains(Archive)')); + assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); + + await testUtils.dom.click($('body .modal button span:contains(Ok)')); + assert.strictEqual(list.$('tbody td.o_list_record_selector').length, 3, + "should have 3 records"); + assert.strictEqual(list.$('.o_pager_counter').text().trim(), '1-3 / 3', + "should have 1 page only"); + + list.destroy(); + }); + QUnit.test('list should ask to scroll to top on page changes', async function (assert) { assert.expect(10);