[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.
This commit is contained in:
eco-odoo
2018-07-20 11:59:12 +02:00
committed by Yannick Tivisse
parent 0dd6a0b2bf
commit c1498ff36b
7 changed files with 210 additions and 4 deletions
@@ -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 <head>
* (and will only be fetched once).
*
* Route example:
* @http.route('/module/hello', auth='user', type='json')
* def hello(self):
* return {'html': '<h1>hello, world</h1>'}
*
* @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 <head> 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 <a> link with type="action", we need to actually
* do the action. This kind of links is used a lot in no-content helpers.
*
* The <a> 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,
@@ -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
+5
View File
@@ -0,0 +1,5 @@
.o_has_banner {
.o_view_nocontent {
top: 30%;
}
}
@@ -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 =
'<div>' +
'<link type="text/css" href="' + test_css_url + '" rel="stylesheet">' +
'<div class="hello_banner"/>' +
'</div>';
createAsyncView({
View: TestView,
model: 'test_model',
data: this.data,
arch: '<test banner_route="/module/hello_banner"/>',
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();
});
});
}
);
});
+2
View File
@@ -52,6 +52,7 @@
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/animation.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/rainbow.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/datepicker.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/banner.scss"/>
<script type="text/javascript" src="/web/static/lib/es5-shim/es5-shim.min.js"></script>
<script type="text/javascript" src="/web/static/lib/underscore/underscore.js"></script>
@@ -528,6 +529,7 @@
<script type="text/javascript" src="/web/static/tests/views/kanban_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/calendar_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/basic_model_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/abstract_view_banner_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/kanban_model_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/view_dialogs_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/search_filters_menu_tests.js"></script>
+39
View File
@@ -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<reference/http/controllers>` 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 <link> tag, it will be
removed and appended to <head>.
To interact with the backend you can use <a type="action"> 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
<tree banner_route="/module_name/hello" />
.. code-block:: python
class MyController(odoo.http.Controller):
@http.route('/module_name/hello', auth='user', type='json')
def hello(self):
return {
'html': """
<div>
<link href="/module_name/static/src/css/banner.css"
rel="stylesheet">
<h1>hello, world</h1>
</div> """
}
.. _reference/views/inheritance:
+1
View File
@@ -30,6 +30,7 @@
<rng:optional><rng:attribute name="decoration-primary"/></rng:optional>
<rng:optional><rng:attribute name="decoration-success"/></rng:optional>
<rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
<rng:optional><rng:attribute name="banner_route"/></rng:optional>
<rng:optional>
<rng:attribute name="limit">
<rng:data type="int"/>