[IMP] web, base: auto save
This commit adds the auto save for editable list and form views but not for settings. Now with auto save, changing the pager, going back in the breadcrumb, going to an other action or clicking on a menu item won't ask to confirm changes if any but will automatically save them. In settings, the confirm dialog has been revamped. We can now decide to "Save" or "Discard" the changes or "Stay Here" to do nothing. task 2330101
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
*
|
||||
|
||||
@@ -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<boolean>}
|
||||
* 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),
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'] = '<form>' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="bar" readonly="1"/>' +
|
||||
'</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();
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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': `
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
`,
|
||||
'partner,false,form': `
|
||||
<form>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
'partner,false,search': '<search></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': `
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
`,
|
||||
'partner,false,form': `
|
||||
<form>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
'partner,false,search': '<search></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': `
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
`,
|
||||
'partner,false,form': `
|
||||
<form>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
</form>
|
||||
`,
|
||||
'partner,false,search': '<search></search>',
|
||||
'partner,false,kanban': `
|
||||
<kanban>
|
||||
<field name="name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div></div>
|
||||
</t>
|
||||
</templates>
|
||||
</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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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': '<search></search>',
|
||||
'foo,2,list': '<tree editable="top"><field name="foo"/></tree>',
|
||||
'foo,3,list': '<tree editable="top"><field name="foo"/></tree>',
|
||||
},
|
||||
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': '<search></search>',
|
||||
'foo,2,list': '<tree editable="top"><field name="foo"/></tree>',
|
||||
'foo,3,list': '<tree editable="top"><field name="foo"/></tree>',
|
||||
},
|
||||
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': '<search></search>',
|
||||
'foo,2,list': '<tree editable="top"><field name="foo" required="1"/></tree>',
|
||||
'foo,3,list': '<tree editable="top"><field name="foo"/></tree>',
|
||||
},
|
||||
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: `
|
||||
<tree editable="top" limit="3">
|
||||
<field name="foo"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
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: `
|
||||
<tree editable="top" limit="3">
|
||||
<field name="foo"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
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: `
|
||||
<tree editable="top" limit="3">
|
||||
<field name="foo" required="1"/>
|
||||
</tree>`,
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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': `
|
||||
<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Foo</span>
|
||||
<div class="text-muted">
|
||||
this is foo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
'res.config.settings,false,search': '<search></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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)',
|
||||
}]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user