summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-06-28 19:01:52 +0200
committerChristophe Simonis <chs@odoo.com>2018-06-28 19:01:52 +0200
commitc8bb02141d2801c243ae65ba6800b07f090bb5c6 (patch)
tree2d099c5577df4783a6db3d853cf0141bf41c1aa9 /addons/web/static
parent4e2733e83a1f42773fb28f66431a0d6fb0393be9 (diff)
parent3cdcbce93cbde548d2d24426ee8de1d93f06706e (diff)
[MERGE] forward port branch saas-11.3 up to 3cdcbce93cb
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js10
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js16
-rw-r--r--addons/web/static/src/js/services/crash_manager.js3
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js42
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js6
-rw-r--r--addons/web/static/tests/chrome/action_manager_tests.js73
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js221
-rw-r--r--addons/web/static/tests/views/form_tests.js45
8 files changed, 399 insertions, 17 deletions
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index 0fe45f011e8..f98447c8983 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -391,10 +391,13 @@ var ActionManager = Widget.extend({
}
return this._startController(controller).then(function (controller) {
+ var prevDialogOnClose;
if (self.currentDialogController) {
+ prevDialogOnClose = self.currentDialogController.onClose;
self._closeDialog(true);
}
+ controller.onClose = prevDialogOnClose || options.on_close;
var dialog = new Dialog(self, _.defaults({}, options, {
buttons: [],
dialogClass: controller.className,
@@ -405,7 +408,7 @@ var ActionManager = Widget.extend({
self._removeAction(action.jsID);
self.currentDialogController = null;
if (silent !== true) {
- options.on_close();
+ controller.onClose();
}
});
controller.dialog = dialog;
@@ -488,8 +491,9 @@ var ActionManager = Widget.extend({
* @returns {Deferred} resolved immediately
*/
_executeCloseAction: function (action, options) {
+ var result;
if (!this.currentDialogController) {
- options.on_close();
+ result = options.on_close();
}
this._closeDialog();
@@ -499,7 +503,7 @@ var ActionManager = Widget.extend({
this.trigger_up('show_effect', action.effect);
}
- return $.when();
+ return $.when(result);
},
/**
* Executes actions of type 'ir.actions.server'.
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 43b4e349a66..d007305dc79 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -2516,6 +2516,21 @@ var AceEditor = DebouncedField.extend({
//--------------------------------------------------------------------------
/**
+ * Format value
+ *
+ * Note: We have to overwrite this method to always return a string.
+ * AceEditor works with string and not boolean value.
+ *
+ * @override
+ * @private
+ * @param {boolean|string} value
+ * @returns {string}
+ */
+ _formatValue: function (value) {
+ return this._super.apply(this, arguments) || '';
+ },
+
+ /**
* @override
* @private
*/
@@ -2537,6 +2552,7 @@ var AceEditor = DebouncedField.extend({
this.aceSession.setValue(newValue);
}
},
+
/**
* Starts the ace library on the given DOM element. This initializes the
* ace editor option according to the edit/readonly mode and binds ace
diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js
index 8d15efda686..f3bfa87341c 100644
--- a/addons/web/static/src/js/services/crash_manager.js
+++ b/addons/web/static/src/js/services/crash_manager.js
@@ -41,7 +41,8 @@ var CrashManager = core.Class.extend({
core.bus.trigger('connection_lost');
this.connection_lost = true;
var timeinterval = setInterval(function() {
- ajax.jsonRpc('/web/webclient/version_info').then(function() {
+ var options = {shadow: true};
+ ajax.jsonRpc('/web/webclient/version_info', 'call', {}, options).then(function () {
clearInterval(timeinterval);
core.bus.trigger('connection_restored');
self.connection_lost = false;
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index ffa93fc4d78..9d509dd703c 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -584,13 +584,16 @@ var BasicModel = AbstractModel.extend({
/**
* Returns true if a record can be abandoned.
*
- * A record can be abandonned if it is a new record, except if
- * this datapoint has been specifically tagged as "do not abandon".
+ * Case for not abandoning the record:
*
- * Example:
+ * 1. flagged as 'no abandon' (i.e. during a `default_get`, including any
+ * `onchange` from a `default_get`)
+ * 2. registered in a list on addition
+ * 2.1. registered as non-new addition
+ * 2.2. registered as new additon on update
+ * 3. record is not new
*
- * - Discard record from "Add an item" => "New" record => abandon
- * - Discard record from `default_get`/`onchange` => do not abandon
+ * Otherwise, the record can be abandoned.
*
* This is useful when discarding changes on this record, as it means that
* we must keep the record even if some fields are invalids (e.g. required
@@ -600,7 +603,29 @@ var BasicModel = AbstractModel.extend({
* @returns {boolean}
*/
canBeAbandoned: function (id) {
- return !this.localData[id]._noAbandon && this.isNew(id);
+ // 1. no drop if flagged
+ if (this.localData[id]._noAbandon) {
+ return false;
+ }
+ // 2. no drop in a list on "ADD in some cases
+ var record = this.localData[id];
+ var parent = this.localData[record.parentID];
+ if (parent) {
+ var entry = _.findWhere(parent._savePoint, {operation: 'ADD', id: id});
+ if (entry) {
+ // 2.1. no drop on non-new addition in list
+ if (!entry.isNew) {
+ return false;
+ }
+ // 2.2. no drop on new addition on "UPDATE"
+ var lastEntry = _.last(parent._savePoint);
+ if (lastEntry.operation === 'UPDATE' && lastEntry.id === id) {
+ return false;
+ }
+ }
+ }
+ // 3. drop new records
+ return this.isNew(id);
},
/**
* Returns true if a record is dirty. A record is considered dirty if it has
@@ -2571,6 +2596,11 @@ var BasicModel = AbstractModel.extend({
relationField: field.relation_field,
viewType: view ? view.type : fieldInfo.viewType,
});
+ // set existing changes to the list
+ if (record._changes && record._changes[fieldName]) {
+ list._changes = self.localData[record._changes[fieldName]]._changes;
+ record._changes[fieldName] = list.id;
+ }
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
var def = self._readUngroupedList(list).then(function () {
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 be49c12e768..d0ca5c3d09f 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -634,7 +634,11 @@ var FormController = BasicController.extend({
*/
_onSave: function (ev) {
ev.stopPropagation(); // Prevent x2m lines to be auto-saved
- this.saveRecord();
+ var self = this;
+ this._disableButtons();
+ this.saveRecord().always(function () {
+ self._enableButtons();
+ });
},
/**
* This method is called when someone tries to sort a column, most likely
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index b0a90173363..216e6cf6756 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -98,6 +98,9 @@ QUnit.module('ActionManager', {
name: 'A Client Action',
tag: 'ClientAction',
type: 'ir.actions.client',
+ }, {
+ id: 10,
+ type: 'ir.actions.act_window_close',
}];
this.archs = {
@@ -2416,6 +2419,51 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('requests for execute_action of type object: disable buttons', function (assert) {
+ assert.expect(2);
+
+ var self = this;
+ var def;
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_button') {
+ return $.when(false);
+ } else if (args.method === 'read') {
+ // Block the 'read' call
+ var result = this._super.apply(this, arguments);
+ return $.when(def).then(_.constant(result));
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ actionManager.doAction(3);
+
+ // open a record in form view
+ actionManager.$('.o_list_view .o_data_row:first').click();
+
+ // click on 'Call method' button (should call an Object method)
+ def = $.Deferred();
+ actionManager.$('.o_form_view button:contains(Call method)').click();
+
+ // Buttons should be disabled
+ assert.strictEqual(
+ actionManager.$('.o_form_view button:contains(Call method)').attr('disabled'),
+ 'disabled', 'buttons should be disabled')
+
+ // Release the 'read' call
+ def.resolve();
+
+ // Buttons should be enabled after the reload
+ assert.strictEqual(
+ actionManager.$('.o_form_view button:contains(Call method)').attr('disabled'),
+ undefined, 'buttons should be disabled')
+
+ actionManager.destroy();
+ });
+
QUnit.test('can open different records from a multi record view', function (assert) {
assert.expect(11);
@@ -3015,6 +3063,31 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('chained action on_close', function (assert) {
+ assert.expect(3);
+
+ function on_close() {
+ assert.step('Close Action');
+ };
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+ actionManager.doAction(5, {on_close: on_close});
+
+ // a target=new action shouldn't activate the on_close
+ actionManager.doAction(5);
+ assert.verifySteps([]);
+
+ // An act_window_close should trigger the on_close
+ actionManager.doAction(10);
+ assert.verifySteps(['Close Action']);
+
+ actionManager.destroy();
+ });
+
QUnit.test('footer buttons are moved to the dialog footer', function (assert) {
assert.expect(3);
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 1147c1398aa..0cf7c43618e 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -143,10 +143,12 @@ QUnit.module('relational_fields', {
user: {
fields: {
name: {string: "Name", type: "char"},
+ partner_ids: {string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id'},
},
records: [{
id: 17,
name: "Aline",
+ partner_ids: [1, 2],
}, {
id: 19,
name: "Christine",
@@ -1178,7 +1180,7 @@ QUnit.module('relational_fields', {
form.destroy();
});
- QUnit.test('list in form: discard newly added element with empty required field (default_get)', function (assert) {
+ QUnit.test('item not dropped on discard with empty required field (default_get)', function (assert) {
// This test simulates discarding a record that has been created with
// one of its required field that is empty. When we discard the changes
// on this empty field, it should not assume that this record should be
@@ -1322,7 +1324,7 @@ QUnit.module('relational_fields', {
form.destroy();
});
- QUnit.test('list in form: discard newly added element with empty required field (onchange in default_get)', function (assert) {
+ QUnit.test('list in form: item not dropped on discard with empty required field (onchange in default_get)', function (assert) {
// variant of the test "list in form: discard newly added element with
// empty required field (default_get)", in which the `default_get`
// performs an `onchange` at the same time. This `onchange` may create
@@ -1392,11 +1394,13 @@ QUnit.module('relational_fields', {
form.destroy();
});
- QUnit.test('list in form: discard newly added element with empty required field (onchange after default_get)', function (assert) {
+ QUnit.test('list in form: item not dropped on discard with empty required field (onchange on list after default_get)', function (assert) {
// discarding a record from an `onchange` in a `default_get` should not
- // abandon the record. However, any `onchange` after `default_get`
- // should be abandoned on discard
- assert.expect(6);
+ // abandon the record. This should not be the case for following
+ // `onchange`, except if an onchange make some changes on the list:
+ // in particular, if an onchange make changes on the list such that
+ // a record is added, this record should not be dropped on discard
+ assert.expect(8);
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
@@ -1448,10 +1452,127 @@ QUnit.module('relational_fields', {
// click off so that the required field still stay empty
$('body').click();
+ // record should not be dropped
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should not have dropped record in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should still have the correct displayed name");
+ assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
+ "should still have empty string in the required field");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('item dropped on discard with empty required field with "Add an item" (invalid on "ADD")', function (assert) {
+ // when a record in a list is added with "Add an item", it should
+ // always be dropped on discard if some required field are empty
+ // at the record creation.
+ assert.expect(6);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // Click on "Add an item"
+ form.$('.o_field_x2many_list_row_add a').click();
+ var charField = form.$('.o_field_widget.o_field_char[name="display_name"]');
+ var requiredField = form.$('.o_field_widget.o_required_modifier[name="trululu"]');
+ charField.val("some text");
+ assert.strictEqual(charField.length, 1,
+ "should have a char field 'display_name' on this record");
+ assert.notOk(charField.hasClass('o_required_modifier'),
+ "the char field should not be required on this record");
+ assert.strictEqual(charField.val(), "some text",
+ "should have entered text in the char field on this record");
+ assert.strictEqual(requiredField.length, 1,
+ "should have a required field 'trululu' on this record");
+ assert.strictEqual(requiredField.val().trim(), "",
+ "should have empty string in the required field on this record");
+
+ // click on empty required field in editable list record
+ requiredField.click();
+ // click off so that the required field still stay empty
+ $('body').click();
+
// record should be dropped
assert.strictEqual(form.$('.o_data_row').length, 0,
"should have dropped record in the editable list");
+ form.destroy();
+ });
+
+ QUnit.test('item not dropped on discard with empty required field with "Add an item" (invalid on "UPDATE")', function (assert) {
+ // when a record in a list is added with "Add an item", it should
+ // be temporarily added to the list when it is valid (e.g. required
+ // fields are non-empty). If the record is updated so that the required
+ // field is empty, and it is discarded, then the record should not be
+ // dropped.
+ assert.expect(8);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('.o_data_row').length, 0,
+ "should initially not have any record in the list");
+
+ // Click on "Add an item"
+ form.$('.o_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should have a temporary record in the list");
+
+ var $inputEditMode = form.$('.o_field_widget.o_required_modifier[name="trululu"] input');
+ assert.strictEqual($inputEditMode.length, 1,
+ "should have a required field 'trululu' on this record");
+ assert.strictEqual($inputEditMode.val(), "",
+ "should have empty string in the required field on this record");
+
+ // add something to required field and leave edit mode of the record
+ $inputEditMode.click();
+ $('li.ui-menu-item').first().click();
+ $('body').click(); // leave edit mode on the line
+
+ var $inputReadonlyMode = form.$('.o_data_cell.o_required_modifier');
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should not have dropped valid record when leaving edit mode");
+ assert.strictEqual($inputReadonlyMode.text(), "first record",
+ "should have put some content in the required field on this record");
+
+ // remove the required field and leave edit mode of the record
+ $('.o_data_row').click(); // enter edit mode on the line
+ $inputEditMode.click();
+ $inputEditMode.val("");
+ $('body').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should not have dropped record in the list on discard (invalid on UPDATE)");
+ assert.strictEqual($inputReadonlyMode.text(), "first record",
+ "should keep previous valid required field content on this record");
+
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
form.destroy();
});
@@ -9418,6 +9539,94 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('onchange affecting inline unopened list view', function (assert) {
+ // when we got onchange result for fields of record that were not
+ // already available because they were in a inline view not already
+ // opened, in a given configuration the change were applied ignoring
+ // existing data, thus a line of a one2many field inside a one2many
+ // field could be duplicated unexplectedly
+ assert.expect(5);
+
+ var numUserOnchange = 0;
+
+ this.data.user.onchanges = {
+ partner_ids: function (obj) {
+ if (numUserOnchange === 0) {
+ // simulate proper server onchange after save of modal with new record
+ obj.partner_ids = [
+ [5],
+ [1, 1, {
+ display_name: 'first record',
+ turtles: [
+ [5],
+ [1, 2, {'display_name': 'donatello'}],
+ ],
+ }],
+ [1, 2, {
+ display_name: 'second record',
+ turtles: [
+ [5],
+ obj.partner_ids[1][2].turtles[0],
+ ],
+ }],
+ ];
+ }
+ numUserOnchange++;
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'user',
+ data: this.data,
+ arch: '<form><sheet><group>' +
+ '<field name="partner_ids">' +
+ '<form>'+
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group></sheet></form>',
+ res_id: 17,
+ });
+
+ // add a turtle on second partner
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_data_row:eq(1)').click();
+ $('.modal .o_field_x2many_list_row_add a').click();
+ $('.modal input[name="display_name"]').val('michelangelo').change();
+ $('.modal .btn-primary').click();
+ // open first partner so changes from previous action are applied
+ form.$('.o_data_row:eq(0)').click();
+ $('.modal .btn-primary').click();
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(numUserOnchange, 2,
+ 'there should 2 and only 2 onchange from closing the partner modal');
+
+ form.$('.o_data_row:eq(0)').click();
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ 'only 1 turtle for first partner');
+ assert.strictEqual($('.modal .o_data_row').text(), 'donatello',
+ 'first partner turtle is donatello');
+ $('.modal .o_form_button_cancel').click();
+
+ form.$('.o_data_row:eq(1)').click();
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ 'only 1 turtle for second partner');
+ assert.strictEqual($('.modal .o_data_row').text(), 'michelangelo',
+ 'second partner turtle is michelangelo');
+ $('.modal .o_form_button_cancel').click();
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 18c82adb0a5..d8950ae44ab 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -1335,6 +1335,51 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('disable buttons until reload data from server', function (assert) {
+ assert.expect(2);
+ var def;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group><field name="foo"/></group>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ args.args[1].foo = "apple";
+ } else if (args.method === 'read') {
+ // Block the 'read' call
+ var result = this._super.apply(this, arguments);
+ return $.when(def).then(_.constant(result));
+ }
+ return this._super(route, args);
+ },
+ res_id: 2,
+ });
+
+ var def = $.Deferred();
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input').val("tralala").trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ // Save button should be disabled
+ assert.strictEqual(
+ form.$buttons.find('.o_form_button_save').attr('disabled'),
+ 'disabled', 'buttons should be disabled')
+
+ // Release the 'read' call
+ def.resolve();
+
+ // Edit button should be enabled after the reload
+ assert.strictEqual(
+ form.$buttons.find('.o_form_button_edit').attr('disabled'),
+ undefined, 'buttons should be enabled')
+
+ form.destroy();
+ });
+
QUnit.test('properly apply onchange in simple case', function (assert) {
assert.expect(2);