From c1498ff36be4ce0b175f340e2f5690a37421ccd7 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Tue, 3 Jul 2018 10:28:44 +0200 Subject: [PATCH] [IMP] web: add option to display a banner above a view Add a banner_route attribute to AbstractView to allow fetching html from a controller route and displaying it above the view. The initial use case was the onboarding panels. Supported views are the ones inheriting both AbstractView and AbstractController. --- .../src/js/views/abstract_controller.js | 84 ++++++++++++++++++- .../web/static/src/js/views/abstract_view.js | 1 + addons/web/static/src/scss/banner.scss | 5 ++ .../tests/views/abstract_view_banner_tests.js | 82 ++++++++++++++++++ addons/web/views/webclient_templates.xml | 2 + doc/reference/views.rst | 39 +++++++++ odoo/addons/base/rng/tree_view.rng | 1 + 7 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 addons/web/static/src/scss/banner.scss create mode 100644 addons/web/static/tests/views/abstract_view_banner_tests.js diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index bf4f3a616f3..f4f323b01ba 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -12,6 +12,7 @@ odoo.define('web.AbstractController', function (require) { * reading localstorage, ...) has to go through the controller. */ +var ajax = require('web.ajax'); var concurrency = require('web.concurrency'); var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); @@ -54,7 +55,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { this.activeActions = params.activeActions; this.controllerID = params.controllerID; this.initialState = params.initialState; - + this.bannerRoute = params.bannerRoute; // use a DropPrevious to correctly handle concurrent updates this.dp = new concurrency.DropPrevious(); // those arguments are temporary, they won't be necessary as soon as the @@ -280,6 +281,54 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { state: state || {}, }); }, + /** + * Renders the html provided by the route specified by the + * bannerRoute attribute on the controller (banner_route in the template). + * Renders it before the view output and add a css class 'o_has_banner' to it. + * There can be only one banner displayed at a time. + * + * If the banner contains stylesheet links, they are moved to + * (and will only be fetched once). + * + * Route example: + * @http.route('/module/hello', auth='user', type='json') + * def hello(self): + * return {'html': '

hello, world

'} + * + * @private + * @returns {Deferred} + */ + _renderBanner: function () { + if (this.bannerRoute !== undefined) { + var self = this; + return this.dp + .add(this._rpc({route: this.bannerRoute})) + .then(function (response) { + if (!response.html) { + self.$el.removeClass('o_has_banner'); + return $.when(); + } + self.$el.addClass('o_has_banner'); + var $banner = $(response.html); + // we should only display one banner at a time + if (self._$banner && self._$banner.remove) { + self._$banner.remove(); + } + // Stylesheets are moved to and we wait for them to be loaded + // to prevent displaying unstyled content. + var defs = []; + $('link[rel="stylesheet"]', $banner).each(function (i, link) { + defs.push(ajax.loadCSS(link.href)); + link.remove(); + }); + return $.when.apply($, defs).then(function () { + $banner.prependTo(self.$el); + self._$banner = $banner; + }); + }); + } + return $.when(); + }, /** * Renders the control elements (buttons, pager and sidebar) of the current * view. @@ -370,7 +419,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { }); this._pushState(); - return $.when(); + return this._renderBanner(); }, //-------------------------------------------------------------------------- @@ -405,12 +454,39 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { * When a user clicks on an link with type="action", we need to actually * do the action. This kind of links is used a lot in no-content helpers. * + * The may have + * - a data-method and data-model attribute, in that case the corresponding + * rpc will be called. If that rpc returns an action it will be executed. + * - a data-reload-on-close attribute, in that case the view will be + * reloaded after the dialog has been closed. + * * @private * @param {OdooEvent} event */ _onActionClicked: function (event) { - event.preventDefault(); - this.do_action(event.currentTarget.name); + var $target = $(event.currentTarget); + var self = this; + var model = $target.data('model'); + var method = $target.data('method'); + + if (method !== undefined && model !== undefined) { + var options = {}; + if ($target.data('reload-on-close')) { + options.on_close = function () { + self.trigger_up('reload'); + }; + } + this.dp.add(this._rpc({ + model: model, + method: method, + })).then(function (action) { + if (action !== undefined) { + self.do_action(action, options); + } + }); + } else { + this.do_action($target.attr('name')); + } }, /** * Intercepts the 'switch_view' event to add the controllerID into the data, diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index fe95ada0a7f..7b1d21e6ea5 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -121,6 +121,7 @@ var AbstractView = Class.extend({ groupable: this.groupable, isEmbedded: this.isEmbedded, controllerID: params.controllerID, + bannerRoute: this.arch.attrs.banner_route, }; // AAB: these params won't be necessary as soon as the ControlPanel will // be instantiated by the View diff --git a/addons/web/static/src/scss/banner.scss b/addons/web/static/src/scss/banner.scss new file mode 100644 index 00000000000..b119d9ab273 --- /dev/null +++ b/addons/web/static/src/scss/banner.scss @@ -0,0 +1,5 @@ +.o_has_banner { + .o_view_nocontent { + top: 30%; + } +} diff --git a/addons/web/static/tests/views/abstract_view_banner_tests.js b/addons/web/static/tests/views/abstract_view_banner_tests.js new file mode 100644 index 00000000000..21532ed60d9 --- /dev/null +++ b/addons/web/static/tests/views/abstract_view_banner_tests.js @@ -0,0 +1,82 @@ +odoo.define('web.abstract_view_banner_tests', function (require) { +"use strict"; + +var AbstractRenderer = require('web.AbstractRenderer'); +var AbstractView = require('web.AbstractView'); + +var testUtils = require('web.test_utils'); +var createAsyncView = testUtils.createAsyncView; + +var TestRenderer = AbstractRenderer.extend({ + _renderView: function () { + this.$el.addClass('test_content'); + return this._super(); + }, +}); + +var TestView = AbstractView.extend({ + type: 'test', + config: _.extend({}, AbstractView.prototype.config, { + Renderer: TestRenderer + }), +}); + +var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css'; + +QUnit.module('Views', { + beforeEach: function () { + this.data = { + test_model: { + fields: {}, + records: [], + }, + }; + }, + afterEach: function () { + $('head link[href$="' + test_css_url + '"]').remove(); + } + }, function () { + QUnit.module('BasicRenderer'); + + QUnit.test("The banner should be fetched from the route", function (assert) { + var done = assert.async(); + assert.expect(5); + + var banner_html = + '
' + + '' + + '
' + + '
'; + + createAsyncView({ + View: TestView, + model: 'test_model', + data: this.data, + arch: '', + mockRPC: function (route, args) { + if (route === '/module/hello_banner') { + assert.step(route); + return $.when({html: banner_html}); + } + return this._super(route, args); + }, + }).then(function (view) { + var $banner = view.$('.hello_banner'); + assert.strictEqual($banner.length, 1, + "The view should contain the response from the controller."); + assert.verifySteps(['/module/hello_banner'], "The banner should be fetched."); + + var $head_link = $('head link[href$="' + test_css_url + '"]'); + assert.strictEqual($head_link.length, 1, + "The stylesheet should have been added to head."); + + var $banner_link = $('link[href$="' + test_css_url + '"]', $banner); + assert.strictEqual($banner_link.length, 0, + "The stylesheet should have been removed from the banner."); + view.destroy(); + done(); + }); + }); + } +); +}); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index d57bad4e2e9..1ab7d27e26a 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -52,6 +52,7 @@ + @@ -528,6 +529,7 @@ + diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 08848604926..8a733dedee4 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -44,6 +44,45 @@ otherwise. ``application`` website feature defining togglable views. By default, views are always applied +``banner_route`` + a route address to be fetched and prepended to the view. + + If this attribute is set, the + :ref:`controller route url` will be fetched and + displayed above the view. The json response from the controller should + contain an "html" key. + + If the html contains a stylesheet tag, it will be + removed and appended to . + + To interact with the backend you can use
tags. Please take + a look at the documentation of the _onActionClicked method of + AbstractController (*addons/web/static/src/js/views/abstract_controller.js*) + for more details. + + Only views extending AbstractView and AbstractController can use this + attribute, like :ref:`reference/views/form`, :ref:`reference/views/kanban`, + :ref:`reference/views/list`, ... + + Example: + + .. code-block:: xml + + + + .. code-block:: python + + class MyController(odoo.http.Controller): + @http.route('/module_name/hello', auth='user', type='json') + def hello(self): + return { + 'html': """ +
+ +

hello, world

+
""" + } .. _reference/views/inheritance: diff --git a/odoo/addons/base/rng/tree_view.rng b/odoo/addons/base/rng/tree_view.rng index 068f88d909a..ff758cb7ed3 100644 --- a/odoo/addons/base/rng/tree_view.rng +++ b/odoo/addons/base/rng/tree_view.rng @@ -30,6 +30,7 @@ +