diff --git a/addons/pad/static/src/js/pad.js b/addons/pad/static/src/js/pad.js index 8fb09df0173..76f6ad9b9ae 100644 --- a/addons/pad/static/src/js/pad.js +++ b/addons/pad/static/src/js/pad.js @@ -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('
'); - 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 = ''; + 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 = ''; - 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('
'); + 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; }); diff --git a/addons/pad/static/tests/pad_tests.js b/addons/pad/static/tests/pad_tests.js new file mode 100644 index 00000000000..69342f5de6e --- /dev/null +++ b/addons/pad/static/tests/pad_tests.js @@ -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:'
' + + '' + + '' + + '' + + '' + + '' + + '
', + 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:'
' + + '' + + '' + + '' + + '' + + '' + + '
', + 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:'
' + + '' + + '' + + '' + + '' + + '' + + '
', + 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; + }); + +}); diff --git a/addons/pad/views/pad.xml b/addons/pad/views/pad.xml index 56bc0b1a997..126f8c84f16 100644 --- a/addons/pad/views/pad.xml +++ b/addons/pad/views/pad.xml @@ -6,4 +6,10 @@ + +