From 98142f731d38aecf87d7e7900e6eeff8c047a9eb Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 2 Jun 2017 11:03:40 +0200 Subject: [PATCH 01/40] [FIX] web: BasicModel: default_get: fetch x2manys of x2manys Suppose that a default_get returns the value [[6, 0, [1]] for an x2many field (i.e. put id 1 in the relation). A read is performed to fetch record of the comodel whose id is 1. Suppose that there is another x2many field of the comodel. This x2many has to be fetched (if not empty), and processed by the BasicModel (i.e. a dataPoint must be created for it) as well. Before this rev., the second x2many wasn't fetched nor processed, and it produced a traceback for example in Expenses app, when clicking on 'Submit to manager' from an expense form view. --- .../static/src/js/views/basic/basic_model.js | 6 ++- .../static/tests/views/basic_model_tests.js | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) 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 60abfde4344..61c285d6581 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -1841,7 +1841,6 @@ var BasicModel = AbstractModel.extend({ return d.res_id === res_id; }); m2mList.data.push(dataPoint.id); - m2mList.count++; }); }); }); @@ -2476,7 +2475,10 @@ var BasicModel = AbstractModel.extend({ // REPLACE_WITH x2manyList.res_ids = value[2]; x2manyList.count = x2manyList.res_ids.length; - defs.push(self._readUngroupedList(x2manyList)); + var def = self._readUngroupedList(x2manyList).then(function () { + return self._fetchX2ManysBatched(x2manyList); + }); + defs.push(def); } }); diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js index 582e06ba62b..fefc7231e5b 100644 --- a/addons/web/static/tests/views/basic_model_tests.js +++ b/addons/web/static/tests/views/basic_model_tests.js @@ -1165,6 +1165,54 @@ QUnit.module('Views', { model.destroy(); }); + QUnit.test('default_get: fetch x2manys inside x2manys', function (assert) { + assert.expect(3); + + this.data.partner.fields.o2m = { + string: "O2M", type: 'one2many', relation: 'partner', default: [[6, 0, [1]]], + }; + + var model = createModel({ + Model: BasicModel, + data: this.data, + }); + + var params = { + fieldNames: ['o2m'], + fields: this.data.partner.fields, + fieldsInfo: { + form: { + o2m: { + relatedFields: this.data.partner.fields, + fieldsInfo: { + list: { + category: { + relatedFields: { display_name: {} }, + }, + }, + }, + viewType: 'list', + }, + }, + }, + modelName: 'partner', + type: 'record', + viewType: 'form', + }; + + model.load(params).then(function (resultID) { + var record = model.get(resultID); + assert.strictEqual(record.data.o2m.count, 1, "o2m field should contain 1 record"); + var categoryList = record.data.o2m.data[0].data.category; + assert.strictEqual(categoryList.count, 1, + "category field should contain 1 record"); + assert.strictEqual(categoryList.data[0].data.display_name, + 'gold', "category records should have been fetched"); + }); + + model.destroy(); + }); + QUnit.test('contexts and domains can be properly fetched', function (assert) { assert.expect(8); @@ -1448,6 +1496,7 @@ QUnit.module('Views', { fieldsInfo: { form: { product_ids: { + relatedFields: this.data.product.fields, fieldsInfo: { list: { name: {}, date: {} } }, viewType: 'list', } From e9de309511d64897444645f8a9ea4bf701709bb8 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 2 Jun 2017 14:28:07 +0200 Subject: [PATCH 02/40] [FIX] web: editable list: setRowMode returns a deferred This function should return a deferred, as indicated in the docstring. However, in some cases, it doesn't. It then crashed as the FieldX2Many tries to call done() on undefined. This happens for instance on the customer invoice form view: create a new record, select a partner, add a line, select a product, directly click on the invoice date field (this will trigger an onchange which will totally override the value of the one2many). The FielX2Many calls setRowMode to switch the edited row back to readonly, but this row doesn't exist anymore as the one2many value has been overriden by the onchange, so setRowMode wrongly returns nothing, and it crashes. --- .../js/views/list/list_editable_renderer.js | 2 +- .../tests/fields/relational_fields_tests.js | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js index 9364d1ba9fe..08b9e7f1602 100644 --- a/addons/web/static/src/js/views/list/list_editable_renderer.js +++ b/addons/web/static/src/js/views/list/list_editable_renderer.js @@ -155,7 +155,7 @@ ListRenderer.include({ var self = this; var rowIndex = _.findIndex(this.state.data, {id: recordID}); if (rowIndex < 0) { - return; + return $.when(); } var editMode = (mode === 'edit'); var record = this.state.data[rowIndex]; diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 4f326e14f98..1c2a0ab86d3 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -3852,6 +3852,61 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable list: value reset by an onchange', function (assert) { + // this test reproduces a subtle behavior that may occur in a form view: + // the user adds a record in a one2many field, and directly clicks on a + // datetime field of the form view which has an onchange, which totally + // overrides the value of the one2many (commands 5 and 0). The handler + // that switches the edited row to readonly is then called after the + // new value of the one2many field is applied (the one returned by the + // onchange), so the row that must go to readonly doesn't exist anymore. + assert.expect(2); + + this.data.partner.onchanges = { + datetime: function (obj) { + obj.turtles = [[5], [0, 0, {display_name: 'new'}]]; + }, + }; + + var def; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return $.when(def).then(_.constant(result)); + } + return result; + }, + }); + + // trigger the two onchanges + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_data_row .o_field_widget').val('a name').trigger('input'); + def = $.Deferred(); + form.$('.o_datepicker_input').click(); // focusout o2m and set value to today + + // resolve the onchange def + def.resolve(); + + assert.strictEqual(form.$('.o_data_row').length, 1, + "should have one record in the o2m"); + assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'new', + "should be the record created by the onchange"); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { From 67747e6fa5f4c1b27eed5dfd7231001e98fb34c1 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Fri, 2 Jun 2017 15:35:29 +0200 Subject: [PATCH 03/40] [FIX] web: FieldOne2Many: re-activate 'Add an item' The 'Add an item' button of one2many editable lists is deactivated once clicked (since rev. f9a1241) to prevent concurrent record creation. It is re-activated when the record has been created. Unfortunately, it might happen that the record is actually not created, so in this case the button is never re-activated. This happens for example on the customer invoice form view, if the user tries to add a line before selecting a partner. --- .../static/src/js/fields/abstract_field.js | 11 ++-- .../static/src/js/fields/relational_fields.js | 5 +- .../src/js/views/field_manager_mixin.js | 4 +- .../tests/fields/relational_fields_tests.js | 54 +++++++++++++++++++ 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 5908f7f5a8b..137d2954993 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -351,12 +351,13 @@ var AbstractField = Widget.extend({ * @param {Object} [options] * @param {boolean} [options.forceChange=false] if true, the change event will be * triggered even if the new value is the same as the old one + * @returns {Deferred} */ _setValue: function (value, options) { // we try to avoid doing useless work, if the value given has not // changed. Note that we compare the unparsed values. if (this.lastSetValue === value || (this.value === false && value === '')) { - return; + return $.when(); } this.lastSetValue = value; try { @@ -364,18 +365,22 @@ var AbstractField = Widget.extend({ this._isValid = true; } catch (e) { this._isValid = false; - return; + return $.Deferred().reject(); } if (!(options && options.forceChange) && this._isSameValue(value)) { - return; + return $.when(); } + var def = $.Deferred(); var changes = {}; changes[this.name] = value; this.trigger_up('field_changed', { dataPointID: this.dataPointID, changes: changes, viewType: this.viewType, + onSuccess: def.resolve.bind(def), + onFailure: def.reject.bind(def), }); + return def; }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index e13c3d29004..e9af73c1343 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -964,7 +964,6 @@ var FieldOne2Many = FieldX2Many.extend({ var index = self.editable === 'top' ? 0 : self.value.data.length - 1; var newID = self.value.data[index].id; self.renderer.editRecord(newID); - self.creatingRecord = false; } } }); @@ -1014,6 +1013,7 @@ var FieldOne2Many = FieldX2Many.extend({ * in the kanban view */ _onAddRecord: function (ev) { + var self = this; // we don't want interference with the components upstream. ev.stopPropagation(); @@ -1023,10 +1023,11 @@ var FieldOne2Many = FieldX2Many.extend({ this._setValue({ operation: 'CREATE', position: this.editable, + }).always(function () { + self.creatingRecord = false; }); } } else { - var self = this; this._openFormDialog({ on_saved: function (record) { self._setValue({ operation: 'ADD', id: record.id }); diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js index 9578e568558..422af121d96 100644 --- a/addons/web/static/src/js/views/field_manager_mixin.js +++ b/addons/web/static/src/js/views/field_manager_mixin.js @@ -99,7 +99,9 @@ var FieldManagerMixin = { // subrecord's form view), otherwise it bubbles up to the main form view // but its model doesn't have any data related to the given dataPointID event.stopPropagation(); - this._applyChanges(event.data.dataPointID, event.data.changes, event); + this._applyChanges(event.data.dataPointID, event.data.changes, event) + .done(event.data.onSuccess || function () {}) + .fail(event.data.onFailure || function () {}); }, /** * Some widgets need to trigger a reload of their data. For example, a diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 1c2a0ab86d3..09b22baa02f 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -3907,6 +3907,60 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable list: onchange that returns a warning', function (assert) { + assert.expect(5); + + this.data.turtle.onchanges = { + display_name: function () {}, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'onchange') { + assert.step(args.method); + return $.when({ + value: {}, + warning: { + title: "Warning", + message: "You must first select a partner" + }, + }); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + mode: 'edit', + }, + intercepts: { + warning: function () { + assert.step('warning'); + }, + }, + }); + + // add a line (this should trigger an onchange and a warning) + form.$('.o_field_x2many_list_row_add a').click(); + + // check if 'Add an item' still works (this should trigger an onchange + // and a warning again) + form.$('.o_field_x2many_list_row_add a').click(); + + assert.verifySteps(['onchange', 'warning', 'onchange', 'warning']); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { From 25e701465081477a1bd31f448c1c4feb533f4a09 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 6 Jun 2017 09:55:56 +0200 Subject: [PATCH 04/40] [FIX] web: Form: correctly autofocus fields The autofocus worked fine when switching from 'readonly' to 'edit', but not when clicking on 'Create' from the multi-record view. This was because in this case, we were trying to focus the field while the view wasn't in the DOM yet. This rev. ensures that we wait for the view to be appended to the DOM before trying to set the focus on the default field. Also ensures that the first visible field is correctly focused. Before this rev., if the first field was invisible (or simply not focusable), no field was focused. Credit goes to @msh-odoo for the preliminary work. --- .../static/src/js/fields/abstract_field.js | 13 +++++- .../src/js/views/form/form_controller.js | 14 +++++-- .../static/src/js/views/form/form_renderer.js | 33 +++++++++++---- .../web/static/src/js/views/view_dialogs.js | 13 ++++-- addons/web/static/tests/helpers/test_utils.js | 10 ++++- addons/web/static/tests/views/form_tests.js | 40 ++++++++++++++++++- 6 files changed, 104 insertions(+), 19 deletions(-) diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 137d2954993..492d9f26552 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -176,8 +176,8 @@ var AbstractField = Widget.extend({ * focusable element was not found or invisible */ activate: function (options) { - var $focusable = this.getFocusableElement(); - if ($focusable.length && $focusable.is(':visible')) { + if (this.isFocusable()) { + var $focusable = this.getFocusableElement(); $focusable.focus(); if ($focusable.is('input[type="text"], textarea')) { $focusable[0].selectionStart = $focusable[0].selectionEnd = $focusable[0].value.length; @@ -211,6 +211,15 @@ var AbstractField = Widget.extend({ getFocusableElement: function () { return $(); }, + /** + * Returns true iff the widget has a visible element that can take the focus + * + * @returns {boolean} + */ + isFocusable: function () { + var $focusable = this.getFocusableElement(); + return $focusable.length && $focusable.is(':visible'); + }, /** * this method is used to determine if the field value is set to a meaningful * value. This is useful to determine if a field should be displayed as empty 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 6adb474303d..464cd06e44d 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -39,12 +39,10 @@ var FormController = BasicController.extend({ //-------------------------------------------------------------------------- /** - * This method is supposed to focus the first active control, I think. It - * is currently only called by the FormViewDialog. - * - * @todo To be implemented + * Calls autofocus on the renderer */ autofocus: function () { + this.renderer.autofocus(); }, /** * This method switches the form view in edit mode, with a new record. @@ -92,6 +90,14 @@ var FormController = BasicController.extend({ var dataPoint = this.model.get(this.handle, {raw: true}); return dataPoint.data.display_name || _t('New'); }, + /** + * Called each time the form view is attached into the DOM + * + * @todo convert to new style + */ + on_attach_callback: function () { + this.autofocus(); + }, /** * Render buttons for the control panel. The form view can be rendered in * a dialog, and in that case, if we have buttons defined in the footer, we 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 ddd77ee5dc6..b15e6b80131 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -25,6 +25,29 @@ var FormRenderer = BasicRenderer.extend({ // Public //-------------------------------------------------------------------------- + /** + * Focuses the field having attribute 'default_focus' set, if any, or the + * first focusable field otherwise. + */ + autofocus: function () { + if (this.mode === 'readonly') { + return; + } + var focusWidget = this.defaultFocusField; + if (!focusWidget || !focusWidget.isFocusable()) { + var widgets = this.allFieldWidgets[this.state.id]; + for (var i = 0; i < (widgets ? widgets.length : 0); i++) { + var widget = widgets[i]; + if (widget.isFocusable()) { + focusWidget = widget; + break; + } + } + } + if (focusWidget) { + focusWidget.activate({noselect: true}); + } + }, /** * Extend the method so that labels also receive the 'o_field_invalid' class * if necessary. @@ -769,20 +792,16 @@ var FormRenderer = BasicRenderer.extend({ core.bus.trigger('DOM_updated'); // Attach the tooltips on the fields' label - var focusWidget = this.defaultFocusField; _.each(this.allFieldWidgets[this.state.id], function (widget) { - if (!focusWidget) { - focusWidget = widget; - } if (core.debug || widget.attrs.help || widget.field.help) { var idForLabel = self.idsForLabels[widget.name]; var $label = idForLabel ? self.$('label[for=' + idForLabel + ']') : $(); self._addFieldTooltip(widget, $label); } }); - if (focusWidget) { - focusWidget.activate({noselect: true}); - } + + // Focus the default field (in edit mode) + self.autofocus(); }, /** * Sets id attribute of given widget to idForLabel diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index b84d60d5b7c..a19e440913e 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -4,6 +4,7 @@ odoo.define('web.view_dialogs', function (require) { var core = require('web.core'); var data = require('web.data'); var Dialog = require('web.Dialog'); +var dom = require('web.dom'); var ListController = require('web.ListController'); var ListView = require('web.ListView'); var pyeval = require('web.pyeval'); @@ -194,8 +195,10 @@ var FormViewDialog = ViewDialog.extend({ if ($buttons.children().length) { self.$footer.empty().append($buttons.contents()); } - _super().$el.append(fragment); - self.form_view.autofocus(); + dom.append(_super().$el, fragment, { + callbacks: [{widget: self.form_view}], + in_DOM: true, + }); }); }); @@ -281,6 +284,7 @@ var SelectCreateDialog = ViewDialog.extend({ if(this.options.initial_view !== "search") { return this.create_edit_record(); } + var self = this; var user_context = this.getSession().user_context; var _super = this._super.bind(this); @@ -298,7 +302,10 @@ var SelectCreateDialog = ViewDialog.extend({ this.loadViews(this.dataset.model, this.dataset.get_context(), [[false, 'list'], [false, 'search']], {}) .then(this.setup.bind(this, search_defaults)) .then(function (fragment) { - _super().$el.append(fragment); + dom.append(_super().$el, fragment, { + callbacks: [{widget: self.list_controller}], + in_DOM: true, + }); }); return this; }, diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index eae31e68e80..3632e251a33 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -13,6 +13,7 @@ odoo.define('web.test_utils', function (require) { var basic_fields = require('web.basic_fields'); var config = require('web.config'); var core = require('web.core'); +var dom = require('web.dom'); var session = require('web.session'); var MockServer = require('web.MockServer'); var Widget = require('web.Widget'); @@ -172,7 +173,14 @@ function createAsyncView(params) { widget.destroy(); $('#qunit-fixture').off('DOMNodeInserted.removeSRC'); }; - return view.appendTo($view_manager).then(function () { + // render the view in a fragment as they must be able to render correctly + // without being in the DOM + var fragment = document.createDocumentFragment(); + return view.appendTo(fragment).then(function () { + dom.append($view_manager, fragment, { + callbacks: [{widget: view}], + in_DOM: true, + }); view.$el.on('click', 'a', function (ev) { ev.preventDefault(); }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 5f7f34a2cc7..2d23f4a9cb4 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -3736,7 +3736,7 @@ QUnit.module('Views', { }); form.$buttons.find('.o_form_button_edit').click(); - assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement, + assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0], "foo field should have focus"); assert.strictEqual(form.$('input[name="foo"]')[0].selectionStart, 3, "cursor should be at the end"); @@ -3758,7 +3758,25 @@ QUnit.module('Views', { res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); - assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement, + assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0], + "foo field should have focus"); + + form.destroy(); + }); + + QUnit.test('in create mode, autofocus fields are focused', function (assert) { + assert.expect(1); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '', + }); + assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0], "foo field should have focus"); form.destroy(); @@ -3787,6 +3805,24 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('autofocus first visible field', function (assert) { + assert.expect(1); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '' + + '' + + '', + }); + assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0], + "foo field should have focus"); + + form.destroy(); + }); + QUnit.test('open one2many form containing many2many_tags', function (assert) { assert.expect(4); From cb9a8bdc79ef6bd6a9fbd43afc73e14825bc3923 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 6 Jun 2017 15:30:20 +0200 Subject: [PATCH 05/40] [FIX] web: ModelFieldSelector: less tweaks --- addons/web/static/src/less/model_field_selector.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/web/static/src/less/model_field_selector.less b/addons/web/static/src/less/model_field_selector.less index 124c15cb8e5..aee5bd17867 100644 --- a/addons/web/static/src/less/model_field_selector.less +++ b/addons/web/static/src/less/model_field_selector.less @@ -7,6 +7,10 @@ .o-flex-flow(row, wrap); .o-align-items(center); height: 100%; + min-height: 20px; // needed when there is no value in it and used standalone + &:active, &:focus, &:active:focus { + outline: none; + } > .o_field_selector_chain_part { padding: 0px 1px; From 5aa1cf189039f4bbfdb5f6f66a1c1604c8bb243b Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 7 Jun 2017 08:28:00 +0200 Subject: [PATCH 06/40] [FIX] web: re-introduce wow effect When clicking on buttons with classname o_wow (e.g. when marking an opportunity as won). --- .../src/js/chrome/abstract_web_client.js | 17 +++++++++ .../src/js/views/form/form_controller.js | 6 ++-- .../static/src/js/views/form/form_renderer.js | 2 +- addons/web/static/tests/views/form_tests.js | 35 +++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index 155690f9663..9efb2a3c937 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -81,6 +81,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, { } }); }, + show_wow: '_onShowWow', }, init: function (parent) { this.client_options = {}; @@ -313,6 +314,22 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, { this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky); } }, + /** + * Displays a thumb up, heart or peace image (randomly) for a moment (e.g. + * used when an opportunity is won) + * + * @private + */ + _onShowWow: function () { + var className = 'o_wow_thumbs'; + if (Math.random() > 0.9) { + var otherClasses = ['o_wow_peace', 'o_wow_heart']; + className = otherClasses[Math.floor(Math.random()*otherClasses.length)]; + } + var $body = $('body'); + $body.addClass(className); + setTimeout($body.removeClass.bind($body, className), 1000); + }, }); return AbstractWebClient; 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 464cd06e44d..27d85778431 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -380,10 +380,8 @@ var FormController = BasicController.extend({ }); } - if (event.data.show_wow) { - def.then(function () { - self.show_wow(); - }); + if (event.data.showWow) { + def.then(this.trigger_up.bind(this, 'show_wow')); } def.always(this._enableButtons.bind(this)); 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 b15e6b80131..2eda699c5d3 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -206,7 +206,7 @@ var FormRenderer = BasicRenderer.extend({ self.trigger_up('button_clicked', { attrs: node.attrs, record: self.state, - show_wow: self.$el.hasClass('o_wow'), // TODO: implement this (in view) + showWow: $el.hasClass('o_wow'), }); }); }, diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 2d23f4a9cb4..5643b2c92ca 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -4895,5 +4895,40 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('wow effect', function (assert) { + assert.expect(3); + + this.data.partner.fields.model_name = { string: "Model name", type: "char" }; + + var clickCount = 0; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '
' + + '