summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoroco-odoo <oco@odoo.com>2018-06-08 15:20:49 +0200
committerGitHub <noreply@github.com>2018-06-08 15:20:49 +0200
commitd4cd8be48f83a7775c667bc6c968a4b9e14679be (patch)
tree9185c59636525a98292e31112ca00152c0db4fb3
parenta8aa0bcbbe48babe877e08d81e5bdcc9e5779fdc (diff)
[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.
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js24
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js7
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js12
-rw-r--r--addons/web/static/src/scss/list_view.scss10
-rw-r--r--addons/web/static/src/scss/webclient.scss11
-rw-r--r--addons/web/static/tests/views/form_tests.js64
-rw-r--r--doc/reference/javascript_reference.rst28
7 files changed, 146 insertions, 10 deletions
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 735e12a3861..9bb61475b83 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -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
@@ -332,6 +338,21 @@ var AbstractField = Widget.extend({
//--------------------------------------------------------------------------
/**
+ * 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.
*
* @private
@@ -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') {
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 31b0c53dd64..01b4cb4371d 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -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();
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 22900e12d6b..2c4ed711223 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -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) : {};
}
diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss
index 539b5685a31..27c10cda556 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -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 {
diff --git a/addons/web/static/src/scss/webclient.scss b/addons/web/static/src/scss/webclient.scss
index 3c791886140..8f4dadb22df 100644
--- a/addons/web/static/src/scss/webclient.scss
+++ b/addons/web/static/src/scss/webclient.scss
@@ -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;
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index c12b96d4ca8..18c82adb0a5 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -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 &lt; 5"/>' +
+ '<field name="foo" decoration-danger="int_field &gt; 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 &lt; 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 &lt; 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);
diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst
index ef41944e389..141c614f29c 100644
--- a/doc/reference/javascript_reference.rst
+++ b/doc/reference/javascript_reference.rst
@@ -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 &lt; 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
---------------------