diff options
| author | Xavier Morel <xmo@odoo.com> | 2018-10-26 14:32:46 +0000 |
|---|---|---|
| committer | Xavier Morel <xmo@odoo.com> | 2019-01-29 10:22:18 +0000 |
| commit | 58e0c88eced65fe8cc8929d5ea7350f817a5a90e (patch) | |
| tree | f1256f81ad9108239426d1f9aa4f4f5f0042fdc3 /addons/web/static | |
| parent | 1d1d5ed7adfdf8352ddb960293c6c07cd51af51d (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.js | 5 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/qweb/qweb_view.js | 202 | ||||
| -rw-r--r-- | addons/web/static/src/scss/webclient.scss | 7 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/mock_server.js | 12 | ||||
| -rw-r--r-- | addons/web/static/tests/qweb_tests.js | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/views/qweb_tests.js | 70 |
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(); + } + }); +}); +}); |
