[IMP] web: onchange x2many with virtual id in create command

When an onchange modifies an x2many field, the line on which the user
works remains in editable mode and the cursor is  returned to the same
position.
The second argument of create command is a reference used to reactivate
the edited line.
This commit is contained in:
Christophe Matthieu
2017-08-30 14:39:17 +02:00
parent 6cf98c4c87
commit 2728fe1ec1
7 changed files with 235 additions and 23 deletions
+41 -1
View File
@@ -152,11 +152,33 @@ return {
}
return $to_detach.detach();
},
/**
* Returns the input or textarea cursor offset
*
* @param {Object} DOM item input or texteara
* @returns {Object}
*/
getCursor: function (node) {
node = $(node)[0];
var offset = 0;
var length = 0;
if ('selectionStart' in node) {
offset = node.selectionStart;
length = node.selectionEnd - offset;
} else if ('selection' in document) {
node.focus();
var sel = document.selection.createRange();
length = document.selection.createRange().text.length;
sel.moveStart('character', -node.value.length);
offset = sel.text.length - length;
}
return { offset: offset, length: length };
},
/**
* Returns the distance between a DOM element and the top-left corner of the
* window
*
* @param {element} [e] the DOM element
* @param {Object} node DOM element (input or texteara)
* @return {Object} the left and top distances in pixels
*/
getPosition: function (e) {
@@ -254,5 +276,23 @@ return {
}
return $container;
},
/**
* Moves the cursor position in a DOM element (input or texteara)
*
* @param {Object} node DOM element (input or textarea)
* @param {Int} pos cursor position
*/
setCursor: function (node, pos) {
node = $(node).first().focus()[0];
if ('createTextRange' in node) {
var textRange = node.createTextRange();
textRange.collapse(true);
textRange.moveEnd(pos);
textRange.moveStart(pos);
textRange.select();
} else if ('setSelectionRange' in node) {
node.setSelectionRange(pos, pos);
}
},
};
});
@@ -20,6 +20,7 @@ var ControlPanel = require('web.ControlPanel');
var dialogs = require('web.view_dialogs');
var core = require('web.core');
var data = require('web.data');
var dom = require('web.dom');
var Dialog = require('web.Dialog');
var KanbanRenderer = require('web.KanbanRenderer');
var ListRenderer = require('web.ListRenderer');
@@ -684,6 +685,7 @@ var FieldX2Many = AbstractField.extend({
if (command.operation === 'UPDATE' && command.data) {
var state = record.data[this.name];
var fieldNames = state.getFieldNames();
this._reset(record, ev);
this.renderer.confirmChange(state, command.id, fieldNames, ev.initialEvent);
return $.when();
}
@@ -895,11 +897,33 @@ var FieldX2Many = AbstractField.extend({
this.lastInitialEvent = undefined;
if (Object.keys(changes).length) {
this.lastInitialEvent = ev;
this._setValue({
// store the cursor position to restore it once potential onchanges have been applied
var self = this;
var editableID = this.renderer.getEditableRecordID();
var datapoint = _.find(this.value.data, {id: editableID});
var ref = datapoint && datapoint.ref;
var cursor = ref && dom.getCursor(ev.target.getFocusableElement());
var fieldName = ev.target.name;
var def = this._setValue({
operation: 'UPDATE',
id: ev.data.dataPointID,
data: changes,
});
if (ref) {
def.then(function () {
var nextEditableRecordID = self.renderer.getEditableRecordID();
if (nextEditableRecordID && editableID !== nextEditableRecordID) {
return;
}
var datapoint = _.find(self.value.data, {ref: ref});
if (datapoint) {
self.renderer.focusField(datapoint.id, fieldName, cursor && cursor.offset);
}
});
}
}
},
/**
@@ -92,10 +92,10 @@ var session = require('web.session');
var _t = core._t;
var x2ManyCommands = {
// (0, _, {values})
// (0, virtualID, {values})
CREATE: 0,
create: function (values) {
return [x2ManyCommands.CREATE, false, values];
create: function (virtualID, values) {
return [x2ManyCommands.CREATE, virtualID || false, values];
},
// (1, id, {values})
UPDATE: 1,
@@ -391,6 +391,7 @@ var BasicModel = AbstractModel.extend({
limit: element.limit,
model: element.model,
offset: element.offset,
ref: element.ref,
res_ids: element.res_ids.slice(0),
specialData: _.extend({}, element.specialData),
type: 'record',
@@ -1213,6 +1214,7 @@ var BasicModel = AbstractModel.extend({
modelName: list.model,
parentID: list.id,
viewType: list.viewType,
ref: command[1],
};
if (command[0] === 1) {
params.res_id = command[1];
@@ -2415,7 +2417,7 @@ var BasicModel = AbstractModel.extend({
commands[fieldName].push(x2ManyCommands.replace_with(realIDs));
_.each(relRecordCreated, function (relRecord) {
var changes = self._generateChanges(relRecord, options);
commands[fieldName].push(x2ManyCommands.create(changes));
commands[fieldName].push(x2ManyCommands.create(relRecord.ref, changes));
});
// generate update commands for records that have been
// updated (it may happen with editable lists)
@@ -2451,7 +2453,7 @@ var BasicModel = AbstractModel.extend({
// this is a new id
relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
changes = this._generateChanges(relRecord, options);
commands[fieldName].push(x2ManyCommands.create(changes));
commands[fieldName].push(x2ManyCommands.create(relRecord.ref, changes));
}
}
if (options.changesOnly && !didChange && addedIds.length === 0 && removedIds.length === 0) {
@@ -2824,6 +2826,7 @@ var BasicModel = AbstractModel.extend({
orderedBy: params.orderedBy || [],
parentID: params.parentID,
rawContext: params.rawContext,
ref: params.ref || res_id,
relationField: params.relationField,
res_id: res_id,
res_ids: res_ids,
@@ -10,7 +10,7 @@ odoo.define('web.BasicRenderer', function (require) {
var AbstractRenderer = require('web.AbstractRenderer');
var config = require('web.config');
var core = require('web.core');
var Domain = require('web.Domain');
var dom = require('web.dom');
var qweb = core.qweb;
@@ -115,6 +115,20 @@ var BasicRenderer = AbstractRenderer.extend({
return resetWidgets;
});
},
/**
* Activates the widget and move the cursor to the given offset
*
* @param {string} id
* @param {string} fieldName
* @param {integer} offset
*/
focusField: function (id, fieldName, offset) {
this.editRecord(id);
if (typeof offset === "number") {
var field = _.findWhere(this.allFieldWidgets[id], {name: fieldName});
dom.setCursor(field.getFocusableElement(), offset);
}
},
//--------------------------------------------------------------------------
// Private
@@ -1005,7 +1005,7 @@ QUnit.module('relational_fields', {
obj.timmy,
[
[6, false, []],
[0, false, {display_name: 'brandon is the new timmy', name: 'brandon'}]
[0, obj.timmy[1][1], {display_name: 'brandon is the new timmy', name: 'brandon'}]
],
"should have properly created the x2many command list");
obj.int_field = obj.timmy.length;
@@ -2077,7 +2077,7 @@ QUnit.module('relational_fields', {
"should send 46 commands (one for each record)");
assert.strictEqual(nbLinkCommands, 45,
"should send a LINK_TO command for each existing record");
assert.deepEqual(args.args[1].p[45], [0, false, {
assert.deepEqual(args.args[1].p[45], [0, args.args[1].p[45][1], {
display_name: 'new record',
}], "should sent a CREATE command for the new record");
break;
@@ -2095,10 +2095,10 @@ QUnit.module('relational_fields', {
assert.strictEqual(nbLinkCommands, 43,
"should send a LINK_TO command for each existing record");
assert.deepEqual(args.args[1].p[43],
[0, false, {display_name: 'new record page 1'}],
[0, args.args[1].p[43][1], {display_name: 'new record page 1'}],
"should sent correct CREATE command");
assert.deepEqual(args.args[1].p[44],
[0, false, {display_name: 'new record page 2'}],
[0, args.args[1].p[44][1], {display_name: 'new record page 2'}],
"should sent correct CREATE command");
assert.deepEqual(args.args[1].p[45],
[2, 11, false],
@@ -3294,7 +3294,7 @@ QUnit.module('relational_fields', {
mockRPC: function (method, args) {
if (args.method === 'write') {
assert.deepEqual(args.args[1].p, [
[0, false, {display_name: 'z'}],
[0, args.args[1].p[0][1], {display_name: 'z'}],
[2, 2, false],
], "correct commands should be sent");
}
@@ -3436,7 +3436,7 @@ QUnit.module('relational_fields', {
},
mockRPC: function (route, args) {
if (args.method === 'onchange') {
assert.deepEqual(args.args[1].p, [[4, 2, false], [0, false, {product_id: 41}]],
assert.deepEqual(args.args[1].p, [[4, 2, false], [0, args.args[1].p[1][1], {product_id: 41}]],
"should trigger onchange with correct parameters");
}
return this._super.apply(this, arguments);
@@ -3498,7 +3498,7 @@ QUnit.module('relational_fields', {
mockRPC: function (route, args) {
rpcCount++;
if (args.method === 'write') {
assert.deepEqual(args.args[1].p, [[0, false, {
assert.deepEqual(args.args[1].p, [[0, args.args[1].p[0][1], {
display_name: false, int_field: 123, product_id: 41
}]]);
}
@@ -3736,7 +3736,7 @@ QUnit.module('relational_fields', {
if (args.method === 'default_get') {
var expected = counter === 0 ?
[[4, 2, false]] :
[[4, 2, false], [0, false, {display_name: false, turtle_foo: 'hammer'}]];
[[4, 2, false], [0, args.kwargs.context.turtles[1][1], {display_name: false, turtle_foo: 'hammer'}]];
assert.deepEqual(args.kwargs.context.turtles, expected,
"should have properly evaluated turtles key in context");
counter++;
@@ -4387,6 +4387,137 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('onchange many2many in one2many list editable', function (assert) {
assert.expect(14);
this.data.product.records.push({
id: 1,
display_name: "xenomorphe",
});
this.data.turtle.onchanges = {
product_id: function (rec) {
if (rec.product_id) {
rec.partner_ids = [
[5],
[4, rec.product_id === 41 ? 1 : 2]
];
}
},
};
var partnerOnchange = function (rec) {
if (!rec.int_field || !rec.turtles.length) {
return;
}
rec.turtles = [
[5],
[0, 0, {
display_name: 'new line',
product_id: [37, 'xphone'],
partner_ids: [
[5],
[4, 1]
]
}],
[0, rec.turtles[0][1], {
display_name: rec.turtles[0][2].display_name,
product_id: [1, 'xenomorphe'],
partner_ids: [
[5],
[4, 2]
]
}],
];
};
this.data.partner.onchanges = {
int_field: partnerOnchange,
turtles: partnerOnchange,
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="int_field"/>' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'<field name="product_id"/>' +
'<field name="partner_ids" widget="many2many_tags"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</form>',
});
form.$('.o_field_x2many_list_row_add a').click();
form.$('input[name="display_name"]').val('first').trigger('input');
form.$('div[name="product_id"] input').click();
$('li.ui-menu-item a').click();
assert.strictEqual(form.$('.o_field_many2manytags.o_input').length, 1,
'should display the line in editable mode');
assert.strictEqual(form.$('.o_field_many2one input').val(), "xpad",
'should display the product');
assert.strictEqual(form.$('.o_field_many2manytags.o_input .o_badge_text').text(), "first record",
'should display the tag from the onchange');
form.$('input.o_field_integer[name="int_field"]').click();
assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xpad",
'should display the product xpad');
assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "first record",
'should display the tag in readonly');
form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input');
assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xphonexenomorphe",
'should display the product xphone and xenomorphe');
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
'should display the name, one2many and many2many value');
form.$('input.o_field_integer[name="int_field"]').val('0').trigger('input');
form.$('.o_list_record_delete:first span').click();
form.$('.o_list_record_delete:first span').click();
form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input');
form.$('.o_field_x2many_list_row_add a').click();
form.$('input[name="display_name"]').val('first').trigger('input');
form.$('div[name="product_id"] input').click();
$('li.ui-menu-item a').click();
assert.strictEqual(form.$('.o_field_many2manytags.o_input').length, 1,
'should display the line in editable mode');
assert.strictEqual(form.$('.o_field_many2one input').val(), "xenomorphe",
'should display the product xenomorphe');
assert.strictEqual(form.$('.o_field_many2manytags.o_input .o_badge_text').text(), "second record",
'should display the tag from the onchange');
form.$('input.o_field_integer[name="int_field"]').click();
assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xphonexenomorphe",
'should display the product xphone and xenomorphe');
assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "first recordsecond record",
'should display the tag in readonly (first record and second record)');
form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input');
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
'should display the name, one2many and many2many value');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
'should display the name, one2many and many2many value after save');
form.destroy();
});
QUnit.test('load view for x2many in one2many', function (assert) {
assert.expect(2);
@@ -1186,7 +1186,7 @@ QUnit.module('Views', {
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'onchange' && args.args[1].total === 150) {
assert.deepEqual(args.args[1].product_ids, [[0, false, {name: "xpod"}]],
assert.deepEqual(args.args[1].product_ids, [[0, args.args[1].product_ids[0][1], {name: "xpod"}]],
"Should have sent the create command in the onchange");
}
return this._super(route, args);
+6 -6
View File
@@ -1174,7 +1174,7 @@ QUnit.module('Views', {
display_name: "first record",
foo: "tralala",
id: 1,
p: [[0, false, {"display_name": "valid line", "qux": 12.4}]]
p: [[0, args.args[1].p[0][1], {"display_name": "valid line", "qux": 12.4}]]
}, "should send the values for the present fields");
}
return this._super(route, args);
@@ -3760,7 +3760,7 @@ QUnit.module('Views', {
mockRPC: function (route, args) {
if (args.method === 'create') {
var command = args.args[0].p;
assert.deepEqual(command, [[0, false, {
assert.deepEqual(command, [[0, command[0][1], {
display_name: false,
timmy: [[6, false, [12]]],
}]], "the default partner_type_id should be equal to 12");
@@ -3809,8 +3809,8 @@ QUnit.module('Views', {
mockRPC: function (route, args) {
if (args.method === 'create') {
assert.deepEqual(args.args[0].p,
[[0, false, {
p: [[0, false, {display_name: "xtv"}]],
[[0, args.args[0].p[0][1], {
p: [[0, args.args[0].p[0][2].p[0][1], {display_name: "xtv"}]],
}]],
"create should be called with the correct arguments");
}
@@ -5488,13 +5488,13 @@ QUnit.module('Views', {
"readonly fields value should be sent for onchanges");
} else { // onchange on field p
assert.deepEqual(args.args[1].p, [
[0, false, {display_name: 'readonly', foo: 'foo value'}]
[0, args.args[1].p[0][1], {display_name: 'readonly', foo: 'foo value'}]
], "readonly fields value should be sent for onchanges");
}
}
if (args.method === 'create') {
assert.deepEqual(args.args[0], {
p: [[0, false, {display_name: 'readonly'}]]
p: [[0, args.args[0].p[0][1], {display_name: 'readonly'}]]
}, "should not have sent the value of the readonly field");
}
return this._super.apply(this, arguments);