diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-13 13:32:51 +0000 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-13 13:32:51 +0000 |
| commit | 5a273e74f0fff18df5ab6cb240c9e260be4bb7ff (patch) | |
| tree | 4fbc7e142335d110fa0a984ff2c345a487e37ca4 /addons/web/static | |
| parent | dc5f83b63313018999b598210b972636986c5bea (diff) | |
| parent | fe59754c5201f9392b75d5d4cce8ca1400a7ef5d (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')
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'); } }, }, |
