summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-10-17 10:54:19 +0200
committerChristophe Simonis <chs@odoo.com>2018-10-17 10:54:19 +0200
commite9ae419fca0a9ebd0071c280bd98fc84f268bb74 (patch)
tree8f33c2a81a60133b04df10d7e81afbc9d54c20a6 /addons/web/static
parent156d099e328304d0f19146f4714d13b7b572358a (diff)
parent0de06a41326662d4505401764035bf9ddf31fc8b (diff)
[MERGE] forward port branch saas-11.3 up to 0de06a41326
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js103
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js4
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js64
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js4
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js2
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js1
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record.js6
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js6
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js122
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js84
-rw-r--r--addons/web/static/tests/helpers/mock_server.js4
-rw-r--r--addons/web/static/tests/helpers/test_utils.js6
-rw-r--r--addons/web/static/tests/views/calendar_tests.js8
-rw-r--r--addons/web/static/tests/views/kanban_model_tests.js7
-rw-r--r--addons/web/static/tests/views/kanban_tests.js77
-rw-r--r--addons/web/static/tests/views/list_tests.js24
16 files changed, 427 insertions, 95 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index c98d25989e9..9f2e9431bb1 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -434,6 +434,10 @@ var FieldDate = InputField.extend({
this._super.apply(this, arguments);
// use the session timezone when formatting dates
this.formatOptions.timezone = true;
+ this.datepickerOptions = _.defaults(
+ this.nodeOptions.datepicker || {},
+ {defaultDate: this.value}
+ );
},
/**
* In edit mode, instantiates a DateWidget datepicker and listen to changes.
@@ -498,13 +502,7 @@ var FieldDate = InputField.extend({
* @private
*/
_makeDatePicker: function () {
- return new datepicker.DateWidget(
- this,
- _.defaults(
- this.nodeOptions.datepicker || {},
- {defaultDate: this.value}
- )
- );
+ return new datepicker.DateWidget(this, this.datepickerOptions);
},
/**
@@ -522,6 +520,17 @@ var FieldDate = InputField.extend({
var FieldDateTime = FieldDate.extend({
supportedFieldTypes: ['datetime'],
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ if (this.value) {
+ var offset = this.getSession().getTZOffset(this.value);
+ var displayedValue = this.value.clone().add(offset, 'minutes');
+ this.datepickerOptions.defaultDate = displayedValue;
+ }
+ },
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -552,10 +561,8 @@ var FieldDateTime = FieldDate.extend({
* @private
*/
_makeDatePicker: function () {
- var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
- return new datepicker.DateTimeWidget(this, {defaultDate: value});
+ return new datepicker.DateTimeWidget(this, this.datepickerOptions);
},
-
/**
* Set the datepicker to the right value rather than the default one.
*
@@ -952,6 +959,9 @@ var HandleWidget = AbstractField.extend({
var FieldEmail = InputField.extend({
className: 'o_field_email',
+ events: _.extend({}, InputField.prototype.events, {
+ 'click': '_onClick',
+ }),
prefix: 'mailto',
supportedFieldTypes: ['char'],
@@ -992,7 +1002,21 @@ var FieldEmail = InputField.extend({
this.$el.text(this.value)
.addClass('o_form_uri o_text_overflow')
.attr('href', this.prefix + ':' + this.value);
- }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prevent the URL click from opening the record (when used on a list).
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClick: function (ev) {
+ ev.stopPropagation();
+ },
});
var FieldPhone = FieldEmail.extend({
@@ -1019,6 +1043,9 @@ var FieldPhone = FieldEmail.extend({
var UrlWidget = InputField.extend({
className: 'o_field_url',
+ events: _.extend({}, InputField.prototype.events, {
+ 'click': '_onClick',
+ }),
supportedFieldTypes: ['char'],
/**
@@ -1060,7 +1087,21 @@ var UrlWidget = InputField.extend({
.addClass('o_form_uri o_text_overflow')
.attr('target', '_blank')
.attr('href', this.value);
- }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prevent the URL click from opening the record (when used on a list).
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClick: function (ev) {
+ ev.stopPropagation();
+ },
});
var AbstractFieldBinary = AbstractField.extend({
@@ -1322,19 +1363,23 @@ var FieldPdfViewer = FieldBinaryFile.extend({
},
/**
* @private
+ * @param {string} [fileURI] file URI if specified
* @returns {string} the pdf viewer URI
*/
- _getURI: function () {
- var queryObj = {
- model: this.model,
- field: this.name,
- id: this.res_id,
- };
+ _getURI: function (fileURI) {
var page = this.recordData[this.name + '_page'] || 1;
- var queryString = $.param(queryObj);
- var url = encodeURIComponent('/web/image?' + queryString);
+ if (!fileURI) {
+ var queryObj = {
+ model: this.model,
+ field: this.name,
+ id: this.res_id,
+ };
+ var queryString = $.param(queryObj);
+ fileURI = '/web/image?' + queryString
+ }
+ fileURI = encodeURIComponent(fileURI);
var viewerURL = '/web/static/lib/pdfjs/web/viewer.html?file=';
- return viewerURL + url + '#page=' + page;
+ return viewerURL + fileURI + '#page=' + page;
},
/**
* @private
@@ -1378,14 +1423,16 @@ var FieldPdfViewer = FieldBinaryFile.extend({
*/
on_file_change: function (ev) {
this._super.apply(this, arguments);
+ var files = ev.target.files;
+ if (!files || files.length === 0) {
+ return;
+ }
+ // TOCheck: is there requirement to fallback on FileReader if browser don't support URL
+ var fileURI = URL.createObjectURL(files[0]);
if (this.PDFViewerApplication) {
- var files = ev.target.files;
- if (!files || files.length === 0) {
- return;
- }
- var file = files[0];
- // TOCheck: is there requirement to fallback on FileReader if browser don't support URL
- this.PDFViewerApplication.open(URL.createObjectURL(file), 0);
+ this.PDFViewerApplication.open(fileURI, 0);
+ } else {
+ this.$('.o_pdfview_iframe').attr('src', this._getURI(fileURI));
}
},
/**
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 25722cd3db9..7ae9e0addf3 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -2377,10 +2377,12 @@ var FieldSelectionBadge = FieldSelection.extend({
* a FieldMany2one for its value.
* Its intern representation is similar to the many2one (a datapoint with a
* `name_get` as data).
+ * Note that there is some logic to support char field because of one use in our
+ * codebase, but this use should be removed along with this note.
*/
var FieldReference = FieldMany2One.extend({
specialData: "_fetchSpecialReference",
- supportedFieldTypes: ['char', 'reference'],
+ supportedFieldTypes: ['reference'],
template: 'FieldReference',
events: _.extend({}, FieldMany2One.prototype.events, {
'change select': '_onSelectionChange',
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 10283c308f6..9b5dec6d169 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -904,27 +904,51 @@ var BasicModel = AbstractModel.extend({
route: '/web/dataset/resequence',
params: params,
})
- .then(function () {
- var offset = options.offset ? options.offset : 0;
- var old_data = data.data.slice();
- data.data = _.sortBy(data.data, function (d) {
- if (_.contains(resIDs, self.localData[d].res_id)) {
- return _.indexOf(resIDs, self.localData[d].res_id) + offset;
- } else {
- return _.indexOf(old_data, d);
- }
- });
- data.res_ids = [];
- _.each(data.data, function (d) {
- var dataPoint = self.localData[d];
- if (dataPoint.type === 'record') {
- data.res_ids.push(dataPoint.res_id);
- } else {
- data.res_ids = data.res_ids.concat(dataPoint.res_ids);
+ .then(function (wasResequenced) {
+ if (!wasResequenced) {
+ // the field on which the resequence was triggered does not
+ // exist, so no resequence happened server-side
+ return $.when();
+ }
+ var field = params.field ? params.field : 'sequence';
+
+ return self._rpc({
+ model: modelName,
+ method: 'read',
+ args: [resIDs, [field]],
+ }).then(function (records) {
+ if (data.data.length) {
+ var dataType = self.localData[data.data[0]].type;
+ if (dataType === 'record') {
+ _.each(data.data, function (dataPoint) {
+ var recordData = self.localData[dataPoint].data;
+ var inRecords = _.findWhere(records, {id: recordData.id});
+ if (inRecords) {
+ recordData[field] = inRecords[field];
+ }
+ });
+ data.data = _.sortBy(data.data, function (d) {
+ return self.localData[d].data[field];
+ });
+ }
+ if (dataType === 'list') {
+ data.data = _.sortBy(data.data, function (d) {
+ return _.indexOf(resIDs, self.localData[d].res_id)
+ });
+ }
}
- });
- self._updateParentResIDs(data);
- return parentID;
+ data.res_ids = [];
+ _.each(data.data, function (d) {
+ var dataPoint = self.localData[d];
+ if (dataPoint.type === 'record') {
+ data.res_ids.push(dataPoint.res_id);
+ } else {
+ data.res_ids = data.res_ids.concat(dataPoint.res_ids);
+ }
+ });
+ self._updateParentResIDs(data);
+ return parentID;
+ })
});
},
/**
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 2c4ed711223..e1befc9daf1 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -378,6 +378,10 @@ var BasicView = AbstractView.extend({
if (!(dependency_name in fields)) {
fields[dependency_name] = dependency_dict;
}
+
+ if (fv.fields && !(dependency_name in fv.fields)) {
+ fv.fields[dependency_name] = dependency_dict;
+ }
}
}
return false;
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 4b4050b67cc..06043f69846 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -77,7 +77,7 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
self.model.get(recordID),
{
mode: 'edit',
- can_create: false,
+ attrs: {can_create: false},
attrs: {
'placeholder': _.str.sprintf(_t("Add %s"), self.title),
},
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index f1e89698e97..46429189e12 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -948,7 +948,6 @@ var FormRenderer = BasicRenderer.extend({
*/
_onNavigationMove: function (ev) {
ev.stopPropagation();
-
var index;
if (ev.data.direction === "next") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target || ev.target);
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index 30bafaab117..654617c9847 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -450,10 +450,12 @@ var KanbanRecord = Widget.extend({
ischild = false;
}
var test_event = events && events.click && (events.click.length > 1 || events.click[0].namespace !== "tooltip");
+ var testLinkWithHref = elem.nodeName.toLowerCase() === 'a' && elem.href;
if (ischild) {
children.push(elem);
- if (test_event) {
- // do not trigger global click if one child has a click event registered
+ if (test_event || testLinkWithHref) {
+ // Do not trigger global click if one child has a click
+ // event registered (or it is a link with href)
trigger = false;
}
}
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index 8f87b36c874..d3f4d134229 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -22,8 +22,6 @@ var DateWidget = Widget.extend({
init: function(parent, options) {
this._super.apply(this, arguments);
- var l10n = _t.database.parameters;
-
this.name = parent.name;
this.options = _.defaults(options || {}, {
format : this.type_of_date === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat(),
@@ -51,12 +49,8 @@ var DateWidget = Widget.extend({
*/
start: function() {
this.$input = this.$('input.o_datepicker_input');
- this.$input.focus(function(e) {
- e.stopImmediatePropagation();
- });
this.$input.datetimepicker(this.options);
this.picker = this.$input.data('DateTimePicker');
- this.$input.click(this.picker.toggle.bind(this.picker));
this._setReadonly(false);
},
/**
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 6b828397ba8..34a2302cd18 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -10,6 +10,7 @@ var KanbanView = require('web.KanbanView');
var ListView = require('web.ListView');
var session = require('web.session');
var testUtils = require('web.test_utils');
+var field_registry = require('web.field_registry');
var createView = testUtils.createView;
var DebouncedField = basicFields.DebouncedField;
@@ -1653,6 +1654,49 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test("pdf_viewer: upload rendering", function (assert) {
+ assert.expect(6);
+
+ testUtils.patch(field_registry.map.pdf_viewer, {
+ on_file_change: function (ev) {
+ ev.target = {files: [new Blob()]};
+ this._super.apply(this, arguments);
+ },
+ _getURI: function (fileURI) {
+ var res = this._super.apply(this, arguments);
+ assert.step('_getURI');
+ assert.ok(_.str.startsWith(fileURI, 'blob:'));
+ this.PDFViewerApplication = {
+ open: function (URI) {
+ assert.step('open');
+ assert.ok(_.str.startsWith(URI, 'blob:'));
+ },
+ };
+ return 'about:blank';
+ },
+ });
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="document" widget="pdf_viewer"/>' +
+ '</form>',
+ });
+
+ // first upload initialize iframe
+ form.$('input[type="file"]').trigger('change');
+ assert.verifySteps(['_getURI']);
+ // second upload call pdfjs method inside iframe
+ form.$('input[type="file"]').trigger('change');
+ assert.verifySteps(['_getURI', 'open']);
+
+ testUtils.unpatch(field_registry.map.pdf_viewer);
+ form.destroy();
+ });
+
QUnit.test('text field rendering in list view', function (assert) {
assert.expect(1);
@@ -1852,6 +1896,40 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('image fields in x2many list are loaded correctly', function (assert) {
+ assert.expect(2);
+
+ this.data.partner_type.fields.image = {name: 'image', type: 'binary'};
+ this.data.partner_type.records[0].image = 'product_image';
+ this.data.partner.records[0].timmy = [12];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="image" widget="image"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === 'data:image/png;base64,product_image') {
+ assert.ok(true, "The list's image should have been fetched");
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ 'There should be one record in the many2many');
+
+ form.destroy();
+ });
+
QUnit.test('image fields with required attribute', function (assert) {
assert.expect(2);
@@ -2148,7 +2226,7 @@ QUnit.module('basic_fields', {
});
QUnit.test('date field value should not set on first click', function (assert) {
- assert.expect(2);
+ assert.expect(3);
var form = createView({
View: FormView,
@@ -2158,10 +2236,10 @@ QUnit.module('basic_fields', {
res_id: 4,
});
form.$buttons.find('.o_form_button_edit').click();
- form.$('.o_datepicker_input').click();
+ assert.strictEqual($('.bootstrap-datetimepicker-widget').length, 1, "there should be a datepicker (autofocus)");
assert.strictEqual(form.$('.o_datepicker_input').val(), '', "date field's input should be empty on first click");
$('.day:contains(22)').click();
- form.$('.o_datepicker_input').click(); // Open Datepicker second time
+ form.$('.o_datepicker_input').focus(); // Open Datepicker second time
assert.strictEqual($('.day.active').text(), '22', 'datepicker should be highlight with 22nd day of month');
form.destroy();
});
@@ -2199,8 +2277,7 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
'the date should be correct in edit mode');
- // click on the input and select another value
- form.$('.o_datepicker_input').click();
+ // select another value
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
assert.strictEqual($('.day.active').data('day'), '02/03/2017', 'datepicker should be highlight February 3');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
@@ -2283,8 +2360,7 @@ QUnit.module('basic_fields', {
assert.strictEqual(list.$('input.o_datepicker_input').val(), '02/03/2017',
'the date should be correct in edit mode');
- // click on the input and select another value
- list.$('input.o_datepicker_input').click();
+ // select another value
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -2401,8 +2477,7 @@ QUnit.module('basic_fields', {
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_datepicker_input').val(), expectedDateString,
'the datetime should be correct in edit mode');
- // click on the input and select 22 February at 8:23:33
- form.$('.o_datepicker_input').click();
+ // select 22 February at 8:23:33
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -2500,8 +2575,7 @@ QUnit.module('basic_fields', {
assert.strictEqual(list.$('input.o_datepicker_input').val(), expectedDateString,
'the date should be correct in edit mode');
- // click on the input and select 22 February at 8:23:33
- list.$('input.o_datepicker_input').click();
+ // select 22 February at 8:23:33
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -2659,6 +2733,32 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('datepicker option: daysOfWeekDisabled', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.datetime.default = "2017-08-02 12:00:05";
+ this.data.partner.fields.datetime.required = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="datetime" ' +
+ 'options=\'{"datepicker": {"daysOfWeekDisabled": [0, 6]}}\'/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_create').click();
+ assert.ok($('.day:last-child(),.day:nth-child(2)').hasClass('disabled'),
+ 'first and last days must be disabled');
+ assert.notOk($('.day:not(:last-child()):not(:nth-child(2))').hasClass('disabled'),
+ 'other days must stay clickable');
+
+ form.destroy();
+ });
+
QUnit.module('FieldMonetary');
QUnit.test('monetary field in form view', function (assert) {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 2763933bae7..20008a92c21 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -9819,6 +9819,37 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('click on URL should not open the record', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="display_name" widget="email"/>' +
+ '<field name="turtle_foo" widget="url"/>' +
+ '</tree>' +
+ '<form></form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ form.$('.o_email_cell a').click();
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ 'click should not open the modal');
+
+ form.$('.o_url_cell a').click();
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ 'click should not open the modal');
+ form.destroy();
+ });
+
QUnit.test('create and edit on m2o in o2m, and press ESCAPE', function (assert) {
assert.expect(4);
@@ -12952,6 +12983,59 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('when creating a new many2one on a x2many then discarding it immediately with ESCAPE, it should not crash', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ viewOptions: {
+ mode: 'edit',
+ },
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form><group><field name="foo"/><field name="bar"/></group></form>'
+ },
+ });
+
+ // add a new line
+ form.$el.find('.o_field_x2many_list_row_add>a').click();
+
+ // open the field turtle_trululu (one2many)
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+ form.$el.find('.o_input_dropdown>input').click();
+
+ concurrency.delay(0).then(function () {
+ // click create and edit
+ $('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter').click();
+
+ // hit escape immediately
+ var escapeKey = $.ui.keyCode.ESCAPE;
+ $(document.activeElement).trigger(
+ $.Event('keydown', {which: escapeKey, keyCode: escapeKey}));
+
+ assert.ok('did not crash');
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ done();
+ });
+ });
+
});
});
});
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 6a6dd62f2df..e0b59ae5119 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -904,10 +904,14 @@ var MockServer = Class.extend({
var offset = args.offset ? Number(args.offset) : 0;
var field = args.field ? args.field : 'sequence';
var records = this.data[args.model].records;
+ if (!(field in this.data[args.model].fields)) {
+ return false;
+ }
for (var i in args.ids) {
var record = _.findWhere(records, {id: args.ids[i]});
record[field] = Number(i) + offset;
}
+ return true;
},
/**
* Simulate a 'search_count' operation
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index beb39687364..db40c2b5049 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -218,6 +218,12 @@ function createAsyncView(params) {
controlPanel.appendTo($web_client);
var $content = $('<div>').addClass('o_content').appendTo($web_client);
+ if (params.interceptsPropagate) {
+ _.each(params.interceptsPropagate, function (cb, name) {
+ intercept(widget, name, cb, true);
+ });
+ }
+
return view.getController(widget).then(function (view) {
// override the view's 'destroy' so that it calls 'destroy' on the widget
// instead, as the widget is the parent of the view and the mockServer.
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index 0a7ca319c2f..71dd1e155c3 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -645,14 +645,14 @@ QUnit.module('Views', {
"should display the datetime from the date with the timezone");
// use datepicker to enter a date: 12/13/2016 08:00:00
- $('.o_field_widget[name="start"] input').trigger('click');
+ $('.o_field_widget[name="start"] input').trigger('focus');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click');
// use datepicker to enter a date: 12/13/2016 10:00:00
- $('.o_field_widget[name="stop"] input').trigger('click');
+ $('.o_field_widget[name="stop"] input').trigger('focus');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click');
@@ -894,14 +894,14 @@ QUnit.module('Views', {
"should display the datetime from the date with the timezone");
// use datepicker to enter a date: 12/13/2016 08:00:00
- $('.o_field_widget[name="start"] input').trigger('click');
+ $('.o_field_widget[name="start"] input').trigger('focus');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click');
// use datepicker to enter a date: 12/13/2016 10:00:00
- $('.o_field_widget[name="stop"] input').trigger('click');
+ $('.o_field_widget[name="stop"] input').trigger('focus');
$('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click');
$('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click');
diff --git a/addons/web/static/tests/views/kanban_model_tests.js b/addons/web/static/tests/views/kanban_model_tests.js
index 1a3391c720d..20eb2918d04 100644
--- a/addons/web/static/tests/views/kanban_model_tests.js
+++ b/addons/web/static/tests/views/kanban_model_tests.js
@@ -192,6 +192,8 @@ QUnit.module('Views', {
var done = assert.async();
assert.expect(8);
+ this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
+ this.data.partner.fields.sequence = {string: "Sequence", type: "integer"};
this.data.partner.records.push({id: 3, foo: 'aaa', product_id: 37});
var nbReseq = 0;
@@ -204,7 +206,7 @@ QUnit.module('Views', {
if (nbReseq === 1) { // resequencing columns
assert.deepEqual(args.ids, [41, 37],
"ids should be correct");
- assert.strictEqual(args.model, 'product_id',
+ assert.strictEqual(args.model, 'product',
"model should be correct");
} else if (nbReseq === 2) { // resequencing records
assert.deepEqual(args.ids, [3, 1],
@@ -212,7 +214,6 @@ QUnit.module('Views', {
assert.strictEqual(args.model, 'partner',
"model should be correct");
}
- return $.when();
}
return this._super.apply(this, arguments);
},
@@ -229,7 +230,7 @@ QUnit.module('Views', {
"first group should be res_id 37");
// resequence columns
- return model.resequence('product_id', [41, 37], stateID);
+ return model.resequence('product', [41, 37], stateID);
})
.then(function (stateID) {
var state = model.get(stateID);
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 7680d8877b1..9db68d8a26e 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -1335,6 +1335,72 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('Do not open record when clicking on `a` with `href`', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records = [
+ { id: 1, foo: 'yop' },
+ ];
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<field name="foo"/>' +
+ '<div>' +
+ '<a class="o_test_link" href="#">test link</a>' +
+ '</div>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>',
+ intercepts: {
+ // when clicking on a record in kanban view,
+ // it switches to form view.
+ switch_view: function () {
+ throw new Error("should not switch view");
+ },
+ },
+ });
+
+ var $record = kanban.$('.o_kanban_record:not(.o_kanban_ghost)');
+ assert.strictEqual($record.length, 1,
+ "should display a kanban record");
+
+ var $testLink = $record.find('a');
+ assert.strictEqual($testLink.length, 1,
+ "should contain a link in the kanban record");
+ assert.ok(!!$testLink[0].href,
+ "link inside kanban record should have non-empty href");
+
+ // Mocked views prevent accessing a link with href. This is intented
+ // most of the time, but not in this test which specifically needs to
+ // let the browser access a link with href.
+ kanban.$el.off('click', 'a');
+ // Prevent the browser default behaviour when clicking on anything.
+ // This includes clicking on a `<a>` with `href`, so that it does not
+ // change the URL in the address bar.
+ // Note that we should not specify a click listener on 'a', otherwise
+ // it may influence the kanban record global click handler to not open
+ // the record.
+ $(document.body).on('click.o_test', function (ev) {
+ assert.notOk(ev.isDefaultPrevented(),
+ "should not prevented browser default behaviour beforehand");
+ assert.strictEqual(ev.target, $testLink[0],
+ "should have clicked on the test link in the kanban record");
+ ev.preventDefault();
+ });
+
+ $testLink.click();
+
+ $(document.body).off('click.o_test');
+ kanban.destroy();
+ });
+
QUnit.test('can drag and drop a record from one column to the next', function (assert) {
assert.expect(9);
@@ -1731,7 +1797,7 @@ QUnit.module('Views', {
});
QUnit.test('delete a column in grouped on m2o', function (assert) {
- assert.expect(33);
+ assert.expect(36);
testUtils.patch(KanbanRenderer, {
_renderGrouped: function () {
@@ -2697,6 +2763,7 @@ QUnit.module('Views', {
QUnit.test('resequence columns in grouped by m2o', function (assert) {
assert.expect(7);
+ this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
var kanban = createView({
@@ -2710,12 +2777,6 @@ QUnit.module('Views', {
'</t></templates>' +
'</kanban>',
groupBy: ['product_id'],
- mockRPC: function (route) {
- if (route === '/web/dataset/resequence') {
- return $.when();
- }
- return this._super.apply(this, arguments);
- },
intercepts: {
env_updated: function (event) {
assert.deepEqual(event.data.env.ids, envIDs,
@@ -2739,7 +2800,7 @@ QUnit.module('Views', {
kanban.update({}, {reload: false}); // re-render without reloading
assert.strictEqual(kanban.$('.o_kanban_group:first').data('id'), 5,
- "first column should be id 5 before resequencing");
+ "first column should be id 5 after resequencing");
kanban.destroy();
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 9f476a6f1c9..05f7495e312 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -3085,10 +3085,10 @@ QUnit.module('Views', {
foo: {
fields: {int_field: {string: "int_field", type: "integer", sortable: true}},
records: [
- {id: 1, int_field: 0},
- {id: 2, int_field: 1},
- {id: 3, int_field: 2},
- {id: 4, int_field: 3},
+ {id: 1, int_field: 11},
+ {id: 2, int_field: 12},
+ {id: 3, int_field: 13},
+ {id: 4, int_field: 14},
]
}
};
@@ -3107,14 +3107,15 @@ QUnit.module('Views', {
assert.deepEqual(args, {
model: "foo",
ids: [4, 3],
- offset: 2,
+ offset: 13,
field: "int_field",
});
}
if (moves === 1) {
assert.deepEqual(args, {
model: "foo",
- ids: [1, 4, 2, 3],
+ ids: [4, 2],
+ offset: 12,
field: "int_field",
});
}
@@ -3122,14 +3123,15 @@ QUnit.module('Views', {
assert.deepEqual(args, {
model: "foo",
ids: [2, 4],
- offset: 1,
+ offset: 12,
field: "int_field",
});
}
if (moves === 3) {
assert.deepEqual(args, {
model: "foo",
- ids: [1, 4, 2, 3],
+ ids: [4, 2],
+ offset: 12,
field: "int_field",
});
}
@@ -3196,7 +3198,6 @@ QUnit.module('Views', {
"should write the right field as sequence");
assert.deepEqual(args.ids, [4, 2, 3],
"should write the sequence in correct order");
- return $.when();
}
return this._super.apply(this, arguments);
},
@@ -3326,13 +3327,16 @@ QUnit.module('Views', {
'</tree>',
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
+ var _super = this._super.bind(this);
assert.strictEqual(args.offset, 1,
"should write the sequence starting from the lowest current one");
assert.strictEqual(args.field, 'int_field',
"should write the right field as sequence");
assert.deepEqual(args.ids, [4, 2, 3],
"should write the sequence in correct order");
- return $.when(def);
+ return $.when(def).then(function () {
+ return _super(route, args);
+ });
}
return this._super.apply(this, arguments);
},