[FIX] pad: make it work with the new views

The pad widget was partially updated to the new views, but was no longer
working.  In this commit, we rewrite the widget to make it work.

Note that we did some functional changes:
- we do only one rpc to check if the server is configured. the result is
  stored on the prototype, so we do not need to do it again
- we do some rpcs with shadow: true, so we will not stop the rest of the
  ui if the server is slow to respond
- we display a loading message in readonly, when the content is not
  loaded yet.
This commit is contained in:
Géry Debongnie
2017-05-11 09:53:10 +02:00
parent 33710d540c
commit c31bf95e4e
3 changed files with 297 additions and 89 deletions
+140 -89
View File
@@ -3,116 +3,167 @@ odoo.define('pad.pad', function (require) {
var AbstractField = require('web.AbstractField');
var core = require('web.core');
var field_registry = require('web.field_registry');
var fieldRegistry = require('web.field_registry');
var _t = core._t;
var FieldPad = AbstractField.extend({
template: 'FieldPad',
content: "",
events: {
'click .oe_pad_switch': function() {
this.$el.toggleClass('oe_pad_fullscreen mb0');
this.$('.oe_pad_switch').toggleClass('fa-expand fa-compress');
},
'click .oe_pad_switch': '_onToggleFullScreen',
},
supportedFieldTypes: ['char'],
init: function() {
var self = this;
this._super.apply(this, arguments);
this.configured = false;
this._configured_deferred = $.Deferred();
this._url_request_deferred = $.Deferred();
/**
* @override
*/
willStart: function () {
if (this.isPadConfigured === undefined) {
return this._rpc({
method: 'pad_is_configured',
model: this.model,
}).then(function (result) {
// we write on the prototype to share the information between
// all pad widgets instances, across all actions
FieldPad.prototype.isPadConfigured = result;
});
}
return $.when();
},
/**
* @override
*/
start: function () {
if (!this.isPadConfigured) {
this.$(".oe_unconfigured").removeClass('hidden');
this.$(".oe_configured").addClass('hidden');
return;
}
return this._super.apply(this, arguments);
},
this.trigger_up('perform_model_rpc', {
method: 'pad_is_configured',
model: this.model,
on_success: function (data) {
self.set_configured(!!data);
self._configured_deferred.resolve();
},
on_fail: function () {
self.set_configured(true);
self._configured_deferred.resolve();
},
});
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* If we had to generate an url, we wait for the generation to be completed,
* so the current record will be associated with the correct pad url.
*
* @override
*/
commitChanges: function () {
return this.urlDef;
},
render_readonly: function() {
var self = this;
this._configured_deferred.then(function() {
if (self.configured){
if (_.str.startsWith(self.value, 'http')) {
self.trigger_up('perform_model_rpc', {
method: 'pad_get_content',
model: self.model,
args: [self.value],
on_success: function(data) {
self.$('.oe_pad_content').removeClass('oe_pad_loading').html('<div class="oe_pad_readonly"><div>');
self.$('.oe_pad_readonly').html(data);
},
on_fail: function() {
self.$('.oe_pad_content').text(_t('Unable to load pad'));
}
});
} else {
self.$('.oe_pad_content').addClass('oe_pad_loading').show().text(_t("This pad will be initialized on first edit"));
}
}
});
/**
* @override
*/
isSet: function () {
return true;
},
render_edit: function() {
var self = this;
this._configured_deferred.then(function() {
if (! self.value || !_.str.startsWith(self.value, 'http')) {
self.trigger_up('perform_model_rpc', {
method: 'pad_generate_url',
model: self.model,
context: {
model: self.model,
field_name: self.name,
object_id: self.model
},
on_success: function(data) {
if (! data.url) {
self.set_configured(false);
} else {
self.set_value(data.url);
}
self._url_request_deferred.resolve();
},
});
} else {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Note that this method has some serious side effects: performing rpcs and
* setting the value of this field. This is not conventional and should not
* be copied in other code, unless really necessary.
*
* @override
* @private
*/
_renderEdit: function () {
if (_.str.startsWith(this.value, 'http')) {
// here, we have a valid url, so we can simply display an iframe
// with the correct src attribute
var userName = encodeURIComponent(this.getSession().userName);
var url = this.value + '?showChat=false&userName=' + userName;
var content = '<iframe width="100%" height="100%" frameborder="0" src="' + url + '"></iframe>';
this.$('.oe_pad_content').html(content);
} else if (this.value) {
// it looks like the field does not contain a valid url, so we just
// display it (it cannot be edited in that case)
this.$('.oe_pad_content').text(this.value);
} else {
// It is totally discouraged to have a render method that does
// non-rendering work, especially since the work in question
// involves doing RPCs and changing the value of the field.
// However, this is kind of necessary in this case, because the
// value of the field is actually only the url of the pad. The
// actual content will be loaded in an iframe. We could do this
// work in the basic model, but the basic model does not know that
// this widget is in edit or readonly, and we really do not want to
// create a pad url everytime a task without a pad is viewed.
var self = this;
this.urlDef = this._rpc({
method: 'pad_generate_url',
model: this.model,
context: {
model: this.model,
field_name: this.name,
object_id: this.res_id
},
}, {
shadow: true
}).then(function (result) {
// We need to write the url of the pad to trigger
// the write function which updates the actual value
// of the field to the value of the pad content
self.set_value(self.value);
self._url_request_deferred.resolve();
}
self._url_request_deferred.then(function() {
if (_.str.startsWith(self.value, 'http')) {
var content = '<iframe width="100%" height="100%" frameborder="0" src="' + self.value + '?showChat=false&userName=' + encodeURIComponent(self.session.username) + '"></iframe>';
self.$('.oe_pad_content').html(content);
}
else {
self.$('.oe_pad_content').text(self.value);
}
self._setValue(result.url);
});
});
},
set_configured: function(configured) {
this.configured = configured;
if (!configured) {
this.$(".oe_unconfigured").removeClass('hidden');
this.$(".oe_configured").addClass('hidden');
}
},
is_set: function() {
return true;
/**
* @override
* @private
*/
_renderReadonly: function () {
if (_.str.startsWith(this.value, 'http')) {
var self = this;
this.$('.oe_pad_content')
.addClass('oe_pad_loading')
.text(_t("Loading"));
this._rpc({
method: 'pad_get_content',
model: this.model,
args: [this.value]
}, {
shadow: true
}).then(function (data) {
self.$('.oe_pad_content')
.removeClass('oe_pad_loading')
.html('<div class="oe_pad_readonly"><div>');
self.$('.oe_pad_readonly').html(data);
}).fail(function () {
self.$('.oe_pad_content').text(_t('Unable to load pad'));
});
} else {
this.$('.oe_pad_content')
.addClass('oe_pad_loading')
.show()
.text(_t("This pad will be initialized on first edit"));
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_onToggleFullScreen: function () {
this.$el.toggleClass('oe_pad_fullscreen mb0');
this.$('.oe_pad_switch').toggleClass('fa-expand fa-compress');
},
});
field_registry.add('pad', FieldPad);
fieldRegistry.add('pad', FieldPad);
return FieldPad;
});
+151
View File
@@ -0,0 +1,151 @@
odoo.define('pad.pad_tests', function (require) {
"use strict";
var FieldPad = require('pad.pad');
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
QUnit.module('pad widget', {
beforeEach: function () {
this.data = {
task: {
fields: {
description: {string: "Description", type: "char"},
},
records: [
{id: 1, description: false},
{id: 2, description: "https://pad.odoo.pad/p/test-03AK6RCJT"},
],
pad_is_configured: function () {
return true;
},
pad_generate_url: function (route, args) {
return {
url:'https://pad.odoo.pad/p/test/' + args.context.object_id
};
},
pad_get_content: function () {
return "we should rewrite this server in haskell";
},
},
};
},
});
QUnit.test('pad widget display help if server not configured', function (assert) {
assert.expect(4);
var form = createView({
View: FormView,
model: 'task',
data: this.data,
arch:'<form>' +
'<sheet>' +
'<group>' +
'<field name="description" widget="pad"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'pad_is_configured') {
return $.when(false);
}
return this._super.apply(this, arguments);
},
});
assert.ok(form.$('p.oe_unconfigured').is(':visible'),
"help message should be visible");
assert.notOk(form.$('p.oe_pad_content').is(':visible'),
"content should not be visible");
form.$buttons.find('.o_form_button_edit').click();
assert.ok(form.$('p.oe_unconfigured').is(':visible'),
"help message should be visible");
assert.notOk(form.$('p.oe_pad_content').is(':visible'),
"content should not be visible");
form.destroy();
delete FieldPad.prototype.isPadConfigured;
});
QUnit.test('pad widget works, basic case', function (assert) {
assert.expect(5);
var form = createView({
View: FormView,
model: 'task',
data: this.data,
arch:'<form>' +
'<sheet>' +
'<group>' +
'<field name="description" widget="pad"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === 'https://pad.odoo.pad/p/test/1?showChat=false&userName=batman') {
assert.ok(true, "should have an iframe with correct src");
return $.when(true);
}
return this._super.apply(this, arguments);
},
session: {
userName: "batman",
},
});
assert.notOk(form.$('p.oe_unconfigured').is(':visible'),
"help message should not be visible");
assert.ok(form.$('.oe_pad_content').is(':visible'),
"content should be visible");
assert.strictEqual(form.$('.oe_pad_content:contains(This pad will be)').length, 1,
"content should display a message when not initialized");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.oe_pad_content iframe').length, 1,
"should have an iframe");
form.destroy();
delete FieldPad.prototype.isPadConfigured;
});
QUnit.test('pad widget works, with existing data', function (assert) {
assert.expect(2);
var contentDef = $.Deferred();
var form = createView({
View: FormView,
model: 'task',
data: this.data,
arch:'<form>' +
'<sheet>' +
'<group>' +
'<field name="description" widget="pad"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'pad_get_content') {
return contentDef.then(_.constant(result));
}
return result;
},
session: {
userName: "batman",
},
});
assert.strictEqual(form.$('.oe_pad_content').text(), "Loading",
"should display loading message");
contentDef.resolve();
assert.strictEqual(form.$('.oe_pad_content').text(), "we should rewrite this server in haskell",
"should display proper value");
form.destroy();
delete FieldPad.prototype.isPadConfigured;
});
});
+6
View File
@@ -6,4 +6,10 @@
<script type="text/javascript" src="/pad/static/src/js/pad.js" />
</xpath>
</template>
<template id="qunit_suite" name="pad tests" inherit_id="web.qunit_suite">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/pad/static/tests/pad_tests.js"></script>
</xpath>
</template>
</odoo>