diff options
| author | oco-odoo <oco@odoo.com> | 2018-06-08 15:20:49 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-06-08 15:20:49 +0200 |
| commit | d4cd8be48f83a7775c667bc6c968a4b9e14679be (patch) | |
| tree | 9185c59636525a98292e31112ca00152c0db4fb3 | |
| parent | a8aa0bcbbe48babe877e08d81e5bdcc9e5779fdc (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.js | 24 | ||||
| -rw-r--r-- | addons/web/static/src/js/fields/basic_fields.js | 7 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/basic/basic_view.js | 12 | ||||
| -rw-r--r-- | addons/web/static/src/scss/list_view.scss | 10 | ||||
| -rw-r--r-- | addons/web/static/src/scss/webclient.scss | 11 | ||||
| -rw-r--r-- | addons/web/static/tests/views/form_tests.js | 64 | ||||
| -rw-r--r-- | doc/reference/javascript_reference.rst | 28 |
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 < 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); 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 < 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 --------------------- |
