[FIX] web: reimplement custom widgets in basic views

With the new views, we lost a (almost unused and undocumented) feature:
the ability to instantiate custom widgets in a form view, not linked to
a particular field.  For example:

<widget type="weekly_timesheet" attrs="{'readonly': [['state', 'not in', ['new', 'draft']]]}"/>

We reintroduce this feature in this commit, with a nice twist: it also
works for the list view and the kanban view.
This commit is contained in:
Géry Debongnie
2017-09-13 14:04:34 +02:00
parent c1fa3b8ab3
commit 93cf24f73a
9 changed files with 196 additions and 14 deletions
@@ -11,6 +11,7 @@ var AbstractRenderer = require('web.AbstractRenderer');
var config = require('web.config');
var core = require('web.core');
var dom = require('web.dom');
var widgetRegistry = require('web.widget_registry');
var qweb = core.qweb;
@@ -331,6 +332,19 @@ var BasicRenderer = AbstractRenderer.extend({
_getModifiersData: function (node) {
return _.findWhere(this.allModifiersData, {node: node});
},
/**
* @private
* @param {jQueryElement} $el
* @param {Object} node
*/
_handleAttributes: function ($el, node) {
if (node.attrs.class) {
$el.addClass(node.attrs.class);
}
if (node.attrs.style) {
$el.attr('style', node.attrs.style);
}
},
/**
* Used by list and kanban renderers to determine whether or not to display
* the no content helper (if there is no data in the state to display)
@@ -540,6 +554,31 @@ var BasicRenderer = AbstractRenderer.extend({
_renderView: function () {
return $.when();
},
/**
* Instantiate custom widgets
*
* @private
* @param {Object} record
* @param {Object} node
* @returns {jQueryElement}
*/
_renderWidget: function (record, node) {
var Widget = widgetRegistry.get(node.attrs.name);
var widget = new Widget(this, record);
// Prepare widget rendering and save the related deferred
var def = widget.__widgetRenderAndInsert(function () {});
if (def.state() === 'pending') {
this.defs.push(def);
}
// handle other attributes/modifiers
this._handleAttributes(widget.$el, node);
this._registerModifiers(node, record, widget);
widget.$el.addClass('o_widget');
return widget.$el;
},
/**
* Rerenders a given widget and make sure the associated data which
* referenced the old one is updated.
@@ -0,0 +1,27 @@
odoo.define('web.widget_registry', function (require) {
"use strict";
// This registry is supposed to contain all custom widgets that will be
// available in the basic views, with the tag <widget/>. There are
// currently no such widget in the web client, but the functionality is
// certainly useful to be able to cleanly add custom behaviour in basic
// views (and most notably, the form view)
//
// The way custom widgets work is that they register themselves to this
// registry:
//
// widgetRegistry.add('some_name', MyWidget);
//
// Then, they are available with the <widget/> tag (in the arch):
//
// <widget name="some_name"/>
//
// Widgets will be then properly instantiated, rendered and destroyed at the
// appropriate time, with the current state in second argument.
//
// For more examples, look at the tests (grep '<widget' in the test folder)
var Registry = require('web.Registry');
return new Registry();
});
@@ -225,19 +225,6 @@ var FormRenderer = BasicRenderer.extend({
}
return idForLabel;
},
/**
* @private
* @param {jQueryElement} $el
* @param {Object} node
*/
_handleAttributes: function ($el, node) {
if (node.attrs.class) {
$el.addClass(node.attrs.class);
}
if (node.attrs.style) {
$el.attr('style', node.attrs.style);
}
},
/**
* @private
* @param {Object} node
@@ -758,6 +745,16 @@ var FormRenderer = BasicRenderer.extend({
$sheet.append(_.map(node.children, this._renderNode.bind(this)));
return $sheet;
},
/**
* Instantiate custom widgets
*
* @private
* @param {Object} node
* @returns {jQueryElement}
*/
_renderTagWidget: function (node) {
return this._renderWidget(this.state, node);
},
/**
* Main entry point for the rendering. From here, we call _renderNode on
* the root of the arch, then, when every deferred (from the field widgets)
@@ -11,6 +11,7 @@ var Domain = require('web.Domain');
var field_utils = require('web.field_utils');
var utils = require('web.utils');
var Widget = require('web.Widget');
var widgetRegistry = require('web.widget_registry');
var _t = core._t;
var QWeb = core.qweb;
@@ -262,6 +263,21 @@ var KanbanRecord = Widget.extend({
this._setFieldDisplay(widget.$el, field_name);
return widget;
},
_processWidgets: function () {
var self = this;
this.$("widget").each(function () {
var $field = $(this);
var Widget = widgetRegistry.get($field.attr('name'));
var widget = new Widget(self, self.state);
var def = widget.__widgetRenderAndInsert(function () {});
if (def.state() === 'pending') {
self.defs.push(def);
}
widget.$el.addClass('o_widget');
$field.replaceWith(widget.$el);
});
},
/**
* Renders the record
*/
@@ -274,6 +290,7 @@ var KanbanRecord = Widget.extend({
this.$el.on('click', this._onGlobalClick.bind(this));
}
this._processFields();
this._processWidgets();
this._setupColor();
this._setupColorPicker();
this._attachTooltip();
@@ -222,7 +222,7 @@ var ListRenderer = BasicRenderer.extend({
var tdClassName = 'o_data_cell';
if (node.tag === 'button') {
tdClassName += ' o_list_button';
} else {
} else if (node.tag === 'field') {
var typeClass = FIELD_CLASSES[this.state.fields[node.attrs.name].type];
if (typeClass) {
tdClassName += (' ' + typeClass);
@@ -245,6 +245,8 @@ var ListRenderer = BasicRenderer.extend({
if (node.tag === 'button') {
return $td.append(this._renderButton(record, node));
} else if (node.tag === 'widget') {
return $td.append(this._renderWidget(record, node));
}
if (node.attrs.widget || (options && options.renderWidgets)) {
var widget = this._renderFieldWidget(node, record, _.pick(options, 'mode'));
@@ -8,6 +8,8 @@ var FormView = require('web.FormView');
var pyeval = require('web.pyeval');
var RainbowMan = require('web.rainbow_man');
var testUtils = require('web.test_utils');
var widgetRegistry = require('web.widget_registry');
var Widget = require('web.Widget');
var _t = core._t;
var createView = testUtils.createView;
@@ -5867,5 +5869,36 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('basic support for widgets', function (assert) {
assert.expect(1);
var MyWidget = Widget.extend({
init: function (parent, dataPoint) {
this.data = dataPoint.data;
},
start: function () {
this.$el.text(JSON.stringify(this.data));
},
});
widgetRegistry.add('test', MyWidget);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'<widget name="test"/>' +
'</form>',
});
assert.strictEqual(form.$el.text(), '{"foo":"My little Foo Value","bar":false}',
"widget should have been instantiated");
form.destroy();
delete widgetRegistry.map.test;
});
});
});
@@ -3,6 +3,8 @@ odoo.define('web.kanban_tests', function (require) {
var KanbanView = require('web.KanbanView');
var testUtils = require('web.test_utils');
var widgetRegistry = require('web.widget_registry');
var Widget = require('web.Widget');
var createView = testUtils.createView;
@@ -1898,6 +1900,40 @@ QUnit.module('Views', {
"the add button should still be visible");
kanban.destroy();
});
QUnit.test('basic support for widgets', function (assert) {
assert.expect(1);
var MyWidget = Widget.extend({
init: function (parent, dataPoint) {
this.data = dataPoint.data;
},
start: function () {
this.$el.text(JSON.stringify(this.data));
},
});
widgetRegistry.add('test', MyWidget);
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
'<div>' +
'<t t-esc="record.foo.value"/>' +
'<field name="foo" blip="1"/>' +
'<widget name="test"/>' +
'</div>' +
'</t></templates></kanban>',
});
assert.strictEqual(kanban.$('.o_widget:eq(2)').text(), '{"foo":"gnap","id":3}',
"widget should have been instantiated");
kanban.destroy();
delete widgetRegistry.map.test;
});
});
});
@@ -5,6 +5,8 @@ var config = require('web.config');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var testUtils = require('web.test_utils');
var widgetRegistry = require('web.widget_registry');
var Widget = require('web.Widget');
var createView = testUtils.createView;
@@ -2936,6 +2938,34 @@ QUnit.module('Views', {
"The first element of the row name should be a span");
list.destroy();
});
QUnit.test('basic support for widgets', function (assert) {
assert.expect(1);
var MyWidget = Widget.extend({
init: function (parent, dataPoint) {
this.data = dataPoint.data;
},
start: function () {
this.$el.text(JSON.stringify(this.data));
},
});
widgetRegistry.add('test', MyWidget);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree><field name="foo"/><field name="int_field"/><widget name="test"/></tree>',
});
assert.strictEqual(list.$('.o_widget').first().text(), '{"foo":"yop","int_field":10,"id":1}',
"widget should have been instantiated");
list.destroy();
delete widgetRegistry.map.test;
});
});
});
+1
View File
@@ -181,6 +181,7 @@
<script type="text/javascript" src="/web/static/src/js/fields/abstract_field.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/basic_fields.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/field_registry.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/basic/widget_registry.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/field_utils.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/relational_fields.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/special_fields.js"></script>