diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index ee39d7cb7a7..19cda7d3241 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -73,7 +73,9 @@ ActionManager.include({ this._closeDialog(true); // there may be a currently opened dialog, close it var viewOptions = {currentId: state.id}; var viewType = state.view_type || currentController.viewType; - return this._switchController(currentAction, viewType, viewOptions); + return this.clearUncommittedChanges().then(() => { + return this._switchController(currentAction, viewType, viewOptions); + }); } else if (!core.action_registry.contains(state.action)) { // the action to load isn't the current one, so execute it var context = {}; @@ -724,7 +726,9 @@ ActionManager.include({ if (ev.data.mode) { options.mode = ev.data.mode; } - this._switchController(action, viewType, options); + this.clearUncommittedChanges().then(() => { + return this._switchController(action, viewType, options); + }); } }, }); diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js index e12f22fcf05..6c0bc91026a 100644 --- a/addons/web/static/src/js/chrome/web_client.js +++ b/addons/web/static/src/js/chrome/web_client.js @@ -127,14 +127,13 @@ return AbstractWebClient.extend({ // -------------------------------------------------------------- // URL state handling // -------------------------------------------------------------- - on_hashchange: function (event) { + on_hashchange: async function (event) { if (this._ignore_hashchange) { this._ignore_hashchange = false; return Promise.resolve(); } var self = this; - return this.clear_uncommitted_changes().then(function () { var stringstate = $.bbq.getState(false); if (!_.isEqual(self._current_state, stringstate)) { var state = $.bbq.getState(true); @@ -162,12 +161,6 @@ return AbstractWebClient.extend({ } } self._current_state = stringstate; - }, function () { - if (event) { - self._ignore_hashchange = true; - window.location = event.originalEvent.oldURL; - } - }); }, // -------------------------------------------------------------- diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index 06baab7b55c..95c01dd53cb 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -144,16 +144,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, { // Public //-------------------------------------------------------------------------- - /** - * @override - */ - canBeRemoved: function () { - // AAB: get rid of 'readonlyIfRealDiscard' option when on_hashchange mechanism is improved - return this.discardChanges(undefined, { - noAbandon: true, - readonlyIfRealDiscard: true, - }); - }, /** * Discards the changes made on the record associated to the given ID, or * all changes made by the current controller if no recordID is given. For diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 2f61fcb4bf6..0093a9e3425 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -47,10 +47,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { // operations to complete before checking if there are changes to // discard when discardChanges is called this.savingDef = Promise.resolve(); - // discardingDef is used to ensure that we don't ask twice the user if - // he wants to discard changes, when 'canBeDiscarded' is called several - // times "in parallel" - this.discardingDef = null; this.viewId = params.viewId; }, /** @@ -68,6 +64,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { // Public //-------------------------------------------------------------------------- + /** + * @override + * @returns {Promise} + */ + canBeRemoved: function () { + return this.saveChanges(this.handle); + }, /** * Determines if we can discard the current changes. If the model is not * dirty, that is not a problem. However, if it is dirty, we have to ask @@ -81,31 +84,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { * rejected otherwise */ canBeDiscarded: function (recordID) { - var self = this; - if (this.discardingDef) { - // discard dialog is already open - return this.discardingDef; - } - if (!this.isDirty(recordID)) { - return Promise.resolve(false); - } - - var message = _t("The record has been modified, your changes will be discarded. Do you want to proceed?"); - this.discardingDef = new Promise(function (resolve, reject) { - var dialog = Dialog.confirm(self, message, { - title: _t("Warning"), - confirm_callback: () => { - resolve(true); - self.discardingDef = null; - }, - cancel_callback: () => { - reject(); - self.discardingDef = null; - }, - }); - dialog.on('closed', self.discardingDef, reject); - }); - return this.discardingDef; + return Promise.resolve(true); }, /** * Ask the renderer if all associated field widget are in a valid state for @@ -155,6 +134,22 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { isDirty: function (recordID) { return this.model.isDirty(recordID || this.handle); }, + /** + * Saves the record if it's dirty. + * + * @param {string} [recordId] - default to main recordId + * @return {Promise} + */ + saveChanges: async function (recordId) { + recordId = recordId || this.handle; + if (this.isDirty(recordId)) { + await Promise.all([this.mutex.getUnlockedDef(), this.savingDef]); + await this.saveRecord(recordId, { + stayInEdit: true, + reload: false, + }); + } + }, /** * Saves the record whose ID is given if necessary (@see _saveRecord). * diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 05745eb507f..a68135cd77a 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -54,9 +54,13 @@ var FormController = BasicController.extend({ * leaving). * * @override + * @param {boolean} [shouldReload] */ - willRestore: function () { + willRestore: function (shouldReload) { this.mode = this.model.isNew(this.handle) ? 'edit' : 'readonly'; + if (shouldReload) { + return this._setMode(this.mode); + } }, //-------------------------------------------------------------------------- @@ -78,6 +82,25 @@ var FormController = BasicController.extend({ } } }, + /** + * @override + * @param {string} [recordID] - default to main recordID + * @returns {Promise} + * resolved if can be discarded, a boolean value is given to tells + * if there is something to discard or not + * rejected otherwise + */ + canBeDiscarded: function (recordId) { + if (recordId !== this.handle && this.isDirty(recordId)) { + // Embedded list views can ask to discard their changes when we + // click in the webclient. If a field in the list is invalid, it + // stay dirty. + // When these conditions are met we don't want to discard. + return Promise.reject(); + } else { + return Promise.resolve(true); + } + }, /** * This method switches the form view in edit mode, with a new record. * @@ -181,7 +204,7 @@ var FormController = BasicController.extend({ return null; } return Object.assign(this._super(...arguments), { - validate: this.canBeDiscarded.bind(this), + validate: this.saveChanges.bind(this), }); }, /** diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index c94980d45fc..c806cadc735 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -78,6 +78,15 @@ var ListController = BasicController.extend({ // Public //-------------------------------------------------------------------------- + /** + * @override + * @returns {Promise} + */ + canBeRemoved: async function () { + const _super = this._super.bind(this); + await this.renderer.unselectRow(); + return _super(...arguments); + }, /* * @override */ @@ -406,7 +415,9 @@ var ListController = BasicController.extend({ if (!state.count) { return null; } - return this._super(...arguments); + return Object.assign(this._super(...arguments), { + validate: () => this.renderer.unselectRow(), + }); }, /** * @override diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index f8dc72c4a93..7069246e7dc 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -440,6 +440,7 @@ QUnit.module('ActionManager', { // execute action 3 and open a record in form view await actionManager.doAction(3); testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first')); + await testUtils.nextTick(); // wait for clearUncommittedChanges // execute action 4 without 'on_reverse_breadcrumb' handler, then go back await actionManager.doAction(4); @@ -1664,6 +1665,7 @@ QUnit.module('ActionManager', { def = testUtils.makeTestPromise(); await testUtils.nextTick(); testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first')); + await testUtils.nextTick(); // wait for clearUncommittedChanges assert.containsOnce(actionManager, '.o_list_view', "should still display the list view of action 3"); @@ -3316,13 +3318,22 @@ QUnit.module('ActionManager', { actionManager.destroy(); }); - QUnit.test('ask for confirmation when leaving a "dirty" view', async function (assert) { + QUnit.test('save when leaving a "dirty" view', async function (assert) { assert.expect(4); var actionManager = await createActionManager({ actions: this.actions, archs: this.archs, data: this.data, + mockRPC(route, { args, method, model }) { + if (model === 'partner' && method === 'write') { + assert.deepEqual(args, [ + [1], + { foo: 'pinkypie', }, + ]); + } + return this._super(...arguments); + } }); await actionManager.doAction(4); @@ -3336,22 +3347,7 @@ QUnit.module('ActionManager', { // go back to kanban view await testUtils.dom.click($('.o_control_panel .breadcrumb-item:first a')); - assert.strictEqual($('.modal .modal-body').text(), - "The record has been modified, your changes will be discarded. Do you want to proceed?", - "should display a modal dialog to confirm discard action"); - - // cancel - await testUtils.dom.click($('.modal .modal-footer button.btn-secondary')); - - assert.containsOnce(actionManager, '.o_form_view', - "should still be in form view"); - - // go back again to kanban view - await testUtils.dom.click($('.o_control_panel .breadcrumb-item:first a')); - - // confirm discard - await testUtils.dom.click($('.modal .modal-footer button.btn-primary')); - + assert.containsNone(document.body, '.modal', "should not display a modal dialog"); assert.containsNone(actionManager, '.o_form_view', "should no longer be in form view"); assert.containsOnce(actionManager, '.o_kanban_view', @@ -3774,10 +3770,11 @@ QUnit.module('ActionManager', { }); QUnit.test('execute action from dirty, new record, and come back', async function (assert) { - assert.expect(17); + assert.expect(18); this.data.partner.fields.bar.default = 1; this.archs['partner,false,form'] = '
' + + '' + '' + '' + ''; @@ -3814,32 +3811,33 @@ QUnit.module('ActionManager', { "PartnersNew"); // set form view dirty and open m2o record + await testUtils.fields.editInput(actionManager.$('input[name="display_name"]'), 'test'); await testUtils.fields.editInput(actionManager.$('input[name=foo]'), 'val'); await testUtils.dom.click(actionManager.$('.o_form_uri:contains(First record)')); - assert.containsOnce($('body'), '.modal'); // confirm discard dialog - - // confirm discard changes - await testUtils.dom.click($('.modal .modal-footer .btn-primary')); + assert.containsNone(document.body, '.modal', 'should not open modal'); assert.containsOnce(actionManager, '.o_form_view.o_form_readonly'); assert.strictEqual(actionManager.$('.o_control_panel .breadcrumb-item').text(), - "PartnersNewFirst record"); + "PartnerstestFirst record"); - // go back to New using the breadcrumbs + // go back to test using the breadcrumbs await testUtils.dom.click(actionManager.$('.o_control_panel .breadcrumb-item:nth(1) a')); - assert.containsOnce(actionManager, '.o_form_view.o_form_editable'); + + // should be readonly and so saved + assert.containsOnce(actionManager, '.o_form_view.o_form_readonly'); assert.strictEqual(actionManager.$('.o_control_panel .breadcrumb-item').text(), - "PartnersNew"); + "Partnerstest"); assert.verifySteps([ '/web/action/load', // action 3 'load_views', // views of action 3 '/web/dataset/search_read', // list - 'onchange', // form (create) + 'onchange', // edit input 'get_formview_action', // click on m2o 'load_views', // form view of dynamic action 'read', // form - 'onchange', // form (create) + 'create', // form (create) + 'read', // click breadcrumb ]); actionManager.destroy(); @@ -4567,9 +4565,10 @@ QUnit.module('ActionManager', { actionManager.destroy(); }); - QUnit.test('Call twice clearUncommittedChanges in a row does not display twice the discard warning', async function (assert) { - assert.expect(4); + QUnit.test('Call twice clearUncommittedChanges in a row does not save twice', async function (assert) { + assert.expect(5); + let writeCalls = 0; var actionManager = await createActionManager({ actions: this.actions, archs: this.archs, @@ -4579,6 +4578,12 @@ QUnit.module('ActionManager', { actionManager.clearUncommittedChanges(); }, }, + mockRPC(route, { method }) { + if (method === 'write') { + writeCalls += 1; + } + return this._super(...arguments); + }, }); // execute an action and edit existing record @@ -4594,14 +4599,13 @@ QUnit.module('ActionManager', { actionManager.trigger_up('clear_uncommitted_changes'); await testUtils.nextTick(); - assert.containsOnce($('body'), '.modal'); // confirm discard dialog - // confirm discard changes - await testUtils.dom.click($('.modal .modal-footer .btn-primary')); + assert.containsNone($('body'), '.modal'); actionManager.trigger_up('clear_uncommitted_changes'); await testUtils.nextTick(); assert.containsNone($('body'), '.modal'); + assert.strictEqual(writeCalls, 1); actionManager.destroy(); }); diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index 5546a4737f9..ecfc02fe9ee 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -1035,7 +1035,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('onchange with modifiers for embedded one2many on the second page', async function (assert) { - assert.expect(7); + assert.expect(9); var data = this.data; var ids = []; @@ -1100,13 +1100,15 @@ QUnit.module('fields', {}, function () { await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first')); await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp'); - // the domain fail if the widget does not use the allready loaded data. - await testUtils.form.clickDiscard(form); + // click on the label to unselect the row + await testUtils.dom.click(form.$('.o_form_label')); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "blurp#21#22#23#24#25#26#27#28#29", "should display the records in order with the changes"); - await testUtils.dom.click($('.modal .modal-footer button:first')); + // the domain fail if the widget does not use the already loaded data. + await testUtils.form.clickDiscard(form); + assert.containsNone(document.body, '.modal', 'should not open modal'); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29", "should cancel changes and display the records in order"); @@ -1133,12 +1135,12 @@ QUnit.module('fields', {}, function () { assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#39#40#41#42#43#44#45#46#47", "should display the records in order after resequence (display record with turtle_int=0)"); - await testUtils.form.clickDiscard(form); - + await testUtils.dom.click(form.$('.o_form_label')); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#39#40#41#42#43#44#45#46#47", "should display the records in order after resequence"); - await testUtils.dom.click($('.modal .modal-footer button:first')); + await testUtils.form.clickDiscard(form); + assert.containsNone(document.body, '.modal', 'should not open modal'); assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29", "should cancel changes and display the records in order"); @@ -2196,9 +2198,7 @@ QUnit.module('fields', {}, function () { // discard changes await testUtils.form.clickDiscard(form); - assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value', - "changes shouldn't have been discarded yet, waiting for user confirmation"); - await testUtils.dom.click($('.modal .modal-footer .btn-primary')); + assert.containsNone(form, '.modal', 'should not open modal'); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1', "display name of first record in o2m list should be 'relational record 1'"); @@ -2847,7 +2847,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('one2many list (editable): edition, part 3', async function (assert) { - assert.expect(3); + assert.expect(4); var form = await createView({ View: FormView, @@ -2876,7 +2876,7 @@ QUnit.module('fields', {}, function () { // cancel the edition await testUtils.form.clickDiscard(form); - await testUtils.dom.click($('.modal-footer button.btn-primary').first()); + assert.containsNone(form, '.modal', 'should not open modal'); assert.containsOnce(form, 'tr.o_data_row', "should have 1 data rows"); @@ -3013,7 +3013,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('editable one2many list, adding line, then discarding', async function (assert) { - assert.expect(2); + assert.expect(3); this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' }); this.data.partner.records[0].turtles = [1, 2, 3, 4]; @@ -3035,10 +3035,9 @@ QUnit.module('fields', {}, function () { // add a record, then discard await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); - await testUtils.form.clickDiscard(form); - // confirm the discard operation - await testUtils.dom.click($('.modal .modal-footer .btn-primary')); + await testUtils.form.clickDiscard(form); + assert.containsNone(form, '.modal', 'should not open modal'); assert.isVisible(form.$('.o_field_widget[name=turtles] .o_pager')); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-3 / 4', @@ -3113,9 +3112,6 @@ QUnit.module('fields', {}, function () { assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5', "pager should still display the correct total"); - // click on cancel - await testUtils.dom.click($('.modal .modal-footer .btn-secondary')); - assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5', "pager should again display the correct total"); assert.containsOnce(form, '.o_field_one2many input.o_field_invalid', @@ -3124,7 +3120,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('editable one2many list, adding, discarding, and pager', async function (assert) { - assert.expect(4); + assert.expect(5); this.data.partner.records[0].turtles = [1]; @@ -3154,7 +3150,7 @@ QUnit.module('fields', {}, function () { // discard await testUtils.form.clickDiscard(form); - await testUtils.dom.click($('.modal .modal-footer .btn-primary')); + assert.containsNone(form, '.modal', 'should not open modal'); assert.containsOnce(form, 'tr.o_data_row'); assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager'); @@ -3163,7 +3159,7 @@ QUnit.module('fields', {}, function () { }); QUnit.test('unselecting a line with missing required data', async function (assert) { - assert.expect(5); + assert.expect(6); this.data.turtle.fields.turtle_foo.required = true; delete this.data.turtle.fields.turtle_foo.default; @@ -3199,17 +3195,17 @@ QUnit.module('fields', {}, function () { // click elsewhere, await testUtils.dom.click(form.$('label.o_form_label')); - assert.strictEqual($('.modal').length, 1, - 'a confirmation model should be opened'); + assert.containsNone(document.body, '.modal', + 'a confirmation modal should not be opened'); - // click on cancel, the line should still be selected - await testUtils.dom.click($('.modal .modal-footer button.btn-secondary')); + // the line should still be selected assert.containsOnce(form, 'tr.o_data_row.o_selected_row', "should still have 1 selected data row"); - // click elsewhere, and click on ok (on the confirmation dialog) - await testUtils.dom.click(form.$('label.o_form_label')); - await testUtils.dom.click($('.modal .modal-footer button.btn-primary')); + // click discard + await testUtils.dom.click(form.$('.o_form_button_cancel')); + assert.containsNone(document.body, '.modal', + 'a confirmation modal should not be opened'); assert.containsNone(form, 'tr.o_data_row', "should have 0 data rows (invalid line has been discarded"); @@ -7129,8 +7125,8 @@ QUnit.module('fields', {}, function () { assert.containsOnce(form, '.o_data_row.o_selected_row', "line should not have been removed and should still be in edition"); - assert.strictEqual($('.modal').length, 1, - "a confirmation dialog should be opened"); + assert.containsNone(document.body, '.modal', + "a confirmation dialog should not be opened"); assert.hasClass(form.$('.o_field_widget[name=int_field]'),'o_field_invalid', "should indicate that int_field is invalid"); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index fc6110ae5bd..7d6db34f022 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -2874,7 +2874,7 @@ QUnit.module('Views', { }); QUnit.test('discard changes on a dirty form view', async function (assert) { - assert.expect(7); + assert.expect(5); var nbWrite = 0; var form = await createView({ @@ -2898,16 +2898,9 @@ QUnit.module('Views', { assert.strictEqual(form.$('input[name=foo]').val(), 'new value', "input should contain new value"); - // click on discard and cancel the confirm request + // click on discard await testUtils.form.clickDiscard(form); - assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed"); - await testUtils.dom.click('.modal-footer .btn-secondary'); - assert.strictEqual(form.$('input').val(), 'new value', 'input should still contain new value'); - - // click on discard and confirm - await testUtils.form.clickDiscard(form); - assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed"); - await testUtils.dom.click('.modal-footer .btn-primary'); + assert.containsNone(document.body, '.modal', "no confirm modal should be displayed"); assert.strictEqual(form.$('.o_field_widget').text(), 'yop', 'field in readonly should display yop'); assert.strictEqual(nbWrite, 0, "no write RPC should have been done"); @@ -2949,7 +2942,7 @@ QUnit.module('Views', { }); QUnit.test('discard changes on relational data on new record', async function (assert) { - assert.expect(3); + assert.expect(4); var form = await createView({ View: FormView, @@ -2982,9 +2975,9 @@ QUnit.module('Views', { assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xphone', "input should contain xphone"); - // click on discard and confirm + // click on discard await testUtils.form.clickDiscard(form); - await testUtils.dom.click('.modal-footer .btn-primary'); // click on confirm + assert.containsNone(form, '.modal', 'modal should not be displayed'); assert.notOk(form.$el.prop('outerHTML').match('xphone'), "the string xphone should not be present after discarding"); @@ -3021,7 +3014,7 @@ QUnit.module('Views', { }); QUnit.test('discard changes on a new (dirty) form view', async function (assert) { - assert.expect(8); + assert.expect(7); this.data.partner.fields.foo.default = "ABC"; @@ -3047,17 +3040,14 @@ QUnit.module('Views', { await testUtils.fields.editInput(form.$('input[name=foo]'), 'DEF'); // discard the changes and check it has properly been discarded - await testUtils.form.clickDiscard(form); - assert.containsOnce(document.body, '.modal', 'there should be a confirm modal'); assert.strictEqual(form.$('input').val(), 'DEF', 'input should be DEF'); - await testUtils.dom.click('.modal-footer .btn-primary'); // click on confirm + await testUtils.form.clickDiscard(form); assert.strictEqual(form.$('input').val(), 'ABC', 'input should now be ABC'); // redirty and discard the field foo (to make sure initial changes haven't been lost) await testUtils.fields.editInput(form.$('input[name=foo]'), 'GHI'); - await testUtils.form.clickDiscard(form); assert.strictEqual(form.$('input').val(), 'GHI', 'input should be GHI'); - await testUtils.dom.click('.modal-footer .btn-primary'); // click on confirm + await testUtils.form.clickDiscard(form); assert.strictEqual(form.$('input').val(), 'ABC', 'input should now be ABC'); form.destroy(); @@ -3123,22 +3113,18 @@ QUnit.module('Views', { await testUtils.fields.editInput(form.$('input[name=foo]'), 'new value'); assert.strictEqual(form.$('input').val(), 'new value', 'input should contain new value'); - // click on the pager to switch to the next record and cancel the confirm request + // click on the pager to switch to the next record (will save record) await cpHelpers.pagerNext(form); - assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed"); - await testUtils.dom.click('.modal-footer .btn-secondary'); // click on cancel - assert.strictEqual(form.$('input[name=foo]').val(), 'new value', - "input should still contain new value"); - assert.strictEqual(cpHelpers.getPagerValue(form), '1', "pager value should still be 1"); - - // click on the pager to switch to the next record and confirm - await cpHelpers.pagerNext(form); - assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed"); - await testUtils.dom.click('.modal-footer .btn-primary'); // click on confirm - assert.strictEqual(form.$('input[name=foo]').val(), 'blip', "input should contain blip"); + assert.containsNone(document.body, '.modal', "no confirm modal should be displayed"); assert.strictEqual(cpHelpers.getPagerValue(form), '2', "pager value should be 2"); + assert.strictEqual(form.$('input[name=foo]').val(), 'blip', "input should contain blip"); - assert.strictEqual(nbWrite, 0, 'no write RPC should have been done'); + await cpHelpers.pagerPrevious(form); + assert.containsNone(document.body, '.modal', "no confirm modal should be displayed"); + assert.strictEqual(cpHelpers.getPagerValue(form), '1', "pager value should be 1"); + assert.strictEqual(form.$('input[name=foo]').val(), 'new value', "input should contain new value"); + + assert.strictEqual(nbWrite, 1, 'one write RPC should have been done'); form.destroy(); }); @@ -3201,8 +3187,7 @@ QUnit.module('Views', { await testUtils.fields.editInput(form.$('input[name=foo]'), 'wrong value'); await testUtils.form.clickDiscard(form); - assert.containsOnce(document.body, '.modal', "a confirm modal should be displayed"); - await testUtils.dom.click('.modal-footer .btn-primary'); // click on confirm + assert.containsNone(document.body, '.modal', "no confirm modal should be displayed"); await cpHelpers.pagerNext(form); assert.strictEqual(cpHelpers.getPagerValue(form), '2', "pager value should be 2"); form.destroy(); @@ -3932,8 +3917,8 @@ QUnit.module('Views', { form.destroy(); }); - QUnit.test('delete a line in a one2many while editing another line triggers a warning', async function (assert) { - assert.expect(3); + QUnit.test('delete a line in a one2many while editing another line', async function (assert) { + assert.expect(2); this.data.partner.records[0].p = [1, 2]; @@ -3956,18 +3941,14 @@ QUnit.module('Views', { await testUtils.fields.editInput(form.$('input[name=display_name]'), ''); await testUtils.dom.click(form.$('.fa-trash-o').eq(1)); - assert.strictEqual($('.modal').find('.modal-title').first().text(), "Warning", - "Clicking out of a dirty line while editing should trigger a warning modal."); - - await testUtils.dom.click($('.modal').find('.btn-primary')); // use of owlCompatibilityExtraNextTick because there are two sequential updates of the // control panel (which is written in owl): each of them waits for the next animation frame // to complete await testUtils.owlCompatibilityExtraNextTick(); - assert.strictEqual(form.$('.o_data_cell').first().text(), "first record", - "Value should have been reset to what it was before editing began."); - assert.containsOnce(form, '.o_data_row', - "The other line should have been deleted."); + assert.hasClass(form.$('.o_data_cell').first(), "o_invalid_cell", + "Cell should be invalidated."); + assert.containsN(form, '.o_data_row', 2, + "The other line should not have been deleted."); form.destroy(); }); @@ -4299,7 +4280,7 @@ QUnit.module('Views', { }); QUnit.test('onchanges that complete after discarding', async function (assert) { - assert.expect(5); + assert.expect(6); var def1 = testUtils.makeTestPromise(); @@ -4338,7 +4319,7 @@ QUnit.module('Views', { // discard changes await testUtils.form.clickDiscard(form); - await testUtils.dom.click($('.modal-footer .btn-primary')); + assert.containsNone(form, '.modal'); assert.strictEqual(form.$('span[name="foo"]').text(), "blip", "field foo should still be displayed to initial value"); @@ -4381,7 +4362,7 @@ QUnit.module('Views', { // save the value and discard directly await testUtils.form.clickSave(form); - form.discardChanges(); // Simulate click on breadcrumb + form.discardChanges(); assert.strictEqual(form.$('.o_field_widget[name="foo"]').val(), "1234", "field foo should still contain new value"); @@ -8673,6 +8654,7 @@ QUnit.module('Views', { QUnit.test('call canBeRemoved twice', async function (assert) { assert.expect(4); + let writeCalls = 0; const form = await createView({ View: FormView, model: 'partner', @@ -8682,28 +8664,30 @@ QUnit.module('Views', { viewOptions: { mode: 'edit', }, + mockRPC(route) { + if (route === '/web/dataset/call_kw/partner/write') { + writeCalls += 1; + } + return this._super(...arguments); + }, }); assert.containsOnce(form, '.o_form_editable'); await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value'); - form.canBeRemoved(); - await testUtils.nextTick(); - assert.containsOnce(document.body, '.modal'); - - form.canBeRemoved(); - await testUtils.nextTick(); - assert.containsOnce(document.body, '.modal'); - - await testUtils.dom.click($('.modal .modal-footer .btn-secondary')); - + await form.canBeRemoved(); assert.containsNone(document.body, '.modal'); + await form.canBeRemoved(); + assert.containsNone(document.body, '.modal'); + + assert.strictEqual(writeCalls, 1, 'should save once'); + form.destroy(); }); QUnit.test('domain returned by onchange is cleared on discard', async function (assert) { - assert.expect(4); + assert.expect(5); this.data.partner.onchanges = { foo: function () {}, @@ -8748,8 +8732,7 @@ QUnit.module('Views', { // switch to another record (should ask to discard changes, and reset the domain) await cpHelpers.pagerNext(form); - // discard changes by clicking on confirm in the dialog - await testUtils.dom.click($('.modal .modal-footer .btn-primary:first')); + assert.containsNone(document.body, '.modal', 'should not open modal'); assert.strictEqual(form.$('input[name=foo]').val(), 'blip', "should be on record 2"); @@ -9665,6 +9648,207 @@ QUnit.module('Views', { assert.verifySteps(['mounted', 'willUnmount']); }); + + QUnit.test('Auto save: save when page changed', async function (assert) { + assert.expect(10); + + const actions = [{ + id: 1, + name: 'Partner', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list'], [false, 'form']], + }]; + + const actionManager = await createActionManager({ + actions, + data: this.data, + archs: { + 'partner,false,list': ` + + + + `, + 'partner,false,form': ` +
+ + + +
+ `, + 'partner,false,search': '', + }, + mockRPC(route, { args, method }) { + if (method === 'write') { + assert.deepEqual(args, [ + [1], + { name: "aaa" }, + ]); + } + return this._super(...arguments); + }, + }); + await actionManager.doAction(1); + + await testUtils.dom.click(actionManager.$('.o_data_row:first')); + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partnerfirst record'); + + await testUtils.dom.click(actionManager.$('.o_form_button_edit')); + await testUtils.fields.editInput(actionManager.$('.o_field_widget[name="name"]'), 'aaa'); + + await testUtils.controlPanel.pagerNext(actionManager); + assert.containsOnce(actionManager, '.o_form_editable'); + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partnersecond record'); + assert.strictEqual(actionManager.$('.o_field_widget[name="name"]').val(), 'name'); + + await testUtils.dom.click(actionManager.$('.o_form_button_cancel')); + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partnersecond record'); + assert.strictEqual(actionManager.$('.o_field_widget[name="name"]').text(), 'name'); + + await testUtils.controlPanel.pagerPrevious(actionManager); + assert.containsOnce(actionManager, '.o_form_readonly'); + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partnerfirst record'); + assert.strictEqual(actionManager.$('.o_field_widget[name="name"]').text(), 'aaa'); + + actionManager.destroy(); + }); + + QUnit.test('Auto save: save when breadcrumb clicked', async function (assert) { + assert.expect(7); + + const actions = [{ + id: 1, + name: 'Partner', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list'], [false, 'form']], + }]; + + const actionManager = await createActionManager({ + actions, + data: this.data, + archs: { + 'partner,false,list': ` + + + + `, + 'partner,false,form': ` +
+ + + +
+ `, + 'partner,false,search': '', + }, + mockRPC(route, { args, method }) { + if (method === 'write') { + assert.deepEqual(args, [ + [1], + { name: "aaa" }, + ]); + } + return this._super(...arguments); + }, + }); + await actionManager.doAction(1); + + await testUtils.dom.click(actionManager.$('.o_data_row:first')); + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partnerfirst record'); + + await testUtils.dom.click(actionManager.$('.o_form_button_edit')); + await testUtils.fields.editInput(actionManager.$('.o_field_widget[name="name"]'), 'aaa'); + + await testUtils.dom.click(actionManager.$('.breadcrumb-item.o_back_button')); + + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partner'); + assert.strictEqual(actionManager.$('.o_field_cell[name="name"]:first').text(), 'aaa'); + + await testUtils.dom.click(actionManager.$('.o_data_row:first')); + assert.containsOnce(actionManager, '.o_form_readonly'); + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partnerfirst record'); + assert.strictEqual(actionManager.$('.o_field_widget[name="name"]').text(), 'aaa'); + + actionManager.destroy(); + }); + + QUnit.test('Auto save: save when action changed', async function (assert) { + assert.expect(6); + + const actions = [{ + id: 1, + name: 'Partner', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list'], [false, 'form']], + }, { + id: 2, + name: 'Other action', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'kanban']], + }]; + + const actionManager = await createActionManager({ + actions, + data: this.data, + archs: { + 'partner,false,list': ` + + + + `, + 'partner,false,form': ` +
+ + + +
+ `, + 'partner,false,search': '', + 'partner,false,kanban': ` + + + + +
+
+
+
+ `, + }, + mockRPC(route, { args, method }) { + if (method === 'write') { + assert.deepEqual(args, [ + [1], + { name: "aaa" }, + ]); + } + return this._super(...arguments); + }, + }); + await actionManager.doAction(1); + + await testUtils.dom.click(actionManager.$('.o_data_row:first')); + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partnerfirst record'); + + await testUtils.dom.click(actionManager.$('.o_form_button_edit')); + await testUtils.fields.editInput(actionManager.$('.o_field_widget[name="name"]'), 'aaa'); + + await actionManager.doAction(2, { clear_breadcrumbs: true }); + + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Other action'); + + await actionManager.doAction(1, { clear_breadcrumbs: true }); + + await testUtils.dom.click(actionManager.$('.o_data_row:first')); + assert.containsOnce(actionManager, '.o_form_readonly'); + assert.strictEqual(actionManager.$('.breadcrumb').text(), 'Partnerfirst record'); + assert.strictEqual(actionManager.$('.o_field_widget[name="name"]').text(), 'aaa'); + + actionManager.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 4c4a8167273..b93b47e3d4f 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -6593,10 +6593,9 @@ QUnit.module('Views', { await testUtils.dom.click(list.$('.o_data_cell:first')); await testUtils.fields.editInput(list.$('input[name="foo"]'), "hello"); await testUtils.dom.click(list.$buttons.find('.o_list_button_discard')); - assert.strictEqual($('.modal:visible').length, 1, - "a modal to ask for discard should be visible"); + assert.containsNone(document.body, '.modal', + "a modal to ask for discard should not be visible"); - await testUtils.dom.click($('.modal:visible .btn-primary')); assert.strictEqual(list.$('.o_data_cell:first').text(), "yop", "first cell should still contain 'yop'"); @@ -7731,8 +7730,7 @@ QUnit.module('Views', { $discardButton[0].dispatchEvent(new MouseEvent('mouseup')); await testUtils.dom.click($discardButton); - assert.ok($('.modal').text().includes("Warning"), "Modal should ask to discard changes"); - await testUtils.dom.click($('.modal .btn-primary')); + assert.containsNone(document.body, '.modal', 'should not open modal'); assert.strictEqual(list.$('.o_data_row:first() .o_data_cell:first()').text(), "yop"); @@ -9261,7 +9259,7 @@ QUnit.module('Views', { }); QUnit.test('pressing ESC in editable grouped list should discard the current line changes', async function (assert) { - assert.expect(5); + assert.expect(6); var list = await createView({ View: ListView, @@ -9280,7 +9278,7 @@ QUnit.module('Views', { await testUtils.fields.editAndTrigger(list.$('tr.o_selected_row .o_data_cell:first input[name="foo"]'), 'new_value', 'input'); // discard by pressing ESC await testUtils.fields.triggerKeydown(list.$('input[name="foo"]'), 'escape'); - await testUtils.dom.click($('.modal .modal-footer .btn-primary')); + assert.containsNone(document.body, '.modal', 'should not open modal'); assert.containsOnce(list, 'tbody tr td:contains(yop)'); assert.containsN(list, 'tr.o_data_row', 3); @@ -9830,9 +9828,8 @@ QUnit.module('Views', { await testUtils.fields.triggerKeydown($(document.activeElement), 'escape'); - assert.hasClass($('body'), 'modal-open', - 'record has been modified, are you sure modal should be opened'); - await testUtils.dom.click($('body .modal button span:contains(Ok)')); + assert.containsNone(document.body, '.modal', + 'record has been modified but modal should not be opened'); assert.doesNotHaveClass(list.$('tr.o_data_row:eq(1)'), 'o_selected_row', 'second row should be closed'); @@ -11121,6 +11118,220 @@ QUnit.module('Views', { list.destroy(); }); + + QUnit.test("Auto save: add a record and leave action", async function (assert) { + assert.expect(4); + + const actionManager = await createActionManager({ + actions: [{ + id: 1, + name: 'Action 1', + res_model: 'foo', + type: 'ir.actions.act_window', + views: [[2, 'list']], + search_view_id: [1, 'search'], + }, { + id: 2, + name: 'Action 2', + res_model: 'foo', + type: 'ir.actions.act_window', + views: [[3, 'list']], + search_view_id: [1, 'search'], + }], + archs: { + 'foo,1,search': '', + 'foo,2,list': '', + 'foo,3,list': '', + }, + data: this.data, + }); + + await actionManager.doAction(1); + + assert.strictEqual(actionManager.$('.o_field_cell[name="foo"]').text(), "yopblipgnapblip"); + assert.containsN(actionManager, '.o_data_row', 4); + + await testUtils.dom.click(actionManager.$('.o_list_button_add')); + await testUtils.fields.editInput(actionManager.$('.o_field_widget[name="foo"]'), "test"); + + // change action and come back + await actionManager.doAction(2); + await actionManager.doAction(1, { clear_breadcrumbs: true }); + + assert.strictEqual(actionManager.$('.o_field_cell[name="foo"]').text(), "yopblipgnapbliptest"); + assert.containsN(actionManager, '.o_data_row', 5); + + actionManager.destroy(); + }); + + QUnit.test("Auto save: modify a record and leave action", async function (assert) { + assert.expect(2); + + const actionManager = await createActionManager({ + actions: [{ + id: 1, + name: 'Action 1', + res_model: 'foo', + type: 'ir.actions.act_window', + views: [[2, 'list']], + search_view_id: [1, 'search'], + }, { + id: 2, + name: 'Action 2', + res_model: 'foo', + type: 'ir.actions.act_window', + views: [[3, 'list']], + search_view_id: [1, 'search'], + }], + archs: { + 'foo,1,search': '', + 'foo,2,list': '', + 'foo,3,list': '', + }, + data: this.data, + }); + + await actionManager.doAction(1); + + assert.strictEqual(actionManager.$('.o_field_cell[name="foo"]').text(), "yopblipgnapblip"); + + await testUtils.dom.click(actionManager.$('.o_field_cell[name="foo"]:first')); + await testUtils.fields.editInput(actionManager.$('.o_field_widget[name="foo"]'), "test"); + + // change action and come back + await actionManager.doAction(2); + await actionManager.doAction(1, { clear_breadcrumbs: true }); + + assert.strictEqual(actionManager.$('.o_field_cell[name="foo"]').text(), "testblipgnapblip"); + + actionManager.destroy(); + }); + + QUnit.test("Auto save: modify a record and leave action (reject)", async function (assert) { + assert.expect(5); + + const actionManager = await createActionManager({ + actions: [{ + id: 1, + name: 'Action 1', + res_model: 'foo', + type: 'ir.actions.act_window', + views: [[2, 'list']], + search_view_id: [1, 'search'], + }, { + id: 2, + name: 'Action 2', + res_model: 'foo', + type: 'ir.actions.act_window', + views: [[3, 'list']], + search_view_id: [1, 'search'], + }], + archs: { + 'foo,1,search': '', + 'foo,2,list': '', + 'foo,3,list': '', + }, + data: this.data, + }); + + await actionManager.doAction(1); + + assert.strictEqual(actionManager.$('.o_field_cell[name="foo"]').text(), "yopblipgnapblip"); + + await testUtils.dom.click(actionManager.$('.o_field_cell[name="foo"]:first')); + await testUtils.fields.editInput(actionManager.$('.o_field_widget[name="foo"]'), ""); + + await assert.rejects(actionManager.doAction(2)); + + assert.strictEqual(actionManager.$('.o_field_cell[name="foo"]').text(), "blipgnapblip"); + assert.hasClass(actionManager.$('.o_field_widget[name="foo"]:first'), 'o_field_invalid'); + assert.containsN(actionManager, '.o_data_row', 4); + + actionManager.destroy(); + }); + + QUnit.test("Auto save: add a record and change page", async function (assert) { + assert.expect(3); + + const list = await createView({ + debug: true, + View: ListView, + model: 'foo', + data: this.data, + arch: ` + + + `, + }); + + assert.strictEqual(list.$('.o_field_cell[name="foo"]').text(), "yopblipgnap"); + + await testUtils.dom.click(list.$('.o_list_button_add')); + await testUtils.fields.editInput(list.$('.o_field_widget[name="foo"]'), "test"); + await testUtils.controlPanel.pagerNext(list); + + assert.strictEqual(list.$('.o_field_cell[name="foo"]').text(), "bliptest"); + + await testUtils.controlPanel.pagerPrevious(list); + + assert.strictEqual(list.$('.o_field_cell[name="foo"]').text(), "yopblipgnap"); + + list.destroy(); + }); + + QUnit.test("Auto save: modify a record and change page", async function (assert) { + assert.expect(3); + + const list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: ` + + + `, + }); + + assert.strictEqual(list.$('.o_field_cell[name="foo"]').text(), "yopblipgnap"); + + await testUtils.dom.click(list.$('.o_field_cell[name="foo"]:first')); + await testUtils.fields.editInput(list.$('.o_field_widget[name="foo"]'), "test"); + await testUtils.controlPanel.pagerNext(list); + + assert.strictEqual(list.$('.o_field_cell[name="foo"]').text(), "blip"); + + await testUtils.controlPanel.pagerPrevious(list); + + assert.strictEqual(list.$('.o_field_cell[name="foo"]').text(), "testblipgnap"); + + list.destroy(); + }); + + QUnit.test("Auto save: modify a record and change page (reject)", async function (assert) { + assert.expect(3); + + const list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: ` + + + `, + }); + + assert.strictEqual(list.$('.o_field_cell[name="foo"]').text(), "yopblipgnap"); + + await testUtils.dom.click(list.$('.o_field_cell[name="foo"]:first')); + await testUtils.fields.editInput(list.$('.o_field_widget[name="foo"]'), ""); + + await testUtils.controlPanel.pagerNext(list); + + assert.hasClass(list.$('.o_field_widget[name="foo"]:first'), 'o_field_invalid'); + assert.strictEqual(list.$('.o_field_cell[name="foo"]').text(), "blipgnap"); + + list.destroy(); + }); }); }); diff --git a/odoo/addons/base/static/src/js/res_config_settings.js b/odoo/addons/base/static/src/js/res_config_settings.js index fdaf5edfd37..6205ddec7a0 100644 --- a/odoo/addons/base/static/src/js/res_config_settings.js +++ b/odoo/addons/base/static/src/js/res_config_settings.js @@ -7,6 +7,7 @@ var FormView = require('web.FormView'); var FormController = require('web.FormController'); var FormRenderer = require('web.FormRenderer'); var view_registry = require('web.view_registry'); +const Dialog = require('web.Dialog'); var QWeb = core.qweb; var _t = core._t; @@ -284,6 +285,10 @@ var BaseSettingController = FormController.extend({ this._super.apply(this, arguments); this.disableAutofocus = true; this.renderer.activeSettingTab = this.initialState.context.module; + // discardingDef is used to ensure that we don't ask twice the user if + // he wants to discard changes, when 'canBeDiscarded' is called several + // times "in parallel" + this.discardingDef = null; }, /** * Settings view should always be in edit mode, so we have to override @@ -294,6 +299,100 @@ var BaseSettingController = FormController.extend({ willRestore: function () { this.mode = 'edit'; }, + /** + * @override + * @returns {Promise} + */ + canBeRemoved: function () { + return this.discardChanges(undefined, { + noAbandon: true, + readonlyIfRealDiscard: true, + }); + }, + /** + * @override + * @param {string} recordId + * @returns {Promise} + */ + canBeDiscarded: function (recordId) { + if (this.discardingDef) { + return this.discardingDef; + } + if (!this.isDirty(recordId)) { + return Promise.resolve(false); + } + const message = _t('Would you like to save your changes?'); + this.discardingDef = new Promise((resolve, reject) => { + const reset = () => { + // enable buttons if user first save which fails because of required field missed + // and then cancel confirmation dialog + this._enableButtons(); + this.discardingDef = null; + }; + const cancel = () => { + reject(); + reset(); + }; + const dialog = Dialog.confirm(this, message, { + title: _t('Unsaved changes'), + buttons: [{ + text: _t('Save'), + classes: 'btn-primary', + click: async () => { + this._disableButtons(); + try { + // _onButtonClicked always saves the record even if + // it's discarded. Here we need to save before + // triggering the changes on the server. + await this.saveRecord(recordId, { + stayInEdit: true, + }); + const record = this.model.get(recordId); + this.trigger_up('execute_action', { + action_data: { + context: record.getContext({ + additionalContext: {}, + }), + name: "execute", + type: "object", + }, + env: { + context: record.getContext(), + currentID: record.data.id, + model: record.model, + resIDs: record.res_ids, + }, + on_success() { + resolve(false); + dialog.close(); + }, + on_fail() { + cancel(); + dialog.close(); + }, + }); + } catch (e) { + cancel(); + dialog.close(); + } + }, + }, { + text: _t('Discard'), + close: true, + click: () => { + resolve(true); + reset(); + }, + }, { + text: _t('Stay Here'), + close: true, + click: cancel, + }], + }); + dialog.on('closed', this.discardingDef, cancel); + }); + return this.discardingDef; + }, //-------------------------------------------------------------------------- // Handlers diff --git a/odoo/addons/base/static/tests/base_settings_tests.js b/odoo/addons/base/static/tests/base_settings_tests.js index 213e93c1e1d..f1cc7f5ec27 100644 --- a/odoo/addons/base/static/tests/base_settings_tests.js +++ b/odoo/addons/base/static/tests/base_settings_tests.js @@ -222,7 +222,7 @@ QUnit.module('base_settings_tests', { await testUtils.dom.click(actionManager.$('button[name="4"]')); assert.containsOnce(document.body, '.modal', "should open a warning dialog"); - await testUtils.dom.click($('.modal button:contains(Ok)')); + await testUtils.dom.click($('.modal button:contains(Discard)')); assert.containsOnce(actionManager, '.o_list_view', "should be open list view"); await testUtils.dom.click($('.o_control_panel .breadcrumb-item a')); @@ -233,7 +233,7 @@ QUnit.module('base_settings_tests', { await testUtils.dom.click(actionManager.$('button[name="4"]')); assert.containsOnce(document.body, '.modal', "should open a warning dialog"); - await testUtils.dom.click($('.modal button:contains(Cancel)')); + await testUtils.dom.click($('.modal button:contains(Stay Here)')); assert.containsOnce(actionManager, '.o_form_view' ,"should be remain on form view"); await testUtils.dom.click(actionManager.$("button[name='execute']")); @@ -477,5 +477,75 @@ QUnit.module('base_settings_tests', { form.destroy(); }); + QUnit.test('call "call_button/execute" when clicking on a button in dirty settings', async function (assert) { + assert.expect(7); + + const actions = [{ + id: 1, + name: 'Settings view', + res_model: 'res.config.settings', + type: 'ir.actions.act_window', + views: [[1, 'form']], + }]; + const archs = { + 'res.config.settings,1,form': ` +
+
+
+
+
+ +
+
+ Foo +
+ this is foo +
+
+
+
+
+
+ `, + 'res.config.settings,false,search': '', + }; + + const actionManager = await createActionManager({ + actions: actions, + archs: archs, + data: this.data, + mockRPC(route, args) { + if (route === '/web/dataset/call_button' && args.method === 'execute') { + assert.step('execute'); + return Promise.resolve(true); + } else if (args.method === 'create') { + assert.step('create'); + } + return this._super.apply(this, arguments); + }, + }); + + await actionManager.doAction(1); + assert.containsNone(actionManager, '.o_field_boolean input:checked', + 'checkbox should not be checked'); + + await testUtils.dom.click(actionManager.$('input[type="checkbox"]')); + assert.containsOnce(actionManager, '.o_field_boolean input:checked', + 'checkbox should be checked'); + + await testUtils.dom.click(actionManager.$('button[name="4"]')); + assert.containsOnce(document.body, '.modal', 'should open a warning dialog'); + + await testUtils.dom.click($('.modal-footer .btn-primary')); + assert.verifySteps([ + 'create', // saveRecord from modal + 'execute', // execute_action + 'create' // saveRecord from FormController._onButtonClicked + ]); + + actionManager.destroy(); + }); + }); }); diff --git a/odoo/addons/test_new_api/static/tests/tours/x2many.js b/odoo/addons/test_new_api/static/tests/tours/x2many.js index a8f207fff6d..9e0080f3423 100644 --- a/odoo/addons/test_new_api/static/tests/tours/x2many.js +++ b/odoo/addons/test_new_api/static/tests/tours/x2many.js @@ -328,9 +328,6 @@ odoo.define('web.test.x2many', function (require) { trigger: '.o_cp_buttons .o_form_button_cancel', extra_trigger: '.tab-pane:eq(0) .o_field_widget tbody:has(tr td:containsExact(ddd))', run: 'click', - }, { - content: "confirm cancel change", - trigger: '.modal-footer button:contains(Ok)', }, ///////////////////////////////////////////////////////////////////////////////////////////// @@ -501,8 +498,5 @@ odoo.define('web.test.x2many', function (require) { content: "cancel change", trigger: '.o_cp_buttons .o_form_button_cancel', run: 'click', - }, { - content: "confirm cancel change", - trigger: '.modal-footer button:contains(Ok)', }]); });