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: + ` + + +
+
Delete
+ +
+
+
+
`, + }); + + 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);