From 1f72644c445826ae7263e9b0f4737ffab99a778e Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Wed, 9 May 2018 09:40:02 +0200 Subject: [PATCH] [IMP] web: float_factor and float_toggle widgets This commit provides new js widgets for list, kanban and form views on float field. 1/ 'float_factor' displays the normal float field, but takes a conversion factor as option. The displays value is the normal one multiplied by the factor. You can use the widget like 2/ 'float_toggle' displays a button (in edit mode) looping on a range of given value at each click. A conversion factor is optional (default will be 1) to display a converted value, but sent to non converted one to the server. You can use the widget like 'range' and 'factor' are put in the options. In simple read mode, the widget display the value as a traditionnal text (like a normal char field). This commit provides tests and documentation for those 2 widgets. Task #39079 --- addons/web/static/src/js/core/utils.js | 20 +++ .../web/static/src/js/fields/basic_fields.js | 140 +++++++++++++++++- .../static/src/js/fields/field_registry.js | 2 + .../web/static/src/js/fields/field_utils.js | 29 ++++ addons/web/static/src/scss/fields.scss | 5 + .../static/tests/fields/basic_fields_tests.js | 84 +++++++++++ doc/reference/javascript_reference.rst | 20 +++ 7 files changed, 299 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index 3068a6528c9..f4031fccec9 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -511,6 +511,26 @@ var utils = { return Object.freeze(obj); }, + /** + * Find the closest value of the given one in the provided array + * + * @param {Number} num + * @param {Array} arr + * @returns {Number|undefined} + */ + closestNumber: function (num, arr) { + var curr = arr[0]; + var diff = Math.abs (num - curr); + for (var val = 0; val < arr.length; val++) { + var newdiff = Math.abs (num - arr[val]); + if (newdiff < diff) { + diff = newdiff; + curr = arr[val]; + } + } + return curr; + }, + }; return utils; diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index f209925f37c..399647d1dbe 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -898,6 +898,142 @@ var FieldFloatTime = FieldFloat.extend({ formatType: 'float_time', }); +var FieldFloatFactor = FieldFloat.extend({ + supportedFieldTypes: ['float'], + className: 'o_field_float_factor', + formatType: 'float_factor', + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + // default values + if (!this.nodeOptions.factor){ + this.nodeOptions.factor = 1; + } + // use as format and parse options + this.parseOptions = this.nodeOptions; + } +}); + +/** + * The goal of this widget is to replace the input field by a button containing a + * range of possible values (given in the options). Each click allows the user to loop + * in the range. The purpose here is to restrict the field value to a predefined selection. + * Also, the widget support the factor conversion as the *float_factor* widget (Range values + * should be the result of the conversion). + **/ +var FieldFloatToggle = AbstractField.extend({ + supportedFieldTypes: ['float'], + formatType: 'float_factor', + className: 'o_field_float_toggle', + tagName: 'span', + events: { + click: '_onClick' + }, + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + + this.formatType = 'float_factor'; + + if (this.mode === 'edit') { + this.tagName = 'button'; + } + + // we don't inherit Float Field + if (this.attrs.digits) { + this.nodeOptions.digits = JSON.parse(this.attrs.digits); + } + // default values + if (!this.nodeOptions.factor){ + this.nodeOptions.factor = 1; + } + if (!this.nodeOptions.range){ + this.nodeOptions.range = [0.0, 0.5, 1.0]; + } + + // use as format and parse options + this.parseOptions = this.nodeOptions; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Get the display value but in real type to use it in calculations + * + * @private + * @returns {float} The current formatted value + */ + _getDisplayedValue: function () { + return parseFloat(this._formatValue(this.value)); + }, + /** + * Formats the HTML input tag for edit mode and stores selection status. + * + * @override + * @private + */ + _renderEdit: function () { + // Keep a reference to the input so $el can become something else + // without losing track of the actual input. + this.$el.text(this._formatValue(this.value)); + }, + /** + * Resets the content to the formated value in readonly mode. + * + * @override + * @private + */ + _renderReadonly: function () { + this.$el.text(this._formatValue(this.value)); + }, + /** + * Get the next value in the range, from the current one. If the current + * one is not in the range, the next value of the closest one will be chosen. + * + * @private + * @returns {number} The next formatted value in the range + */ + _nextValue: function () { + var range = this.nodeOptions.range; + var val = utils.closestNumber(this._getDisplayedValue(), range); + var index = _.indexOf(range, val); + if (index !== -1) { + if (index + 1 < range.length) { + return range[index + 1]; + } + } + return range[0]; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Clicking on the button triggers the change of value; the next one of + * the range will be displayed. + * + * @private + * @param {OdooEvent} ev + */ + _onClick: function(ev) { + if (this.mode === 'edit') { + ev.stopPropagation(); // only stop propagation in edit mode + var next_val = this._nextValue(); + this._setValue(next_val.toString()); // will be parsed in _setValue + } + }, + +}); + var FieldPercentage = FieldFloat.extend({ formatType:'percentage', }); @@ -1102,7 +1238,7 @@ var CopyClipboard = { $clipboardBtn.tooltip({title: _t('Copied !'), trigger: 'manual', placement: 'right'}); this.clipboard = new ClipboardJS($clipboardBtn[0], { text: function (_) { - return self.value.trim(); + return self.value.trim(); }, // Container added because of Bootstrap modal that give the focus to another element. // We need to give to correct focus to ClipboardJS (see in ClipboardJS doc) @@ -2757,6 +2893,8 @@ return { FieldDomain: FieldDomain, FieldFloat: FieldFloat, FieldFloatTime: FieldFloatTime, + FieldFloatFactor: FieldFloatFactor, + FieldFloatToggle: FieldFloatToggle, FieldPercentage : FieldPercentage, FieldInteger: FieldInteger, FieldMonetary: FieldMonetary, diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js index 20af2a01064..df8ffc9dd5b 100644 --- a/addons/web/static/src/js/fields/field_registry.js +++ b/addons/web/static/src/js/fields/field_registry.js @@ -53,6 +53,8 @@ registry .add('statinfo', basic_fields.StatInfo) .add('percentpie', basic_fields.FieldPercentPie) .add('float_time', basic_fields.FieldFloatTime) + .add('float_factor', basic_fields.FieldFloatFactor) + .add('float_toggle', basic_fields.FieldFloatToggle) .add('progressbar', basic_fields.FieldProgressBar) .add('toggle_button', basic_fields.FieldToggleBoolean) .add('dashboard_graph', basic_fields.JournalDashboardGraph) diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js index 989f99c5fef..0e9dc063834 100644 --- a/addons/web/static/src/js/fields/field_utils.js +++ b/addons/web/static/src/js/fields/field_utils.js @@ -173,6 +173,21 @@ function formatFloat(value, field, options) { return formatted.join(l10n.decimal_point); } + +/** + * Returns a string representing a float value, from a float converted with a + * factor. + * + * @param {number} value + * @param {number} [options.factor] + * Conversion factor, default value is 1.0 + * @returns {string} + */ +function formatFloatFactor(value, field, options) { + var factor = options.factor || 1; + return formatFloat(value * factor, field, options); +} + /** * Returns a string representing a time value, from a float. The idea is that * we sometimes want to display something like 1:45 instead of 1.75, or 0:15 @@ -528,6 +543,18 @@ function parseMonetary(value, field, options) { return parseFloat(values[0] === currency.symbol ? values[1] : values[0]); } +/** + * Parse a String containing float and unconvert it with a conversion factor + * + * @param {number} [options.factor] + * Conversion factor, default value is 1.0 + */ +function parseFloatFactor(value, field, options) { + var parsed = parseFloat(value); + var factor = options.factor || 1.0; + return parsed / factor; +} + function parseFloatTime(value) { var factor = 1; if (value[0] === '-') { @@ -596,6 +623,7 @@ return { date: formatDate, datetime: formatDateTime, float: formatFloat, + float_factor: formatFloatFactor, float_time: formatFloatTime, html: _.identity, // todo integer: formatInteger, @@ -615,6 +643,7 @@ return { date: parseDate, // todo datetime: parseDateTime, // todo float: parseFloat, + float_factor: parseFloatFactor, float_time: parseFloatTime, html: _.identity, // todo integer: parseInteger, diff --git a/addons/web/static/src/scss/fields.scss b/addons/web/static/src/scss/fields.scss index 5f14493c613..9a1e904fdc2 100644 --- a/addons/web/static/src/scss/fields.scss +++ b/addons/web/static/src/scss/fields.scss @@ -429,3 +429,8 @@ } } + +button.o_field_float_toggle { + width: 100%; + text-align: center; +} diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 030d173dbeb..1855b032bf3 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -3264,6 +3264,90 @@ QUnit.module('basic_fields', { }); + QUnit.module('FieldFloatFactor'); + + QUnit.test('float_factor field in form view', function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + // 16.4 / 2 = 8.2 + assert.strictEqual(args.args[1].qux, 4.6, 'the correct float value should be saved'); + } + return this._super.apply(this, arguments); + }, + res_id: 5, + }); + assert.strictEqual(form.$('.o_field_widget').first().text(), '4.55', // 9.1 / 0.5 + 'The formatted value should be displayed properly.'); + + form.$buttons.find('.o_form_button_edit').click(); + assert.strictEqual(form.$('input').val(), '4.55', + 'The value should be rendered correctly in the input.'); + + form.$('input').val('2.3').trigger('input'); + + form.$buttons.find('.o_form_button_save').click(); + assert.strictEqual(form.$('.o_field_widget').first().text(), '2.30', + 'The new value should be saved and displayed properly.'); + + form.destroy(); + }); + + QUnit.module('FieldFloatToggle'); + + QUnit.test('float_toggle field in form view', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '
', + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + // 1.000 / 0.125 = 8 + assert.strictEqual(args.args[1].qux, 8, 'the correct float value should be saved'); + } + return this._super.apply(this, arguments); + }, + res_id: 1, + }); + assert.strictEqual(form.$('.o_field_widget').first().text(), '0.056', + 'The formatted time value should be displayed properly.'); + + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('button.o_field_float_toggle').text(), '0.056', + 'The value should be rendered correctly on the button.'); + + form.$('button.o_field_float_toggle').click(); // clicking will make the next value 1, since 0 was the closest of 0.056 + + assert.strictEqual(form.$('button.o_field_float_toggle').text(), '1.000', + 'The value should be rendered correctly on the button.'); + + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_field_widget').first().text(), '1.000', + 'The new value should be saved and displayed properly.'); + + form.destroy(); + }); + + QUnit.module('PhoneWidget'); QUnit.test('phone field in form view on extra small screens', function (assert) { diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 6cbfd3563dd..14647955afa 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -1418,6 +1418,26 @@ order. - Supported field types: *float* +- float_factor (FieldFloatFactor) + This widget aims to display properly a float value that converted using a factor + given in its options. So, for example, the value saved in database is 0.5 and the + factor is 3, the widget value should be formatted as 1.5. + + - Supported field types: *float* + +- float_toggle (FieldFloatToggle) + The goal of this widget is to replace the input field by a button containing a + range of possible values (given in the options). Each click allows the user to loop + in the range. The purpose here is to restrict the field value to a predefined selection. + Also, the widget support the factor conversion as the *float_factor* widget (Range values + should be the result of the conversion). + + - Supported field types: *float* + + .. code-block:: xml + + + - boolean (FieldBoolean) This is the default field type for fields of type *boolean*.