[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
<field name="my_float_field" widget="float_factor"
option="{'factor': 2}"/>
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
<field name="my_float" widget="float_toggle"
option="{'range': [0, 0.5, 1], 'factor': 2}"/>
'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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -429,3 +429,8 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
button.o_field_float_toggle {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -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:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="qux" widget="float_factor" options="{\'factor\': 0.5}" digits="[16,2]"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
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:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="qux" widget="float_toggle" options="{\'factor\': 0.125, \'range\': [0, 1, 0.75, 0.5, 0.25]}" digits="[5,3]"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
<field name="days_to_close" widget="float_toggle" options='{"factor": 2, "range": [0, 4, 8]}'/>
|
||||
|
||||
- boolean (FieldBoolean)
|
||||
This is the default field type for fields of type *boolean*.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user