diff options
| author | Martin Trigaux <mat@odoo.com> | 2019-07-15 07:07:48 +0000 |
|---|---|---|
| committer | Martin Trigaux <mat@odoo.com> | 2019-07-15 08:12:26 +0000 |
| commit | fd15ca19183a765e1307c4d197bf0dda30b73ab7 (patch) | |
| tree | 896042073ebc752e8bcf535f21696320c69bbe13 /addons/web/static/src | |
| parent | 6b8a0beda65dce688ecd0ee34828097b67a13dab (diff) | |
| parent | 1f5a4649a67908ecb6acfec37673c8149de0471f (diff) | |
[MERGE] Forward port of saas-12.4 to master up to 1f5a4649a67908
closes odoo/odoo#34850
Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
Diffstat (limited to 'addons/web/static/src')
| -rw-r--r-- | addons/web/static/src/js/core/ajax.js | 96 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/basic/basic_model.js | 93 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/basic/basic_renderer.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/list/list_renderer.js | 3 | ||||
| -rw-r--r-- | addons/web/static/src/scss/fields.scss | 4 |
5 files changed, 165 insertions, 35 deletions
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js index b17a3f501e6..bc5250ab620 100644 --- a/addons/web/static/src/js/core/ajax.js +++ b/addons/web/static/src/js/core/ajax.js @@ -2,6 +2,7 @@ odoo.define('web.ajax', function (require) { "use strict"; var config = require('web.config'); +var concurrency = require('web.concurrency'); var core = require('web.core'); var time = require('web.time'); var download = require('web.download'); @@ -9,6 +10,10 @@ var contentdisposition = require('web.contentdisposition'); var _t = core._t; +// Create the final object containing all the functions first to allow monkey +// patching them correctly if ever needed. +var ajax = {}; + function _genericJsonRpc (fct_name, params, settings, fct) { var shadow = settings.shadow || false; delete settings.shadow; @@ -469,44 +474,79 @@ var loadAsset = (function () { })(); /** - * Loads the given js and css libraries. Note that the ajax loadJS and loadCSS methods - * don't do anything if the given file is already loaded. + * Loads the given js/css libraries and asset bundles. Note that no library or + * asset will be loaded if it was already done before. * * @param {Object} libs - * @Param {Array | Array<Array>} [libs.jsLibs=[]] The list of JS files that we want to - * load. The list may contain strings (the files to load), or lists of strings. The - * first level is loaded sequentially, and files listed in inner lists are loaded in - * parallel. - * @param {Array<string>} [libs.cssLibs=[]] A list of css files, to be loaded in - * parallel - * @param {Array<string>} [libs.assetLibs=[]] A list of xmlId. The loaded template - * contains the script and link to be loaded + * @param {Array<string|string[]>} [libs.assetLibs=[]] + * The list of assets to load. Each list item may be a string (the xmlID + * of the asset to load) or a list of strings. The first level is loaded + * sequentially (so use this if the order matters) while the assets in + * inner lists are loaded in parallel (use this for efficiency but only + * if the order does not matter, should rarely be the case for assets). + * @param {string[]} [libs.cssLibs=[]] + * The list of CSS files to load. They will all be loaded in parallel but + * put in the DOM in the given order (only the order in the DOM is used + * to determine priority of CSS rules, not loaded time). + * @param {Array<string|string[]>} [libs.jsLibs=[]] + * The list of JS files to load. Each list item may be a string (the URL + * of the file to load) or a list of strings. The first level is loaded + * sequentially (so use this if the order matters) while the files in inner + * lists are loaded in parallel (use this for efficiency but only + * if the order does not matter). + * @param {string[]} [libs.cssContents=[]] + * List of inline styles to add after loading the CSS files. + * @param {string[]} [libs.jsContents=[]] + * List of inline scripts to add after loading the JS files. * * @returns {Promise} */ -function loadLibs (libs) { - var defs = []; - (libs.jsLibs || []).forEach(function (urls) { - defs.push(Promise.all(defs).then(function () { - if (typeof(urls) === 'string') { - return ajax.loadJS(urls); - } else { - return Promise.all(urls.map(ajax.loadJS)); +function loadLibs(libs) { + var mutex = new concurrency.Mutex(); + mutex.exec(function () { + var defs = []; + var cssLibs = [libs.cssLibs || []]; // Force loading in parallel + defs.push(_loadArray(cssLibs, ajax.loadCSS).then(function () { + if (libs.cssContents && libs.cssContents.length) { + $('head').append($('<style/>', { + html: libs.cssContents.join('\n'), + })); } })); - }); - (libs.cssLibs || []).forEach(function (url) { - defs.push(ajax.loadCSS(url)); - }); - _.each(libs.assetLibs || [], function (xmlId) { - defs.push(loadAsset(xmlId).then(function (asset) { - return loadLibs(asset); + defs.push(_loadArray(libs.jsLibs || [], ajax.loadJS).then(function () { + if (libs.jsContents && libs.jsContents.length) { + $('head').append($('<script/>', { + html: libs.jsContents.join('\n'), + })); + } })); + return Promise.all(defs); + }); + mutex.exec(function () { + return _loadArray(libs.assetLibs || [], function (xmlID) { + return ajax.loadAsset(xmlID).then(function (asset) { + return ajax.loadLibs(asset); + }); + }); }); - return Promise.all(defs); + + function _loadArray(array, loadCallback) { + var _mutex = new concurrency.Mutex(); + array.forEach(function (urlData) { + _mutex.exec(function () { + if (typeof urlData === 'string') { + return loadCallback(urlData); + } + return Promise.all(urlData.map(loadCallback)); + }); + }); + return _mutex.getUnlockedDef(); + } + + return mutex.getUnlockedDef(); } -var ajax = { +_.extend(ajax, { jsonRpc: jsonRpc, rpc: rpc, loadCSS: loadCSS, @@ -516,7 +556,7 @@ var ajax = { loadLibs: loadLibs, get_file: get_file, post: post, -}; +}); return ajax; diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index ca0c7829a2c..b9a7d26e470 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -155,6 +155,10 @@ var BasicModel = AbstractModel.extend({ // save is performed. this.mutex = new concurrency.Mutex(); + // this array is used to accumulate RPC requests done in the same call + // stack, so that they can be batched in the minimum number of RPCs + this.batchedRPCsRequests = []; + this.localData = Object.create(null); this._super.apply(this, arguments); }, @@ -4071,6 +4075,86 @@ var BasicModel = AbstractModel.extend({ }); }, /** + * This function accumulates RPC requests done in the same call stack, and + * performs them in the next micro task tick so that similar requests can be + * batched in a single RPC. + * + * For now, only 'read' calls are supported. + * + * @private + * @param {Object} params + * @returns {Promise} + */ + _performRPC: function (params) { + var self = this; + + // save the RPC request + var request = _.extend({}, params); + var prom = new Promise(function (resolve, reject) { + request.resolve = resolve; + request.reject = reject; + }); + this.batchedRPCsRequests.push(request); + + // empty the pool of RPC requests in the next micro tick + Promise.resolve().then(function () { + if (!self.batchedRPCsRequests.length) { + // pool has already been processed + return; + } + + // reset pool of RPC requests + var batchedRPCsRequests = self.batchedRPCsRequests; + self.batchedRPCsRequests = []; + + // batch similar requests + var batches = {}; + var key; + for (var i = 0; i < batchedRPCsRequests.length; i++) { + var request = batchedRPCsRequests[i]; + key = request.model + ',' + JSON.stringify(request.context); + if (!batches[key]) { + batches[key] = _.extend({}, request, {requests: [request]}); + } else { + batches[key].ids = _.uniq(batches[key].ids.concat(request.ids)); + batches[key].fieldNames = _.uniq(batches[key].fieldNames.concat(request.fieldNames)); + batches[key].requests.push(request); + } + } + + // perform batched RPCs + function onSuccess(batch, results) { + for (var i = 0; i < batch.requests.length; i++) { + var request = batch.requests[i]; + var fieldNames = request.fieldNames.concat(['id']); + var filteredResults = results.filter(function (record) { + return request.ids.indexOf(record.id) >= 0; + }).map(function (record) { + return _.pick(record, fieldNames); + }); + request.resolve(filteredResults); + } + } + function onFailure(batch, error) { + for (var i = 0; i < batch.requests.length; i++) { + var request = batch.requests[i]; + request.reject(error); + } + } + for (key in batches) { + var batch = batches[key]; + self._rpc({ + model: batch.model, + method: 'read', + args: [batch.ids, batch.fieldNames], + context: batch.context, + }).then(onSuccess.bind(null, batch)).guardedCatch(onFailure.bind(null, batch)); + } + }); + + return prom; + }, + /** * Once a record is created and some data has been fetched, we need to do * quite a lot of computations to determine what needs to be fetched. This * method is doing that. @@ -4277,11 +4361,12 @@ var BasicModel = AbstractModel.extend({ var def; if (missingIDs.length && fieldNames.length) { - def = self._rpc({ - model: list.model, - method: 'read', - args: [missingIDs, fieldNames], + def = self._performRPC({ context: list.getContext(), + fieldNames: fieldNames, + ids: missingIDs, + method: 'read', + model: list.model, }); } else { def = Promise.resolve(_.map(missingIDs, function (id) { diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js index 6ffe35574bd..f73209a0330 100644 --- a/addons/web/static/src/js/views/basic/basic_renderer.js +++ b/addons/web/static/src/js/views/basic/basic_renderer.js @@ -651,8 +651,8 @@ var BasicRenderer = AbstractRenderer.extend({ // this.renderFieldWidget(...).addClass(...), the class is added on // the temporary div and not on the actual element that will be // rendered. As we do not return a promise and some callers cannot - // wait for this.defs, we copy those attributes to the final element. - widget.$el.attr($el.getAttributes()); + // wait for this.defs, we copy those classnames to the final element. + widget.$el.addClass($el.attr('class')); $el.replaceWith(widget.$el); self._registerModifiers(node, record, widget, { diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index 23a26bb6d73..ad1a95cd14f 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -405,7 +405,8 @@ var ListRenderer = BasicRenderer.extend({ isPassword: 'password' in node.attrs, }); this._handleAttributes($td, node); - return $td.html(formattedValue).attr('title', formattedValue); + var title = field.type !== 'boolean' ? formattedValue : ''; + return $td.html(formattedValue).attr('title', title); }, /** * Renders the button element associated to the given node and record. diff --git a/addons/web/static/src/scss/fields.scss b/addons/web/static/src/scss/fields.scss index 55559546e74..120896dceee 100644 --- a/addons/web/static/src/scss/fields.scss +++ b/addons/web/static/src/scss/fields.scss @@ -88,6 +88,7 @@ &.o_field_many2manytags { flex-flow: row wrap; align-items: baseline; + max-width: 100%; > .o_field_widget { flex: 1 0 100px; @@ -101,6 +102,8 @@ font-size: 12px; cursor: pointer; user-select: none; + display: flex; + max-width: 100%; a { color: inherit; @@ -115,6 +118,7 @@ .o_delete { color: inherit; cursor: pointer; + padding-left: 4px; } } |
