From 4520a09f2c789626d6aba074e063deb490a5bb52 Mon Sep 17 00:00:00 2001 From: "Michael Mattiello (mcm)" Date: Fri, 27 Mar 2020 10:56:44 +0000 Subject: [PATCH] [REF] web: convert FieldBoolean to owl This commit does 4 things: - adds a file for deprecated legacy fields. - moves legacy FieldBoolean into this file. - converts FieldBoolean to owl component. - adds a new xml file for component fields' templates. task id: 2193996 --- addons/web/__manifest__.py | 1 + .../static/src/js/_deprecated/basic_fields.js | 154 ++++++++++++++++++ .../web/static/src/js/fields/basic_fields.js | 131 +-------------- .../static/src/js/fields/basic_fields_owl.js | 111 +++++++++++++ addons/web/static/src/xml/fields.xml | 9 + .../static/tests/fields/basic_fields_tests.js | 7 +- addons/web/views/webclient_templates.xml | 2 + 7 files changed, 285 insertions(+), 130 deletions(-) create mode 100644 addons/web/static/src/js/_deprecated/basic_fields.js create mode 100644 addons/web/static/src/js/fields/basic_fields_owl.js create mode 100644 addons/web/static/src/xml/fields.xml diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index ccfbd9b8499..e72fabaff2a 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -23,6 +23,7 @@ This module provides the core of the Odoo Web Client. 'qweb': [ "static/src/xml/base.xml", "static/src/xml/chart.xml", + "static/src/xml/fields.xml", "static/src/xml/file_upload_progress_bar.xml", "static/src/xml/file_upload_progress_card.xml", "static/src/xml/kanban.xml", diff --git a/addons/web/static/src/js/_deprecated/basic_fields.js b/addons/web/static/src/js/_deprecated/basic_fields.js new file mode 100644 index 00000000000..2a70c3793c0 --- /dev/null +++ b/addons/web/static/src/js/_deprecated/basic_fields.js @@ -0,0 +1,154 @@ +//////////////////////////////////////////////////////////////////////////////// +// /!\ DEPRECATED +// +// Legacy Field Widgets are added in this file when they are converted into +// Owl Component. +//////////////////////////////////////////////////////////////////////////////// + +odoo.define('web.basic_fields.deprecated', function (require) { +"use strict"; + +/** + * This module contains most of the basic (meaning: non relational) field + * widgets. Field widgets are supposed to be used in views inheriting from + * BasicView, so, they can work with the records obtained from a BasicModel. + */ + +var AbstractField = require('web.AbstractField'); +var core = require('web.core'); + +var _lt = core._lt; + +var FieldBoolean = AbstractField.extend({ + className: 'o_field_boolean', + description: _lt("Checkbox"), + events: _.extend({}, AbstractField.prototype.events, { + change: '_onChange', + }), + supportedFieldTypes: ['boolean'], + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Toggle the checkbox if it is activated due to a click on itself. + * + * @override + */ + activate: function (options) { + var activated = this._super.apply(this, arguments); + // The formatValue of boolean fields renders HTML elements similar to + // the one rendered by the widget itself. Even though the event might + // have been fired on the non-widget version of this field, we can still + // test the presence of its custom class. + if (activated && options && options.event && $(options.event.target).closest('.custom-control.custom-checkbox').length) { + this._setValue(!this.value); // Toggle the checkbox + } + return activated; + }, + + /** + * @override + * @returns {jQuery} the focusable checkbox input + */ + getFocusableElement: function () { + return this.mode === 'readonly' ? $() : this.$input; + }, + /** + * A boolean field is always set since false is a valid value. + * + * @override + */ + isSet: function () { + return true; + }, + /** + * When the checkbox is rerendered, we need to check if it was the actual + * origin of the reset. If it is, we need to activate it back so it looks + * like it was not rerendered but is still the same input. + * + * @override + */ + reset: function (record, event) { + var rendered = this._super.apply(this, arguments); + if (event && event.target.name === this.name) { + this.activate(); + } + return rendered; + }, + /** + * Associates the 'for' attribute of the internal label. + * + * @override + */ + setIDForLabel: function (id) { + this._super.apply(this, arguments); + this.$('.custom-control-label').attr('for', id); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * The actual checkbox is designed in css to have full control over its + * appearance, as opposed to letting the browser and the os decide how + * a checkbox should look. The actual input is disabled and hidden. In + * readonly mode, the checkbox is disabled. + * + * @override + * @private + */ + _render: function () { + var $checkbox = this._formatValue(this.value); + this.$input = $checkbox.find('input'); + this.$input.prop('disabled', this.mode === 'readonly'); + this.$el.addClass($checkbox.attr('class')); + this.$el.empty().append($checkbox.contents()); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Properly update the value when the checkbox is (un)ticked to trigger + * possible onchanges. + * + * @private + */ + _onChange: function () { + this._setValue(this.$input[0].checked); + }, + /** + * Implement keyboard movements. Mostly useful for its environment, such + * as a list view. + * + * @override + * @private + * @param {KeyEvent} ev + */ + _onKeydown: function (ev) { + switch (ev.which) { + case $.ui.keyCode.ENTER: + // prevent subsequent 'click' event (see _onKeydown of AbstractField) + ev.preventDefault(); + this.$input.prop('checked', !this.value); + this._setValue(!this.value); + return; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + ev.preventDefault(); + } + this._super.apply(this, arguments); + }, +}); + +return { + FieldBoolean: FieldBoolean, +}; + +}); diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 3a3a037a0f0..ce7a7fac07d 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -11,6 +11,7 @@ var AbstractField = require('web.AbstractField'); var config = require('web.config'); var core = require('web.core'); var datepicker = require('web.datepicker'); +var deprecatedFields = require('web.basic_fields.deprecated'); var dom = require('web.dom'); var Domain = require('web.Domain'); var DomainSelector = require('web.DomainSelector'); @@ -24,6 +25,8 @@ var field_utils = require('web.field_utils'); var time = require('web.time'); var ColorpickerDialog = require('web.ColorpickerDialog'); +let FieldBoolean = deprecatedFields.FieldBoolean; + require("web.zoomodoo"); var qweb = core.qweb; @@ -986,134 +989,6 @@ var FieldMonetary = NumericField.extend({ }, }); -var FieldBoolean = AbstractField.extend({ - className: 'o_field_boolean', - description: _lt("Checkbox"), - events: _.extend({}, AbstractField.prototype.events, { - change: '_onChange', - }), - supportedFieldTypes: ['boolean'], - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Toggle the checkbox if it is activated due to a click on itself. - * - * @override - */ - activate: function (options) { - var activated = this._super.apply(this, arguments); - // The formatValue of boolean fields renders HTML elements similar to - // the one rendered by the widget itself. Even though the event might - // have been fired on the non-widget version of this field, we can still - // test the presence of its custom class. - if (activated && options && options.event && $(options.event.target).closest('.custom-control.custom-checkbox').length) { - this._setValue(!this.value); // Toggle the checkbox - } - return activated; - }, - - /** - * @override - * @returns {jQuery} the focusable checkbox input - */ - getFocusableElement: function () { - return this.mode === 'readonly' ? $() : this.$input; - }, - /** - * A boolean field is always set since false is a valid value. - * - * @override - */ - isSet: function () { - return true; - }, - /** - * When the checkbox is rerendered, we need to check if it was the actual - * origin of the reset. If it is, we need to activate it back so it looks - * like it was not rerendered but is still the same input. - * - * @override - */ - reset: function (record, event) { - var rendered = this._super.apply(this, arguments); - if (event && event.target.name === this.name) { - this.activate(); - } - return rendered; - }, - /** - * Associates the 'for' attribute of the internal label. - * - * @override - */ - setIDForLabel: function (id) { - this._super.apply(this, arguments); - this.$('.custom-control-label').attr('for', id); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * The actual checkbox is designed in css to have full control over its - * appearance, as opposed to letting the browser and the os decide how - * a checkbox should look. The actual input is disabled and hidden. In - * readonly mode, the checkbox is disabled. - * - * @override - * @private - */ - _render: function () { - var $checkbox = this._formatValue(this.value); - this.$input = $checkbox.find('input'); - this.$input.prop('disabled', this.mode === 'readonly'); - this.$el.addClass($checkbox.attr('class')); - this.$el.empty().append($checkbox.contents()); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Properly update the value when the checkbox is (un)ticked to trigger - * possible onchanges. - * - * @private - */ - _onChange: function () { - this._setValue(this.$input[0].checked); - }, - /** - * Implement keyboard movements. Mostly useful for its environment, such - * as a list view. - * - * @override - * @private - * @param {KeyEvent} ev - */ - _onKeydown: function (ev) { - switch (ev.which) { - case $.ui.keyCode.ENTER: - // prevent subsequent 'click' event (see _onKeydown of AbstractField) - ev.preventDefault(); - this.$input.prop('checked', !this.value); - this._setValue(!this.value); - return; - case $.ui.keyCode.UP: - case $.ui.keyCode.RIGHT: - case $.ui.keyCode.DOWN: - case $.ui.keyCode.LEFT: - ev.preventDefault(); - } - this._super.apply(this, arguments); - }, -}); - var FieldInteger = NumericField.extend({ description: _lt("Integer"), className: 'o_field_integer o_field_number', diff --git a/addons/web/static/src/js/fields/basic_fields_owl.js b/addons/web/static/src/js/fields/basic_fields_owl.js new file mode 100644 index 00000000000..a00b14fb9ee --- /dev/null +++ b/addons/web/static/src/js/fields/basic_fields_owl.js @@ -0,0 +1,111 @@ +odoo.define('web.basic_fields_owl', function (require) { + "use strict"; + + const AbstractField = require('web.AbstractFieldOwl'); + const CustomCheckbox = require('web.CustomCheckbox'); + const core = require('web.core'); + + const _lt = core._lt; + + class FieldBoolean extends AbstractField { + patched() { + super.patched(); + if (this.props.event && this.props.event.target === this) { + this.activate(); + } + } + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + /** + * @override + * @returns {HTMLElement|null} the focusable checkbox input + */ + get focusableElement() { + return this.mode === 'readonly' ? null : this.el.querySelector('input'); + } + /** + * A boolean field is always set since false is a valid value. + * + * @override + */ + get isSet() { + return true; + } + /** + * Toggle the checkbox if it is activated due to a click on itself. + * + * @override + * @param {Object} [options] + * @param {Event} [options.event] the event which fired this activation + * @returns {boolean} true if the component was activated, false if the + * focusable element was not found or invisible + */ + activate(options) { + const activated = super.activate(options); + // The event might have been fired on the non field version of + // this field, we can still test the presence of its custom class. + if (activated && options && options.event && options.event.target + .closest('.custom-control.custom-checkbox')) { + this._setValue(!this.value); // Toggle the checkbox + } + return activated; + } + /** + * Associates the 'for' attribute of the internal label. + * + * @override + */ + setIdForLabel(id) { + super.setIdForLabel(id); + this.el.querySelector('label').setAttribute('for', id); + } + + //---------------------------------------------------------------------- + // Handlers + //---------------------------------------------------------------------- + + /** + * Properly update the value when the checkbox is (un)ticked to trigger + * possible onchanges. + * + * @private + */ + _onChange(ev) { + this._setValue(ev.target.checked); + } + /** + * Implement keyboard movements. Mostly useful for its environment, such + * as a list view. + * + * @override + * @private + * @param {KeyEvent} ev + */ + _onKeydown(ev) { + switch (ev.which) { + case $.ui.keyCode.ENTER: + // prevent subsequent 'click' event (see _onKeydown of AbstractField) + ev.preventDefault(); + this._setValue(!this.value); + return; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + ev.preventDefault(); + } + super._onKeydown(ev); + } + } + FieldBoolean.components = { CustomCheckbox }; + FieldBoolean.description = _lt("Checkbox"); + FieldBoolean.supportedFieldTypes = ['boolean']; + FieldBoolean.template = 'web.FieldBoolean'; + + return { + FieldBoolean, + }; +}); diff --git a/addons/web/static/src/xml/fields.xml b/addons/web/static/src/xml/fields.xml new file mode 100644 index 00000000000..ff7939f36a3 --- /dev/null +++ b/addons/web/static/src/xml/fields.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 6bd62b2d25c..710dd66e40e 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -267,12 +267,14 @@ QUnit.module('basic_fields', { assert.containsOnce(form, '.o_field_boolean input:checked', "checkbox should now be checked"); // blindly press enter again, it should uncheck the checkbox - $(document.activeElement).trigger({type: "keydown", which: $.ui.keyCode.ENTER}); + await testUtils.dom.triggerEvent(document.activeElement, "keydown", + {which: $.ui.keyCode.ENTER}); assert.containsNone(form, '.o_field_boolean input:checked', "checkbox should not be checked"); await testUtils.nextTick(); // blindly press enter again, it should check the checkbox back - $(document.activeElement).trigger({type: "keydown", which: $.ui.keyCode.ENTER}); + await testUtils.dom.triggerEvent(document.activeElement, "keydown", + {which: $.ui.keyCode.ENTER}); assert.containsOnce(form, '.o_field_boolean input:checked', "checkbox should still be checked"); @@ -333,6 +335,7 @@ QUnit.module('basic_fields', { // the checkbox directly in readonly mode ! $cell = list.$('tr.o_data_row:has(.custom-checkbox input:not(:checked)) td:not(.o_list_record_selector)').first(); await testUtils.dom.click($cell.find('.custom-checkbox .custom-control-label')); + await testUtils.nextTick(); assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5, "should still have 5 checkboxes"); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 4778e5a328c..20bf8b43205 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -290,6 +290,7 @@ + @@ -301,6 +302,7 @@ +