[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:
committed by
Yannick Tivisse
parent
0dd6a0b2bf
commit
c1498ff36b
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user