summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-09-13 13:32:51 +0000
committerChristophe Simonis <chs@odoo.com>2019-09-13 13:32:51 +0000
commit5a273e74f0fff18df5ab6cb240c9e260be4bb7ff (patch)
tree4fbc7e142335d110fa0a984ff2c345a487e37ca4 /addons/web/static
parentdc5f83b63313018999b598210b972636986c5bea (diff)
parentfe59754c5201f9392b75d5d4cce8ca1400a7ef5d (diff)
[MERGE] forward port branch saas-12.4 up to fe59754c520
closes odoo/odoo#36721 Signed-off-by: Christophe Simonis <chs@odoo.com>
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/js/core/abstract_storage_service.js13
-rw-r--r--addons/web/static/src/js/core/local_storage.js10
-rw-r--r--addons/web/static/src/js/core/ram_storage.js14
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js12
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js8
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js4
-rw-r--r--addons/web/static/src/js/views/form/form_view.js4
-rw-r--r--addons/web/static/src/js/views/graph/graph_view.js3
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_record.js8
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js2
-rw-r--r--addons/web/static/src/js/widgets/auto_complete.js12
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js5
-rw-r--r--addons/web/static/src/scss/ui.scss8
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js39
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js39
-rw-r--r--addons/web/static/tests/helpers/mock_server.js21
-rw-r--r--addons/web/static/tests/helpers/test_utils_create.js3
-rw-r--r--addons/web/static/tests/views/form_tests.js75
-rw-r--r--addons/web/static/tests/views/graph_tests.js21
-rw-r--r--addons/web/static/tests/views/kanban_tests.js55
-rw-r--r--addons/web/static/tests/views/list_tests.js89
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js6
22 files changed, 410 insertions, 41 deletions
diff --git a/addons/web/static/src/js/core/abstract_storage_service.js b/addons/web/static/src/js/core/abstract_storage_service.js
index ef828b5dd5c..5d724f87911 100644
--- a/addons/web/static/src/js/core/abstract_storage_service.js
+++ b/addons/web/static/src/js/core/abstract_storage_service.js
@@ -45,6 +45,19 @@ var AbstractStorageService = AbstractService.extend({
return val ? JSON.parse(val) : defaultValue;
},
/**
+ * @param {integer} index
+ * @return {string}
+ */
+ key: function (index) {
+ return this.storage.key(index);
+ },
+ /**
+ * @return {integer}
+ */
+ length: function () {
+ return this.storage.length;
+ },
+ /**
* Removes the given key from the storage
*
* @param {string} key
diff --git a/addons/web/static/src/js/core/local_storage.js b/addons/web/static/src/js/core/local_storage.js
index 0ae98498b31..ecc0cb7b78f 100644
--- a/addons/web/static/src/js/core/local_storage.js
+++ b/addons/web/static/src/js/core/local_storage.js
@@ -35,12 +35,10 @@ try {
var newValue = e.originalEvent.newValue;
try {
JSON.parse(newValue);
- if (localStorage.getItem(key) === newValue) {
- storage.trigger('storage', {
- key: key,
- newValue: newValue,
- });
- }
+ storage.trigger('storage', {
+ key: key,
+ newValue: newValue,
+ });
} catch (error) {}
});
return storage;
diff --git a/addons/web/static/src/js/core/ram_storage.js b/addons/web/static/src/js/core/ram_storage.js
index 9bf0fc29f93..abad86c4538 100644
--- a/addons/web/static/src/js/core/ram_storage.js
+++ b/addons/web/static/src/js/core/ram_storage.js
@@ -31,6 +31,7 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
*/
clear: function () {
this.storage = Object.create(null);
+ this.length = 0;
},
/**
* Returns the value associated with a given key in the storage
@@ -42,11 +43,21 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
return this.storage[key];
},
/**
+ * @param {integer} index
+ * @return {string}
+ */
+ key: function (index) {
+ return _.keys(this.storage)[index];
+ },
+ /**
* Removes the given key from the storage
*
* @param {string} key
*/
removeItem: function (key) {
+ if (key in this.storage) {
+ this.length--;
+ }
delete this.storage[key];
this.trigger('storage', {key: key, newValue: null});
},
@@ -58,6 +69,9 @@ var RamStorage = Class.extend(mixins.EventDispatcherMixin, {
* @param {string} value
*/
setItem: function (key, value) {
+ if (!(key in this.storage)) {
+ this.length++;
+ }
this.storage[key] = value;
this.trigger('storage', {key: key, newValue: value});
},
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index f4337d0c8f9..b2d3379aef1 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -2576,7 +2576,7 @@ var FieldSelection = AbstractField.extend({
description: _lt("Selection"),
template: 'FieldSelection',
specialData: "_fetchSpecialRelation",
- supportedFieldTypes: ['selection', 'many2one'],
+ supportedFieldTypes: ['selection'],
events: _.extend({}, AbstractField.prototype.events, {
'change': '_onChange',
}),
@@ -2629,10 +2629,14 @@ var FieldSelection = AbstractField.extend({
*/
_renderEdit: function () {
this.$el.empty();
+ var required = this.attrs.modifiersValue && this.attrs.modifiersValue.required;
for (var i = 0 ; i < this.values.length ; i++) {
+ var disabled = required && this.values[i][0] === false;
+
this.$el.append($('<option/>', {
value: JSON.stringify(this.values[i][0]),
- text: this.values[i][1]
+ text: this.values[i][1],
+ style: disabled ? "display: none" : "",
}));
}
var value = this.value;
@@ -2671,9 +2675,7 @@ var FieldSelection = AbstractField.extend({
return v[0] === false && v[1] === '';
});
}
- if (!this.attrs.modifiersValue || !this.attrs.modifiersValue.required) {
- this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
- }
+ this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
},
//--------------------------------------------------------------------------
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 614602a5791..2d239520dfb 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -2068,9 +2068,11 @@ var BasicModel = AbstractModel.extend({
if (field.onChange) {
hasOnchange = true;
}
- _.each(fieldInfo.views, function (view) {
- generateSpecs(view.fieldsInfo[view.type], view.fields, key + '.');
- });
+ if (field.type === 'one2many' || field.type === 'many2many') {
+ _.each(fieldInfo.views, function (view) {
+ generateSpecs(view.fieldsInfo[view.type], view.fields, key + '.');
+ });
+ }
});
}
return hasOnchange ? specs : false;
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 22de0e8d68b..bd31cddd50d 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -631,7 +631,9 @@ var FormController = BasicController.extend({
* @private
*/
_onEdit: function () {
- this._setMode('edit');
+ // wait for potential pending changes to be saved (done with widgets
+ // allowing to edit in readonly)
+ this.mutex.getUnlockedDef().then(this._setMode.bind(this, 'edit'));
},
/**
* This method is called when someone tries to freeze the order, most likely
diff --git a/addons/web/static/src/js/views/form/form_view.js b/addons/web/static/src/js/views/form/form_view.js
index 95b84423444..13d73cf37c2 100644
--- a/addons/web/static/src/js/views/form/form_view.js
+++ b/addons/web/static/src/js/views/form/form_view.js
@@ -123,6 +123,10 @@ var FormView = BasicView.extend({
var refinedContext = _.pick(self.loadParams.context, function (value, key) {
return key.indexOf('_view_ref') === -1;
});
+ // Specify the main model to prevent access rights defined in the context
+ // (e.g. create: 0) to apply to subviews. We use here the same logic as
+ // the one applied by the server for inline views.
+ refinedContext.base_model_name = self.controllerParams.modelName;
defs.push(parent.loadViews(
field.relation,
new Context(context, self.userContext, refinedContext).eval(),
diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js
index f8125675d4e..c3ad22f6ced 100644
--- a/addons/web/static/src/js/views/graph/graph_view.js
+++ b/addons/web/static/src/js/views/graph/graph_view.js
@@ -48,6 +48,9 @@ var GraphView = AbstractView.extend({
this.arch.children.forEach(function (field) {
var fieldName = field.attrs.name;
+ if (fieldName === "id") {
+ return;
+ }
var interval = field.attrs.interval;
if (interval) {
fieldName = fieldName + ':' + interval;
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 701180a357f..0c4213c60b7 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -70,12 +70,14 @@ var KanbanRecord = Widget.extend({
* Called each time the record is attached to the DOM.
*/
on_attach_callback: function () {
+ this.isInDOM = true;
_.invoke(this.subWidgets, 'on_attach_callback');
},
/**
* Called each time the record is detached from the DOM.
*/
on_detach_callback: function () {
+ this.isInDOM = false;
_.invoke(this.subWidgets, 'on_detach_callback');
},
@@ -256,6 +258,9 @@ var KanbanRecord = Widget.extend({
// a widget already exists for that field, so reset it with the new state
widget.reset(self.state);
$field.replaceWith(widget.$el);
+ if (self.isInDOM && widget.on_attach_callback) {
+ widget.on_attach_callback();
+ }
}
} else {
self._processField($field, field_name);
@@ -342,6 +347,9 @@ var KanbanRecord = Widget.extend({
*/
_render: function () {
this.defs = [];
+ // call 'on_detach_callback' on each subwidget as they will be removed
+ // from the DOM at the next line
+ _.invoke(this.subWidgets, 'on_detach_callback');
this._replaceElement(this.qweb.render('kanban-box', this.qweb_context));
this.$el.addClass('o_kanban_record').attr("tabindex", 0);
this.$el.attr('role', 'article');
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 1e109d7c0b8..5ec42156000 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -622,6 +622,7 @@ var ListController = BasicController.extend({
_onEditGroupClicked: function (ev) {
ev.stopPropagation();
this.do_action({
+ context: {create: false},
type: 'ir.actions.act_window',
views: [[false, 'form']],
res_model: ev.data.record.model,
@@ -704,6 +705,7 @@ var ListController = BasicController.extend({
* @param {OdooEvent} ev
*/
_onToggleGroup: function (ev) {
+ ev.stopPropagation();
var self = this;
this.model
.toggleGroup(ev.data.group.id)
diff --git a/addons/web/static/src/js/widgets/auto_complete.js b/addons/web/static/src/js/widgets/auto_complete.js
index 4da54cf8b5c..1bb00be8d38 100644
--- a/addons/web/static/src/js/widgets/auto_complete.js
+++ b/addons/web/static/src/js/widgets/auto_complete.js
@@ -140,12 +140,22 @@ return Widget.extend({
var $item = self.make_list_item(result).appendTo($list);
result.$el = $item;
});
+ // IE9 doesn't support addEventListener with option { once: true }
+ this.el.onmousemove = function (ev) {
+ self.$('li').each(function (index, li) {
+ li.onmouseenter = self.focus_element.bind(self, $(li));
+ });
+ var targetFocus = ev.target.tagName === 'LI' ?
+ ev.target :
+ ev.target.closest('li');
+ self.focus_element($(targetFocus));
+ self.el.onmousemove = null;
+ };
this.show();
},
make_list_item: function (result) {
var self = this;
var $li = $('<li>')
- .hover(function () {self.focus_element($li);})
.mousedown(function (ev) {
if (ev.button === 0) { // left button
self.select(ev, {item: {facet: result.facet}});
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index 5b4480838fe..ac8e83c8e30 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -92,8 +92,8 @@ var DateWidget = Widget.extend({
* set datetime value
*/
changeDatetime: function () {
+ var oldValue = this.getValue();
if (this.isValid()) {
- var oldValue = this.getValue();
this._setValueFromUi();
var newValue = this.getValue();
var hasChanged = !oldValue !== !newValue;
@@ -110,6 +110,9 @@ var DateWidget = Widget.extend({
}
this.trigger("datetime_changed");
}
+ } else {
+ var formattedValue = oldValue ? this._formatClient(oldValue) : null;
+ this.$input.val(formattedValue);
}
},
/**
diff --git a/addons/web/static/src/scss/ui.scss b/addons/web/static/src/scss/ui.scss
index 44d639805be..9ab1729a4ac 100644
--- a/addons/web/static/src/scss/ui.scss
+++ b/addons/web/static/src/scss/ui.scss
@@ -83,3 +83,11 @@
.o_debounce_disabled {
pointer-events: none;
}
+
+span.o_force_ltr {
+ display: inline-block;
+}
+.o_force_ltr, .o_field_phone {
+ /*rtl:ignore*/
+ direction: ltr;
+}
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index e8060e23e58..eca412c036a 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -2848,8 +2848,32 @@ QUnit.module('basic_fields', {
form.destroy();
});
- QUnit.test('date field should remove the date if the date is not valid', async function (assert) {
- assert.expect(1);
+ QUnit.test('date field: set an invalid date when the field is already set', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var $input = form.$('.o_field_widget[name=date] input');
+
+ assert.strictEqual($input.val(), "02/03/2017");
+
+ $input.val('mmmh').trigger('change');
+ assert.strictEqual($input.val(), "02/03/2017", "should have reset the original value");
+
+ form.destroy();
+ });
+
+ QUnit.test('date field: set an invalid date when the field is not set yet', async function (assert) {
+ assert.expect(2);
var form = await createView({
View: FormView,
@@ -2857,13 +2881,18 @@ QUnit.module('basic_fields', {
data: this.data,
arch: '<form string="Partners"><field name="date"/></form>',
res_id: 4,
+ viewOptions: {
+ mode: 'edit',
+ },
});
- // switch to edit mode
- await testUtils.form.clickEdit(form);
- // set an invalid date
+
var $input = form.$('.o_field_widget[name=date] input');
+
+ assert.strictEqual($input.text(), "");
+
$input.val('mmmh').trigger('change');
assert.strictEqual($input.text(), "", "The date field should be empty");
+
form.destroy();
});
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 7284b9099d8..7f3f54e3ad7 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -1191,7 +1191,7 @@ QUnit.module('relational_fields', {
});
QUnit.test('required selection widget should not have blank option', async function (assert) {
- assert.expect(3);
+ assert.expect(12);
this.data.partner.fields.feedback_value = {
type: "selection",
@@ -1212,15 +1212,38 @@ QUnit.module('relational_fields', {
});
await testUtils.form.clickEdit(form);
- assert.strictEqual(form.$('.o_field_widget[name=color]')[0].options.length, 3,
- "non required selection field must have 3 options 1 blank option and 2 value options");
- assert.strictEqual(form.$('.o_field_widget[name=feedback_value]')[0].options.length, 2,
- "should have only 2 options without blank option");
+
+ var $colorField = form.$('.o_field_widget[name=color]');
+ assert.containsN($colorField, 'option', 3, "Three options in non required field");
+
+ assert.hasAttrValue($colorField.find('option:first()'), 'style', "",
+ "Should not have display=none");
+ assert.hasAttrValue($colorField.find('option:eq(1)'), 'style', "",
+ "Should not have display=none");
+ assert.hasAttrValue($colorField.find('option:eq(2)'), 'style', "",
+ "Should not have display=none");
+
+ const $requiredSelect = form.$('.o_field_widget[name=feedback_value]');
+
+ assert.containsN($requiredSelect, 'option', 3, "Three options in required field");
+ assert.hasAttrValue($requiredSelect.find('option:first()'), 'style', "display: none",
+ "Should have display=none");
+ assert.hasAttrValue($requiredSelect.find('option:eq(1)'), 'style', "",
+ "Should not have display=none");
+ assert.hasAttrValue($requiredSelect.find('option:eq(2)'), 'style', "",
+ "Should not have display=none");
// change value to update widget modifier values
- await testUtils.fields.editSelect(form.$('.o_field_widget[name=feedback_value]'), '"bad"');
- assert.strictEqual(form.$('.o_field_widget[name=color]')[0].options.length, 2,
- "should have only 2 options");
+ await testUtils.fields.editSelect($requiredSelect, '"bad"');
+ $colorField = form.$('.o_field_widget[name=color]');
+
+ assert.containsN($colorField, 'option', 3, "Three options in required field");
+ assert.hasAttrValue($colorField.find('option:first()'), 'style', "display: none",
+ "Should have display=none");
+ assert.hasAttrValue($colorField.find('option:eq(1)'), 'style', "",
+ "Should not have display=none");
+ assert.hasAttrValue($colorField.find('option:eq(2)'), 'style', "",
+ "Should not have display=none");
form.destroy();
});
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index be6dd4c8e72..d77e8ceae79 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -85,7 +85,7 @@ var MockServer = Class.extend({
throw new Error('Model ' + model + ' was not defined in mock server data');
}
var fields = $.extend(true, {}, this.data[model].fields);
- var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context);
+ var fvg = this._fieldsViewGet(params.arch, model, fields, viewOptions.context || {});
if (toolbar) {
fvg.toolbar = toolbar;
}
@@ -214,6 +214,17 @@ var MockServer = Class.extend({
var inTreeView = (doc.tagName === 'tree');
+ // mock _postprocess_access_rights
+ const isBaseModel = !context.base_model_name || (model === context.base_model_name);
+ var views = ['kanban', 'tree', 'form', 'gantt', 'activity'];
+ if (isBaseModel && views.indexOf(doc.tagName) !== -1) {
+ for (let action of ['create', 'delete', 'edit', 'write']) {
+ if (!doc.getAttribute(action) && action in context && !context[action]) {
+ doc.setAttribute(action, 'false');
+ }
+ }
+ }
+
this._traverse(doc, function (node) {
if (node.nodeType === Node.TEXT_NODE) {
return false;
@@ -318,7 +329,7 @@ var MockServer = Class.extend({
relModel = field.relation;
relFields = $.extend(true, {}, self.data[relModel].fields);
field.views[children.tagName] = self._fieldsViewGet(children, relModel,
- relFields, context);
+ relFields, _.extend({}, context, {base_model_name: model}));
});
}
@@ -338,9 +349,9 @@ var MockServer = Class.extend({
node._isProcessed = true;
// postprocess simulation
field.views.groupby = self._fieldsViewGet(node, relModel, relFields, context);
- node.childNodes.forEach(function (child) {
- node.removeChild(child);
- });
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
});
var xmlSerializer = new XMLSerializer();
diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js
index b8f42236c24..892a946bba0 100644
--- a/addons/web/static/tests/helpers/test_utils_create.js
+++ b/addons/web/static/tests/helpers/test_utils_create.js
@@ -140,6 +140,7 @@ async function createView(params) {
_updateMVCParams: function () {
this._super.apply(this, arguments);
this.loadParams.groupedBy = params.groupBy || viewOptions.groupBy || [];
+ testUtilsMock.unpatch(View);
},
});
if ('hasSelectors' in params) {
@@ -178,8 +179,6 @@ async function createView(params) {
delete view.destroy;
widget.destroy();
$webClient.remove();
-
- testUtilsMock.unpatch(View);
};
// render the view in a fragment as they must be able to render correctly
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 8e4b2e82d02..16524ba171d 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -490,8 +490,10 @@ QUnit.module('Views', {
'The correct _view_ref should have been sent to the server, first time');
}
if (args.model === 'partner_type') {
- assert.deepEqual(context, {tree_view_ref: 'some_other_tree_view'},
- 'The correct _view_ref should have been sent to the server for the subview');
+ assert.deepEqual(context, {
+ base_model_name: 'product',
+ tree_view_ref: 'some_other_tree_view',
+ }, 'The correct _view_ref should have been sent to the server for the subview');
}
}
return this._super.apply(this, arguments);
@@ -4757,6 +4759,35 @@ QUnit.module('Views', {
});
});
+ QUnit.test('non inline subview and create=0 in action context', async function (assert) {
+ // the create=0 should apply on the main view (form), but not on subviews
+ assert.expect(2);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="product_ids" mode="kanban"/></form>',
+ archs: {
+ "product,false,kanban": `<kanban>
+ <templates><t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t></templates>
+ </kanban>`,
+ },
+ res_id: 1,
+ viewOptions: {
+ context: {create: false},
+ mode: 'edit',
+ },
+ });
+
+ assert.containsNone(form, '.o_form_button_create');
+ assert.containsOnce(form, '.o-kanban-button-new');
+
+ form.destroy();
+ });
+
QUnit.test('readonly fields with modifiers may be saved', async function (assert) {
// the readonly property on the field description only applies on view,
// this is not a DB constraint. It should be seen as a default value,
@@ -8032,6 +8063,46 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('edit a record in readonly and switch to edit before it is actually saved', async function (assert) {
+ assert.expect(3);
+
+ const prom = testUtils.makeTestPromise();
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form>
+ <field name="foo"/>
+ <field name="bar" widget="toggle_button"/>
+ </form>`,
+ mockRPC: function (route, args) {
+ const result = this._super.apply(this, arguments);
+ if (args.method === 'write') { // delay the write RPC
+ assert.deepEqual(args.args[1], {bar: false});
+ return prom.then(_.constant(result));
+ }
+ return result;
+ },
+ res_id: 1,
+ });
+
+ // edit the record (in readonly) with toogle_button widget (and delay the write RPC)
+ await testUtils.dom.click(form.$('.o_field_widget[name=bar]'));
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+
+ assert.hasClass(form.$('.o_form_view'), 'o_form_readonly'); // should wait for the RPC to return
+
+ // make write RPC return
+ prom.resolve();
+ await testUtils.nextTick();
+
+ assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
+
+ form.destroy();
+ });
+
QUnit.test('"bare" buttons in template should not trigger button click', async function (assert) {
assert.expect(3);
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
index f958924061b..7cbab784829 100644
--- a/addons/web/static/tests/views/graph_tests.js
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -176,6 +176,27 @@ QUnit.module('Views', {
graph.destroy();
});
+ QUnit.test('field id not in groupBy', async function (assert) {
+ assert.expect(1);
+
+ var graph = await createView({
+ View: GraphView,
+ model: "foo",
+ data: this.data,
+ arch: '<graph string="Partners">' +
+ '<field name="id"/>' +
+ '</graph>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ assert.deepEqual(args.kwargs.groupby, [],
+ 'groupby should not contain id field');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ graph.destroy();
+ });
+
QUnit.test('switching mode', async function (assert) {
assert.expect(6);
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index ca04c9413a8..fcb7df4ff29 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -1,6 +1,7 @@
odoo.define('web.kanban_tests', function (require) {
"use strict";
+var AbstractField = require('web.AbstractField');
var fieldRegistry = require('web.field_registry');
var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
var kanbanExamplesRegistry = require('web.kanban_examples_registry');
@@ -4595,6 +4596,60 @@ QUnit.module('Views', {
delete widgetRegistry.map.test;
});
+ QUnit.test('subwidgets with on_attach_callback when changing record color', async function (assert) {
+ assert.expect(3);
+
+ var counter = 0;
+ var MyTestWidget = AbstractField.extend({
+ on_attach_callback: function () {
+ counter++;
+ },
+ });
+ fieldRegistry.add('test_widget', MyTestWidget);
+
+ var kanban = await createView({
+ View: KanbanView,
+ model: 'category',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="color"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div color="color">' +
+ '<div class="o_dropdown_kanban dropdown">' +
+ '<a class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#">' +
+ '<span class="fa fa-bars fa-lg"/>' +
+ '</a>' +
+ '<ul class="dropdown-menu" role="menu">' +
+ '<li>' +
+ '<ul class="oe_kanban_colorpicker"/>' +
+ '</li>' +
+ '</ul>' +
+ '</div>' +
+ '<field name="name" widget="test_widget"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>',
+ });
+
+ // counter should be 2 as there are 2 records
+ assert.strictEqual(counter, 2, "on_attach_callback should have been called twice");
+
+ // set a color to kanban record
+ var $firstRecord = kanban.$('.o_kanban_record:first()');
+ testUtils.kanban.toggleRecordDropdown($firstRecord);
+ await testUtils.dom.click($firstRecord.find('.oe_kanban_colorpicker a.oe_kanban_color_9'));
+
+ // first record has replaced its $el with a new one
+ $firstRecord = kanban.$('.o_kanban_record:first()');
+ assert.hasClass($firstRecord, 'oe_kanban_color_9');
+ assert.strictEqual(counter, 3, "on_attach_callback method should be called 3 times");
+
+ delete fieldRegistry.map.test_widget;
+ kanban.destroy();
+ });
+
QUnit.test('column progressbars properly work', async function (assert) {
assert.expect(2);
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index e49bd499c42..c97b1916265 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -2879,6 +2879,7 @@ QUnit.module('Views', {
intercepts: {
do_action: function (event) {
assert.deepEqual(event.data.action, {
+ context: {create: false},
res_id: 2,
res_model: 'res_currency',
type: 'ir.actions.act_window',
@@ -2892,6 +2893,42 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('groupby node with subfields, and onchange', async function (assert) {
+ assert.expect(1);
+
+ this.data.foo.onchanges = {
+ foo: function () {},
+ };
+
+ const list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: `<tree editable="bottom" expand="1">
+ <field name="foo"/>
+ <field name="currency_id"/>
+ <groupby name="currency_id">
+ <field name="position" invisible="1"/>
+ </groupby>
+ </tree>`,
+ groupBy: ['currency_id'],
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.deepEqual(args.args[3], {
+ foo: "1",
+ currency_id: "",
+ }, 'onchange spec should not follow relation of many2one fields');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click(list.$('.o_data_row:first .o_data_cell:first'));
+ await testUtils.fields.editInput(list.$('.o_field_widget[name=foo]'), "new value");
+
+ list.destroy();
+ });
+
QUnit.test('list view, editable, without data', async function (assert) {
assert.expect(12);
@@ -4432,6 +4469,58 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('grouped list with another grouped list parent, click unfold', async function (assert) {
+ assert.expect(3);
+ this.data.bar.fields = {
+ cornichon: {string: 'cornichon', type: 'char'},
+ };
+
+ var rec = this.data.bar.records[0];
+ // create records to have the search more button
+ var newRecs = [];
+ for (var i=0; i<8; i++) {
+ var newRec = _.extend({}, rec);
+ newRec.id = 10 + i;
+ newRec.cornichon = 'extra fin';
+ newRecs.push(newRec);
+ }
+ this.data.bar.records = newRecs;
+
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="top"><field name="foo"/><field name="m2o"/></tree>',
+ groupBy: ['bar'],
+ archs: {
+ 'bar,false,list': '<tree><field name="cornichon"/></tree>',
+ 'bar,false,search': '<search><filter context="{\'group_by\': \'cornichon\'}" string="cornichon"/></search>',
+ },
+ });
+
+ await list.update({groupBy: []});
+
+ await testUtils.dom.click(list.$('.o_data_cell:eq(0)'));
+
+ await testUtils.dom.click(list.$('.o_selected_row .o_data_cell .o_field_many2one input'));
+ await testUtils.dom.triggerEvents($('.ui-autocomplete a:contains(Search More)'),
+ ['mouseenter', 'click']);
+
+ assert.containsOnce($('body'), '.modal-content');
+
+ assert.containsNone($('body'), '.modal-content .o_group_name', 'list in modal not grouped');
+
+ await testUtils.dom.click($('body .modal-content button:contains(Group By)'));
+
+ await testUtils.dom.click($('body .modal-content .o_menu_item a:contains(cornichon)'));
+
+ await testUtils.dom.click($('body .modal-content .o_group_header'));
+
+ assert.containsOnce($('body'), '.modal-content .o_group_open');
+
+ list.destroy();
+ });
+
QUnit.test('field values are escaped', async function (assert) {
assert.expect(1);
var value = '<script>throw Error();</script>';
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index 558caeeaf37..b68d676a125 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -411,8 +411,10 @@ QUnit.module('Views', {
'The correct _view_ref should have been sent to the server, first time');
}
if (ev.data.modelName === 'badassery') {
- assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_other_tree_view'},
- 'The correct _view_ref should have been sent to the server for the subview');
+ assert.deepEqual(evaluatedContext, {
+ base_model_name: 'instrument',
+ tree_view_ref: 'some_other_tree_view',
+ }, 'The correct _view_ref should have been sent to the server for the subview');
}
},
},