Skip to content

Commit

Permalink
[FIX] web: kanban: rendering of async widgets
Browse files Browse the repository at this point in the history
This rev. fixes 3 issues with async (field) widgets in the kanban
view.

From rev. 5faec34, 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
  • Loading branch information
aab-odoo committed Feb 21, 2019
1 parent faf8c41 commit 383c558
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 13 deletions.
13 changes: 8 additions & 5 deletions addons/web/static/src/js/views/kanban/kanban_record.js
Expand Up @@ -300,6 +300,7 @@ var KanbanRecord = Widget.extend({
// field's widgets point of view // field's widgets point of view
// that dict being shared between records, we don't modify it // that dict being shared between records, we don't modify it
// in place // in place
var self = this;
var attrs = Object.create(null); var attrs = Object.create(null);
_.each(this.fieldsInfo[field_name], function (value, key) { _.each(this.fieldsInfo[field_name], function (value, key) {
if (_.str.startsWith(key, 't-att-')) { if (_.str.startsWith(key, 't-att-')) {
Expand All @@ -310,11 +311,12 @@ var KanbanRecord = Widget.extend({
}); });
var options = _.extend({}, this.options, {attrs: attrs}); var options = _.extend({}, this.options, {attrs: attrs});
var widget = new Widget(this, field_name, this.state, options); 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') { if (def.state() === 'pending') {
this.defs.push(def); this.defs.push(def);
} }
this._setFieldDisplay(widget.$el, field_name);
return widget; return widget;
}, },
_processWidgets: function () { _processWidgets: function () {
Expand All @@ -324,12 +326,13 @@ var KanbanRecord = Widget.extend({
var Widget = widgetRegistry.get($field.attr('name')); var Widget = widgetRegistry.get($field.attr('name'));
var widget = new Widget(self, self.state); 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') { if (def.state() === 'pending') {
self.defs.push(def); self.defs.push(def);
} }
widget.$el.addClass('o_widget');
$field.replaceWith(widget.$el);
}); });
}, },
/** /**
Expand Down
16 changes: 8 additions & 8 deletions addons/web/static/src/js/views/kanban/kanban_renderer.js
Expand Up @@ -244,7 +244,6 @@ var KanbanRenderer = BasicRenderer.extend({
*/ */
updateState: function (state) { updateState: function (state) {
this._setState(state); this._setState(state);
this._toggleNoContentHelper();
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },


Expand Down Expand Up @@ -382,11 +381,8 @@ var KanbanRenderer = BasicRenderer.extend({
var self = this; var self = this;
var oldWidgets = this.widgets; var oldWidgets = this.widgets;
this.widgets = []; this.widgets = [];
this.$el.empty();


var isGrouped = !!this.state.groupedBy.length; var isGrouped = !!this.state.groupedBy.length;
this.$el.toggleClass('o_kanban_grouped', isGrouped);
this.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
var fragment = document.createDocumentFragment(); var fragment = document.createDocumentFragment();
// render the kanban view // render the kanban view
this.defs = []; this.defs = [];
Expand All @@ -395,15 +391,19 @@ var KanbanRenderer = BasicRenderer.extend({
} else { } else {
this._renderUngrouped(fragment); this._renderUngrouped(fragment);
} }
this.$el.append(fragment);
this._toggleNoContentHelper();
var defs = this.defs; 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'); _.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) { if (self._isInDom) {
_.invoke(self.widgets, 'on_attach_callback'); _.invoke(self.widgets, 'on_attach_callback');
} }
return $.when.apply(null, defs);
}); });
}, },
/** /**
Expand Down
110 changes: 110 additions & 0 deletions addons/web/static/tests/views/kanban_tests.js
Expand Up @@ -5133,6 +5133,116 @@ QUnit.module('Views', {
delete fieldRegistry.map.asyncWidget; 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;
});
}); });


}); });

0 comments on commit 383c558

Please sign in to comment.