[IMP] web: add decoration support for all field widgets (#25160)
It is sometimes useful to be able to customize slightly the color of a field depending on some conditions on the current record. For example, imagine that we want to show a VAT number field in green/red, if it was validated or not. To do that, we can add a field with an onchange, and simply add decoration-danger and decoration-success attributes to the field.
This commit is contained in:
@@ -172,6 +172,12 @@ var AbstractField = Widget.extend({
|
||||
// calls to the format (resp. parse) function.
|
||||
this.formatOptions = {};
|
||||
this.parseOptions = {};
|
||||
|
||||
// if we add decorations, we need to reevaluate the field whenever any
|
||||
// value from the record is changed
|
||||
if (this.attrs.decorations) {
|
||||
this.resetOnAnyFieldChange = true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Loads the libraries listed in this.jsLibs and this.cssLibs
|
||||
@@ -331,6 +337,21 @@ var AbstractField = Widget.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Apply field decorations (only if field-specific decorations have been
|
||||
* defined in an attribute).
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_applyDecorations: function () {
|
||||
var self = this;
|
||||
this.attrs.decorations.forEach(function (dec) {
|
||||
var isToggled = py.PY_isTrue(
|
||||
py.evaluate(dec.expression, self.record.evalContext)
|
||||
);
|
||||
self.$el.toggleClass(dec.className, isToggled);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Converts the value from the field to a string representation.
|
||||
*
|
||||
@@ -377,6 +398,9 @@ var AbstractField = Widget.extend({
|
||||
* @returns {Deferred|undefined}
|
||||
*/
|
||||
_render: function () {
|
||||
if (this.attrs.decorations) {
|
||||
this._applyDecorations();
|
||||
}
|
||||
if (this.mode === 'edit') {
|
||||
return this._renderEdit();
|
||||
} else if (this.mode === 'readonly') {
|
||||
|
||||
@@ -206,6 +206,13 @@ var InputField = DebouncedField.extend({
|
||||
this.isDirty = false;
|
||||
}
|
||||
if (this.isDirty || (event && event.target === this && event.data.changes[this.name] === this.value)) {
|
||||
if (this.attrs.decorations) {
|
||||
// if a field is modified, then it could have triggered an onchange
|
||||
// which changed some of its decorations. Since we bypass the
|
||||
// render function, we need to apply decorations here to make
|
||||
// sure they are recomputed.
|
||||
this._applyDecorations();
|
||||
}
|
||||
return $.when();
|
||||
} else {
|
||||
return this._render();
|
||||
|
||||
@@ -209,6 +209,18 @@ var BasicView = AbstractView.extend({
|
||||
var self = this;
|
||||
attrs.Widget = this._getFieldWidgetClass(viewType, field, attrs);
|
||||
|
||||
// process decoration attributes
|
||||
_.each(attrs, function (value, key) {
|
||||
var splitKey = key.split('-');
|
||||
if (splitKey[0] === 'decoration') {
|
||||
attrs.decorations = attrs.decorations || [];
|
||||
attrs.decorations.push({
|
||||
className: 'text-' + splitKey[1],
|
||||
expression: py.parse(py.tokenize(value))
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!_.isObject(attrs.options)) { // parent arch could have already been processed (TODO this should not happen)
|
||||
attrs.options = attrs.options ? pyeval.py_eval(attrs.options) : {};
|
||||
}
|
||||
|
||||
@@ -86,16 +86,6 @@
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
// Decoration of the row
|
||||
.text-bf {
|
||||
font-weight: bold;
|
||||
}
|
||||
.text-it {
|
||||
font-style: italic;
|
||||
}
|
||||
.text-muted {
|
||||
color: lighten($text-muted, 10%);
|
||||
}
|
||||
|
||||
// Grouped list views
|
||||
tbody > tr.o_group_header {
|
||||
|
||||
@@ -137,6 +137,17 @@ div.o_boolean_toggle {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Decorations
|
||||
.text-bf {
|
||||
font-weight: bold;
|
||||
}
|
||||
.text-it {
|
||||
font-style: italic;
|
||||
}
|
||||
.text-muted {
|
||||
color: lighten($text-muted, 10%);
|
||||
}
|
||||
|
||||
//== Badges
|
||||
.badge {
|
||||
margin: 1px 2px 1px 0;
|
||||
|
||||
@@ -206,6 +206,70 @@ QUnit.module('Views', {
|
||||
def.resolve();
|
||||
});
|
||||
|
||||
QUnit.test('decoration works on widgets', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="int_field"/>' +
|
||||
'<field name="display_name" decoration-danger="int_field < 5"/>' +
|
||||
'<field name="foo" decoration-danger="int_field > 5"/>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
});
|
||||
assert.ok(!form.$('span[name="display_name"]').hasClass('text-danger'),
|
||||
'field display name should not have decoration');
|
||||
assert.ok(form.$('span[name="foo"]').hasClass('text-danger'),
|
||||
'field foo should have decoration');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('decoration on widgets are reevaluated if necessary', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="int_field"/>' +
|
||||
'<field name="display_name" decoration-danger="int_field < 5"/>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
viewOptions: {mode: 'edit'},
|
||||
});
|
||||
assert.ok(!form.$('input[name="display_name"]').hasClass('text-danger'),
|
||||
'field display name should not have decoration');
|
||||
form.$('input[name="int_field"]').val(3).trigger('input');
|
||||
assert.ok(form.$('input[name="display_name"]').hasClass('text-danger'),
|
||||
'field display name should now have decoration');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('decoration on widgets works on same widget', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="int_field" decoration-danger="int_field < 5"/>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
viewOptions: {mode: 'edit'},
|
||||
});
|
||||
assert.ok(!form.$('input[name="int_field"]').hasClass('text-danger'),
|
||||
'field should not have decoration');
|
||||
form.$('input[name="int_field"]').val(3).trigger('input');
|
||||
assert.ok(form.$('input[name="int_field"]').hasClass('text-danger'),
|
||||
'field should now have decoration');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -1358,6 +1358,34 @@ Let us mention the most important ones:
|
||||
- the field widgets can be used outside of a view. Their API is slightly
|
||||
awkward, but they are designed to be standalone.
|
||||
|
||||
Decorations
|
||||
-----------
|
||||
|
||||
Like the list view, field widgets have a simple support for decorations. The
|
||||
goal of decorations is to have a simple way to specify a text color depending on
|
||||
the record current state. For example,
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="state" decoration-danger="amount < 10000"/>
|
||||
|
||||
The valid decoration names are:
|
||||
|
||||
- decoration-bf
|
||||
- decoration-it
|
||||
- decoration-danger
|
||||
- decoration-info
|
||||
- decoration-muted
|
||||
- decoration-primary
|
||||
- decoration-success
|
||||
- decoration-warning
|
||||
|
||||
Each decoration *decoration-X* will be mapped to a css class *text-X*, which is
|
||||
a standard bootstrap css class (except for *text-it* and *text-bf*, which are
|
||||
handled by odoo and correspond to italic and bold, respectively). Note that the
|
||||
value of the decoration attribute should be a valid python expression, which
|
||||
will be evaluated with the record as evaluation context.
|
||||
|
||||
Non relational fields
|
||||
---------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user