summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
authorXavier Morel <xmo@odoo.com>2018-10-26 14:32:46 +0000
committerXavier Morel <xmo@odoo.com>2019-01-29 10:22:18 +0000
commit58e0c88eced65fe8cc8929d5ea7350f817a5a90e (patch)
treef1256f81ad9108239426d1f9aa4f4f5f0042fdc3 /addons/web/static
parent1d1d5ed7adfdf8352ddb960293c6c07cd51af51d (diff)
[ADD] web: server-side-rendered qweb templates as views
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/js/views/abstract_controller.js5
-rw-r--r--addons/web/static/src/js/views/qweb/qweb_view.js202
-rw-r--r--addons/web/static/src/scss/webclient.scss7
-rw-r--r--addons/web/static/tests/helpers/mock_server.js12
-rw-r--r--addons/web/static/tests/qweb_tests.js2
-rw-r--r--addons/web/static/tests/views/qweb_tests.js70
6 files changed, 295 insertions, 3 deletions
diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js
index 57b3771427b..ba00b4dbab7 100644
--- a/addons/web/static/src/js/views/abstract_controller.js
+++ b/addons/web/static/src/js/views/abstract_controller.js
@@ -456,7 +456,10 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
}
});
} else if ($target.attr('name')) {
- this.do_action($target.attr('name'));
+ this.do_action(
+ $target.attr('name'),
+ data.context && {additional_context: data.context}
+ );
} else {
this.do_action({
name: $target.attr('title') || _.str.strip($target.text()),
diff --git a/addons/web/static/src/js/views/qweb/qweb_view.js b/addons/web/static/src/js/views/qweb/qweb_view.js
new file mode 100644
index 00000000000..9e79ecf19d2
--- /dev/null
+++ b/addons/web/static/src/js/views/qweb/qweb_view.js
@@ -0,0 +1,202 @@
+/**
+ * Client-side implementation of a qweb view.
+ */
+odoo.define('web.qweb', function (require) {
+"use strict";
+
+var core = require('web.core');
+var AbstractView = require('web.AbstractView');
+var AbstractModel = require('web.AbstractModel');
+var AbstractRenderer = require('web.AbstractRenderer');
+var AbstractController = require('web.AbstractController');
+var registry = require('web.view_registry');
+
+var _lt = core._lt;
+
+/**
+ * model
+ */
+var Model = AbstractModel.extend({
+ /**
+ * init
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this._state = {
+ viewId: false,
+ modelName: false,
+ body: '',
+ context: {},
+ domain: [],
+ };
+ },
+ /**
+ * fetches the rendered qweb view
+ */
+ _fetch: function () {
+ var state = this._state;
+ return this._rpc({
+ model: state.modelName,
+ method: 'qweb_render_view',
+ kwargs: {
+ view_id: state.viewId,
+ domain: state.domain,
+ context: state.context
+ }
+ }).then(function (r) {
+ state.body = r;
+ return state.viewId;
+ });
+ },
+ /**
+ * get
+ */
+ get: function () {
+ return this._state;
+ },
+ /**
+ * load
+ */
+ load: function (params) {
+ _.extend(this._state, _.pick(params, ['viewId', 'modelName', 'domain', 'context']));
+
+ return this._fetch();
+ },
+ /**
+ * reload
+ */
+ reload: function (_id, _params) {
+ // FIXME: update this._state or something?
+ return this._fetch();
+ }
+});
+/**
+ * renderer
+ */
+var Renderer = AbstractRenderer.extend({
+ /**
+ * render
+ */
+ _render: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ self.$el.html(self.state.body);
+ });
+ }
+});
+/**
+ * controller
+ */
+var Controller = AbstractController.extend({
+ events: _.extend({}, AbstractController.prototype.events, {
+ 'click [type="toggle"]': '_onLazyToggle',
+ }),
+
+ init: function () {
+ this._super.apply(this, arguments);
+ this._$buttons = $('<nav>').on('click', '[type="action"]', this.proxy('_onActionClicked'));
+ },
+
+ renderButtons: function ($node) {
+ this._super($node);
+ $node.append(this._$buttons);
+ },
+ _update: function () {
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ // move control panel buttons from the view to the control panel
+ // area
+ var $cp_buttons = self.renderer.$('nav.o_qweb_cp_buttons');
+ $cp_buttons.children().appendTo(self._$buttons.empty());
+ $cp_buttons.remove();
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Lazy toggle. Content is not remembered between unfolds.
+ */
+ _onLazyToggle: function (e) {
+ // TODO: add support for view (possibly action as well?)
+ var $target = $(e.target);
+ var $t = $target.closest('[data-model]');
+ if (!($target.hasClass('fa-caret-down') || $target.hasClass('fa-caret-right'))) {
+ $target = $t.find('.fa-caret-down, .fa-caret-right');
+ }
+
+ var data = $t.data();
+ if (this._fold($t)) {
+ $target.removeClass('fa-caret-down').addClass('fa-caret-right');
+ return;
+ }
+
+ // NB: $.data() automatically parses json attributes, but does not
+ // automatically parse lone float literals in data-*, so a
+ // data-args (as a json object) is very convenient
+ var args = data.args || _.omit(data, 'model', 'method', 'id');
+
+ return this._rpc({
+ model: data.model,
+ method: data.method,
+ args: data.id ? [data.id] : undefined,
+ kwargs: args // FIXME: context?
+ }).then(function (s) {
+ return $(s);
+ }).then(function ($newcontent) {
+ $t.data('children', $newcontent).after($newcontent);
+ $target.removeClass('fa-caret-right').addClass('fa-caret-down');
+ });
+ },
+ /**
+ * Attempts to fold the parameter, returns whether that happened.
+ */
+ _fold: function ($el) {
+ var $children = $el.data('children');
+ if (!$children) {
+ return false;
+ }
+
+ var self = this;
+ $children.each(function (_i, e) {
+ self._fold($(e));
+ }).remove();
+ $el.removeData('children');
+ return true;
+ }
+});
+
+/**
+ * view
+ */
+var QWebView = AbstractView.extend({
+ display_name: _lt('Freedom View'),
+ icon: 'fa-file-picture-o',
+ viewType: 'qweb',
+ // groupable?
+ enableTimeRangeMenu: true,
+ config: {
+ Model: Model,
+ Renderer: Renderer,
+ Controller: Controller,
+ },
+
+ /**
+ * init method
+ */
+ init: function (viewInfo, params) {
+ this._super.apply(this, arguments);
+ this.loadParams.viewId = viewInfo.view_id;
+ }
+});
+
+registry.add('qweb', QWebView);
+return {
+ View: QWebView,
+ Controller: Controller,
+ Renderer: Renderer,
+ Model: Model
+};
+});
diff --git a/addons/web/static/src/scss/webclient.scss b/addons/web/static/src/scss/webclient.scss
index 3dfebe8deed..436c8074302 100644
--- a/addons/web/static/src/scss/webclient.scss
+++ b/addons/web/static/src/scss/webclient.scss
@@ -182,3 +182,10 @@ div.o_boolean_toggle.custom-control.custom-checkbox {
overflow-x: initial;
}
}
+
+//== Action manager
+// ensure special links are styled as pointers even when they don't
+// have an href
+[type="action"], [type="toggle"] {
+ cursor: pointer !important;
+}
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 75f8afeeff2..85d3a8b8119 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -70,6 +70,7 @@ var MockServer = Class.extend({
*
* @param {Object} params
* @param {string|Object} params.arch a string OR a parsed xml document
+ * @param {Number} [params.view_id] the id of the arch's view
* @param {string} params.model a model name (that should be in this.data)
* @param {Object} params.toolbar the actions possible in the toolbar
* @param {Object} [params.viewOptions] the view options set in the test (optional)
@@ -78,6 +79,7 @@ var MockServer = Class.extend({
fieldsViewGet: function (params) {
var model = params.model;
var toolbar = params.toolbar;
+ var viewId = params.view_id;
var viewOptions = params.viewOptions || {};
if (!(model in this.data)) {
throw new Error('Model ' + model + ' was not defined in mock server data');
@@ -87,6 +89,9 @@ var MockServer = Class.extend({
if (toolbar) {
fvg.toolbar = toolbar;
}
+ if (viewId) {
+ fvg.view_id = viewId;
+ }
return fvg;
},
/**
@@ -506,12 +511,17 @@ var MockServer = Class.extend({
}
}
var key = [model, viewID, viewType].join(',');
- var arch = self.archs[key];
+ var arch = self.archs[key] || _.find(self.archs, function (_v, k) {
+ var ka = k.split(',');
+ viewID = parseInt(ka[1], 10);
+ return ka[0] === model && ka[2] === viewType;
+ });
if (!arch) {
throw new Error('No arch found for key ' + key);
}
views[viewType] = {
arch: arch,
+ view_id: viewID,
model: model,
viewOptions: {
context: kwargs.context,
diff --git a/addons/web/static/tests/qweb_tests.js b/addons/web/static/tests/qweb_tests.js
index b31c604b4bf..fbb710d8cfc 100644
--- a/addons/web/static/tests/qweb_tests.js
+++ b/addons/web/static/tests/qweb_tests.js
@@ -1,4 +1,4 @@
-odoo.define('web.qweb', function (require) {
+odoo.define('web.qweb_tests', function (require) {
"use strict";
var qwebPath = '/web/static/lib/qweb/';
diff --git a/addons/web/static/tests/views/qweb_tests.js b/addons/web/static/tests/views/qweb_tests.js
new file mode 100644
index 00000000000..e9b7398fd09
--- /dev/null
+++ b/addons/web/static/tests/views/qweb_tests.js
@@ -0,0 +1,70 @@
+odoo.define('web.qweb_view_tests', function (require) {
+"use strict";
+
+var qweb = require('web.qweb');
+var utils = require('web.test_utils');
+
+QUnit.module("Views", {
+
+}, function () {
+ QUnit.module("QWeb");
+ QUnit.test("basic", function (assert) {
+ assert.expect(13);
+ var am = utils.createActionManager({
+ data: {
+ test: {
+ fields: {},
+ records: [],
+ }
+ },
+ archs: {
+ 'test,5,qweb': '<div id="xxx"><t t-esc="ok"/></div>',
+ 'test,false,search': '<search/>'
+ },
+ mockRPC: function (route, args) {
+ if (/^\/web\/dataset\/call_kw/.test(route)) {
+ switch (_.str.sprintf('%(model)s.%(method)s', args)) {
+ case 'test.qweb_render_view':
+ assert.step('fetch');
+ assert.equal(args.kwargs.view_id, 5);
+ return $.when(
+ '<div>foo' +
+ '<div data-model="test" data-method="wheee" data-id="42" data-other="5">' +
+ '<a type="toggle" class="fa fa-caret-right">Unfold</a>' +
+ '</div>' +
+ '</div>'
+ );
+ case 'test.wheee':
+ assert.step('unfold');
+ assert.deepEqual(args.args, [42]);
+ assert.deepEqual(args.kwargs, {other: 5});
+ return $.when('<div id="sub">ok</div>');
+ }
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ try {
+ var resolved = false;
+ am.doAction({
+ type: 'ir.actions.act_window',
+ views: [[false, 'qweb']],
+ res_model: 'test',
+ }).then(function () { resolved = true; });
+ assert.ok(resolved, "Action should have resolved synchronously");
+
+ var $content = am.$('.o_content');
+ assert.ok(/^\s*foo/.test($content.text()));
+ utils.dom.click($content.find('[type=toggle]'));
+ assert.equal($content.find('div#sub').text(), 'ok', 'should have unfolded the sub-item');
+ utils.dom.click($content.find('[type=toggle]'));
+ assert.equal($content.find('div#sub').length, 0, "should have removed the sub-item");
+ utils.dom.click($content.find('[type=toggle]'));
+
+ assert.verifySteps(['fetch', 'unfold', 'unfold']);
+ } finally {
+ am.destroy();
+ }
+ });
+});
+});