[FIX] web: kanban: rendering of async widgets

This rev. fixes 3 issues with async (field) widgets in the kanban
view.

From rev. 5faec34a, widget'$el doesn't exist before start. This
means that for async widgets, one can't interact with the widget's
$el right after calling appentTo. In KanbanRecord, this is exactly
what we did for both field widgets (<field name=.../>) and widgets
(<widget name=.../>).

Moreover, when the kanban view was updated (e.g. when the user
refined the search using the search view), and the rendering was
async (because of the presence of an async widget), the renderer
didn't wait at all for the widget to be ready before updating the
view. This caused flickering, mostly, but also a crash in
accounting with the JournalDashboardGraph widget (because
on_attach_callabck was called before the widget was ready). To
reproduce this particular issue, go to accounting, add a filter
that doesn't match any record and save it as favorite (default),
press F5 (no record is displayed), remove the filter.

opw-1925079
opw-1925479

Fixes #30087
Closes #31254

closes odoo/odoo#31327
This commit is contained in:
Aaron Bohy
2019-02-21 13:54:47 +00:00
parent 6fc08a88ae
commit 7ffe3ce5c6
3 changed files with 126 additions and 13 deletions
@@ -300,6 +300,7 @@ var KanbanRecord = Widget.extend({
// field's widgets point of view
// that dict being shared between records, we don't modify it
// in place
var self = this;
var attrs = Object.create(null);
_.each(this.fieldsInfo[field_name], function (value, key) {
if (_.str.startsWith(key, 't-att-')) {
@@ -310,11 +311,12 @@ var KanbanRecord = Widget.extend({
});
var options = _.extend({}, this.options, {attrs: attrs});
var widget = new Widget(this, field_name, this.state, options);
var def = widget.replace($field);
var def = widget.replace($field).then(function () {
self._setFieldDisplay(widget.$el, field_name);
});
if (def.state() === 'pending') {
this.defs.push(def);
}
this._setFieldDisplay(widget.$el, field_name);
return widget;
},
_processWidgets: function () {
@@ -324,12 +326,13 @@ var KanbanRecord = Widget.extend({
var Widget = widgetRegistry.get($field.attr('name'));
var widget = new Widget(self, self.state);
var def = widget._widgetRenderAndInsert(function () {});
var def = widget._widgetRenderAndInsert(function () {}).then(function () {
widget.$el.addClass('o_widget');
$field.replaceWith(widget.$el);
});
if (def.state() === 'pending') {
self.defs.push(def);
}
widget.$el.addClass('o_widget');
$field.replaceWith(widget.$el);
});
},
/**
@@ -244,7 +244,6 @@ var KanbanRenderer = BasicRenderer.extend({
*/
updateState: function (state) {
this._setState(state);
this._toggleNoContentHelper();
return this._super.apply(this, arguments);
},
@@ -382,11 +381,8 @@ var KanbanRenderer = BasicRenderer.extend({
var self = this;
var oldWidgets = this.widgets;
this.widgets = [];
this.$el.empty();
var isGrouped = !!this.state.groupedBy.length;
this.$el.toggleClass('o_kanban_grouped', isGrouped);
this.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
var fragment = document.createDocumentFragment();
// render the kanban view
this.defs = [];
@@ -395,15 +391,19 @@ var KanbanRenderer = BasicRenderer.extend({
} else {
this._renderUngrouped(fragment);
}
this.$el.append(fragment);
this._toggleNoContentHelper();
var defs = this.defs;
return this._super.apply(this, arguments).then(function () {
delete this.defs;
defs.push(this._super.apply(this, arguments));
return $.when.apply($, defs).then(function () {
_.invoke(oldWidgets, 'destroy');
self.$el.empty();
self.$el.toggleClass('o_kanban_grouped', isGrouped);
self.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
self.$el.append(fragment);
self._toggleNoContentHelper();
if (self._isInDom) {
_.invoke(self.widgets, 'on_attach_callback');
}
return $.when.apply(null, defs);
});
},
/**
@@ -5133,6 +5133,116 @@ QUnit.module('Views', {
delete fieldRegistry.map.asyncWidget;
});
QUnit.test('asynchronous rendering of a field widget with display attr', function (assert) {
assert.expect(3);
var fooFieldDef = $.Deferred();
var FieldChar = fieldRegistry.get('char');
fieldRegistry.add('asyncwidget', FieldChar.extend({
willStart: function () {
return fooFieldDef;
},
start: function () {
this.$el.html('LOADED');
},
}));
var kanbanController;
testUtils.createAsyncView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
'<div><field name="foo" display="right" widget="asyncwidget"/></div>' +
'</t></templates></kanban>',
}).then(function (kanban) {
kanbanController = kanban;
});
assert.containsNone(document.body, '.o_kanban_record');
fooFieldDef.resolve();
assert.strictEqual(kanbanController.$('.o_kanban_record').text(),
"LOADEDLOADEDLOADEDLOADED");
assert.hasClass(kanbanController.$('.o_kanban_record:first .o_field_char'), 'float-right');
kanbanController.destroy();
delete fieldRegistry.map.asyncWidget;
});
QUnit.test('asynchronous rendering of a widget', function (assert) {
assert.expect(2);
var widgetDef = $.Deferred();
widgetRegistry.add('asyncwidget', Widget.extend({
willStart: function () {
return widgetDef;
},
start: function () {
this.$el.html('LOADED');
},
}));
var kanbanController;
testUtils.createAsyncView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
'<div><widget name="asyncwidget"/></div>' +
'</t></templates></kanban>',
}).then(function (kanban) {
kanbanController = kanban;
});
assert.containsNone(document.body, '.o_kanban_record');
widgetDef.resolve();
assert.strictEqual(kanbanController.$('.o_kanban_record .o_widget').text(),
"LOADEDLOADEDLOADEDLOADED");
kanbanController.destroy();
delete widgetRegistry.map.asyncWidget;
});
QUnit.test('update kanban with asynchronous field widget', function (assert) {
assert.expect(3);
var fooFieldDef = $.Deferred();
var FieldChar = fieldRegistry.get('char');
fieldRegistry.add('asyncwidget', FieldChar.extend({
willStart: function () {
return fooFieldDef;
},
start: function () {
this.$el.html('LOADED');
},
}));
var kanban = testUtils.createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
'<div><field name="foo" widget="asyncwidget"/></div>' +
'</t></templates></kanban>',
domain: [['id', '=', '0']], // no record matches this domain
});
assert.containsNone(kanban, '.o_kanban_record:not(.o_kanban_ghost)');
kanban.update({domain: []}); // this rendering will be async
assert.containsNone(kanban, '.o_kanban_record:not(.o_kanban_ghost)');
fooFieldDef.resolve();
assert.strictEqual(kanban.$('.o_kanban_record').text(),
"LOADEDLOADEDLOADEDLOADED");
kanban.destroy();
delete widgetRegistry.map.asyncWidget;
});
});
});