diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/services | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/services')
| -rw-r--r-- | addons/web/static/src/js/services/ajax_service.js | 41 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/config.js | 122 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/core.js | 49 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/crash_manager.js | 412 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/crash_manager_service.js | 9 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/data_manager.js | 225 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/local_storage_service.js | 20 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/notification_service.js | 111 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/report_service.js | 35 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/session.js | 12 | ||||
| -rw-r--r-- | addons/web/static/src/js/services/session_storage_service.js | 20 |
11 files changed, 1056 insertions, 0 deletions
diff --git a/addons/web/static/src/js/services/ajax_service.js b/addons/web/static/src/js/services/ajax_service.js new file mode 100644 index 00000000..da3d436c --- /dev/null +++ b/addons/web/static/src/js/services/ajax_service.js @@ -0,0 +1,41 @@ +odoo.define('web.AjaxService', function (require) { +"use strict"; + +var AbstractService = require('web.AbstractService'); +var ajax = require('web.ajax'); +var core = require('web.core'); +var session = require('web.session'); + +var AjaxService = AbstractService.extend({ + /** + * @param {Object} libs - @see ajax.loadLibs + * @param {Object} [context] - @see ajax.loadLibs + * @param {Object} [tplRoute] - @see ajax.loadLibs + */ + loadLibs: function (libs, context, tplRoute) { + return ajax.loadLibs(libs, context, tplRoute); + }, + rpc: function (route, args, options, target) { + var rpcPromise; + var promise = new Promise(function (resolve, reject) { + rpcPromise = session.rpc(route, args, options); + rpcPromise.then(function (result) { + if (!target.isDestroyed()) { + resolve(result); + } + }).guardedCatch(function (reason) { + if (!target.isDestroyed()) { + reject(reason); + } + }); + }); + promise.abort = rpcPromise.abort.bind(rpcPromise); + return promise; + }, +}); + +core.serviceRegistry.add('ajax', AjaxService); + +return AjaxService; + +}); diff --git a/addons/web/static/src/js/services/config.js b/addons/web/static/src/js/services/config.js new file mode 100644 index 00000000..6c576c35 --- /dev/null +++ b/addons/web/static/src/js/services/config.js @@ -0,0 +1,122 @@ +odoo.define('web.config', function (require) { +"use strict"; + +const Bus = require('web.Bus'); + +const bus = new Bus(); + +/** + * This module contains all the (mostly) static 'environmental' information. + * This is often necessary to allow the rest of the web client to properly + * render itself. + * + * Note that many information currently stored in session should be moved to + * this file someday. + */ + +var config = { + device: { + /** + * bus to use in order to be able to handle device config related events + * - 'size_changed' : triggered when window size is + * corresponding to a new bootstrap breakpoint. The new size_class + * is provided. + */ + bus: bus, + /** + * touch is a boolean, true if the device supports touch interaction + * + * @type Boolean + */ + touch: 'ontouchstart' in window || 'onmsgesturechange' in window, + /** + * size_class is an integer: 0, 1, 2, 3 or 4, depending on the (current) + * size of the device. This is a dynamic property, updated whenever the + * browser is resized + * + * @type Number + */ + size_class: null, + /** + * A frequent use case is to have a different render in 'mobile' mode, + * meaning when the screen is small. This flag (boolean) is true when + * the size is XS/VSM/SM. It is also updated dynamically. + * + * @type Boolean + */ + isMobile: null, + /** + * Mobile device detection using userAgent. + * This flag doesn't depend on the size/resolution of the screen. + * It targets mobile devices which suggests that there is a virtual keyboard. + * + * @return {boolean} + */ + isMobileDevice: navigator.userAgent.match(/Android/i) || + navigator.userAgent.match(/webOS/i) || + navigator.userAgent.match(/iPhone/i) || + navigator.userAgent.match(/iPad/i) || + navigator.userAgent.match(/iPod/i) || + navigator.userAgent.match(/BlackBerry/i) || + navigator.userAgent.match(/Windows Phone/i), + /** + * Mapping between the numbers 0,1,2,3,4,5,6 and some descriptions + */ + SIZES: { XS: 0, VSM: 1, SM: 2, MD: 3, LG: 4, XL: 5, XXL: 6 }, + }, + /** + * States whether the current environment is in debug or not. + * + * @param debugMode the debug mode to check, empty for simple debug mode + * @returns {boolean} + */ + isDebug: function (debugMode) { + if (debugMode) { + return odoo.debug && odoo.debug.indexOf(debugMode) !== -1; + } + return odoo.debug; + }, +}; + + +var medias = [ + window.matchMedia('(max-width: 474px)'), + window.matchMedia('(min-width: 475px) and (max-width: 575px)'), + window.matchMedia('(min-width: 576px) and (max-width: 767px)'), + window.matchMedia('(min-width: 768px) and (max-width: 991px)'), + window.matchMedia('(min-width: 992px) and (max-width: 1199px)'), + window.matchMedia('(min-width: 1200px) and (max-width: 1533px)'), + window.matchMedia('(min-width: 1534px)'), +]; + +/** + * Return the current size class + * + * @returns {integer} a number between 0 and 5, included + */ +function _getSizeClass() { + for (var i = 0 ; i < medias.length ; i++) { + if (medias[i].matches) { + return i; + } + } +} +/** + * Update the size dependant properties in the config object. This method + * should be called every time the size class changes. + */ +function _updateSizeProps() { + var sc = _getSizeClass(); + if (sc !== config.device.size_class) { + config.device.size_class = sc; + config.device.isMobile = config.device.size_class <= config.device.SIZES.SM; + config.device.bus.trigger('size_changed', config.device.size_class); + } +} + +_.invoke(medias, 'addListener', _updateSizeProps); +_updateSizeProps(); + +return config; + +}); diff --git a/addons/web/static/src/js/services/core.js b/addons/web/static/src/js/services/core.js new file mode 100644 index 00000000..1d4619f6 --- /dev/null +++ b/addons/web/static/src/js/services/core.js @@ -0,0 +1,49 @@ +odoo.define('web.core', function (require) { +"use strict"; + +var Bus = require('web.Bus'); +var config = require('web.config'); +var Class = require('web.Class'); +var QWeb = require('web.QWeb'); +var Registry = require('web.Registry'); +var translation = require('web.translation'); + +/** + * Whether the client is currently in "debug" mode + * + * @type Boolean + */ +var bus = new Bus(); + +_.each('click,dblclick,keydown,keypress,keyup'.split(','), function (evtype) { + $('html').on(evtype, function (ev) { + bus.trigger(evtype, ev); + }); +}); +_.each('resize,scroll'.split(','), function (evtype) { + $(window).on(evtype, function (ev) { + bus.trigger(evtype, ev); + }); +}); + +return { + qweb: new QWeb(config.isDebug()), + + // core classes and functions + Class: Class, + bus: bus, + main_bus: new Bus(), + _t: translation._t, + _lt: translation._lt, + + // registries + action_registry: new Registry(), + crash_registry: new Registry(), + serviceRegistry: new Registry(), + /** + * @type {String} + */ + csrf_token: odoo.csrf_token, +}; + +}); diff --git a/addons/web/static/src/js/services/crash_manager.js b/addons/web/static/src/js/services/crash_manager.js new file mode 100644 index 00000000..abd40e93 --- /dev/null +++ b/addons/web/static/src/js/services/crash_manager.js @@ -0,0 +1,412 @@ +odoo.define('web.ErrorDialogRegistry', function (require) { +"use strict"; + +var Registry = require('web.Registry'); + +return new Registry(); +}); + +odoo.define('web.CrashManager', function (require) { +"use strict"; + +const AbstractService = require('web.AbstractService'); +var ajax = require('web.ajax'); +const BrowserDetection = require('web.BrowserDetection'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var ErrorDialogRegistry = require('web.ErrorDialogRegistry'); +var Widget = require('web.Widget'); + +var _t = core._t; +var _lt = core._lt; + +// Register this eventlistener before qunit does. +// Some errors needs to be negated by the crash_manager. +window.addEventListener('unhandledrejection', ev => + core.bus.trigger('crash_manager_unhandledrejection', ev) +); + +let active = true; + +/** + * An extension of Dialog Widget to render the warnings and errors on the website. + * Extend it with your template of choice like ErrorDialog/WarningDialog + */ +var CrashManagerDialog = Dialog.extend({ + xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat( + ['/web/static/src/xml/crash_manager.xml'] + ), + + /** + * @param {Object} error + * @param {string} error.message the message in Warning/Error Dialog + * @param {string} error.traceback the traceback in ErrorDialog + * + * @constructor + */ + init: function (parent, options, error) { + this._super.apply(this, [parent, options]); + this.message = error.message; + this.traceback = error.traceback; + core.bus.off('close_dialogs', this); + }, +}); + +var ErrorDialog = CrashManagerDialog.extend({ + template: 'CrashManager.error', +}); + +var WarningDialog = CrashManagerDialog.extend({ + template: 'CrashManager.warning', + + /** + * Sets size to medium by default. + * + * @override + */ + init: function (parent, options, error) { + this._super(parent, _.extend({ + size: 'medium', + }, options), error); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Focuses the ok button. + * + * @override + */ + open: function () { + this._super({shouldFocusButtons: true}); + }, +}); + +var CrashManager = AbstractService.extend({ + init: function () { + var self = this; + active = true; + this.isConnected = true; + this.odooExceptionTitleMap = { + 'odoo.addons.base.models.ir_mail_server.MailDeliveryException': _lt("MailDeliveryException"), + 'odoo.exceptions.AccessDenied': _lt("Access Denied"), + 'odoo.exceptions.AccessError': _lt("Access Error"), + 'odoo.exceptions.MissingError': _lt("Missing Record"), + 'odoo.exceptions.UserError': _lt("User Error"), + 'odoo.exceptions.ValidationError': _lt("Validation Error"), + 'odoo.exceptions.Warning': _lt("Warning"), + }; + + this.browserDetection = new BrowserDetection(); + this._super.apply(this, arguments); + + // crash manager integration + core.bus.on('rpc_error', this, this.rpc_error); + window.onerror = function (message, file, line, col, error) { + // Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror. + // The browser will just give you a 'Script error.' as message and nothing else for security issue. + // To enable onerror to work properly with CORS file, you should: + // 1. add crossorigin="anonymous" to your <script> tag loading the file + // 2. enabling 'Access-Control-Allow-Origin' on the server serving the file. + // Since in some case it wont be possible to to this, this handle should have the possibility to be + // handled by the script manipulating the injected file. For this, you will use window.onOriginError + // If it is not handled, we should display something clearer than the common crash_manager error dialog + // since it won't show anything except "Script error." + // This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html + if (!file && !line && !col) { + // Chrome and Opera set "Script error." on the `message` and hide the `error` + // Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error` + if (window.onOriginError) { + window.onOriginError(); + delete window.onOriginError; + } else { + self.show_error({ + type: _t("Odoo Client Error"), + message: _t("Unknown CORS error"), + data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")}, + }); + } + } else { + // ignore Chrome video internal error: https://crbug.com/809574 + if (!error && message === 'ResizeObserver loop limit exceeded') { + return; + } + var traceback = error ? error.stack : ''; + self.show_error({ + type: _t("Odoo Client Error"), + message: message, + data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback}, + }); + } + }; + + // listen to unhandled rejected promises, and throw an error when the + // promise has been rejected due to a crash + core.bus.on('crash_manager_unhandledrejection', this, function (ev) { + if (ev.reason && ev.reason instanceof Error) { + // Error.prototype.stack is non-standard. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + // However, most engines provide an implementation. + // In particular, Chrome formats the contents of Error.stack + // https://v8.dev/docs/stack-trace-api#compatibility + let traceback; + if (self.browserDetection.isBrowserChrome()) { + traceback = ev.reason.stack; + } else { + traceback = `${_t("Error:")} ${ev.reason.message}\n${ev.reason.stack}`; + } + self.show_error({ + type: _t("Odoo Client Error"), + message: '', + data: {debug: _t('Traceback:') + "\n" + traceback}, + }); + } else { + // the rejection is not due to an Error, so prevent the browser + // from displaying an 'unhandledrejection' error in the console + ev.stopPropagation(); + ev.stopImmediatePropagation(); + ev.preventDefault(); + } + }); + }, + enable: function () { + active = true; + }, + disable: function () { + active = false; + }, + handleLostConnection: function () { + var self = this; + if (!this.isConnected) { + // already handled, nothing to do. This can happen when several + // rpcs are done in parallel and fail because of a lost connection. + return; + } + this.isConnected = false; + var delay = 2000; + core.bus.trigger('connection_lost'); + + setTimeout(function checkConnection() { + ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () { + core.bus.trigger('connection_restored'); + self.isConnected = true; + }).guardedCatch(function () { + // exponential backoff, with some jitter + delay = (delay * 1.5) + 500*Math.random(); + setTimeout(checkConnection, delay); + }); + }, delay); + }, + rpc_error: function(error) { + // Some qunit tests produces errors before the DOM is set. + // This produces an error loop as the modal/toast has no DOM to attach to. + if (!document.body || !active || this.connection_lost) return; + + // Connection lost error + if (error.code === -32098) { + this.handleLostConnection(); + return; + } + + // Special exception handlers, see crash_registry bellow + var handler = core.crash_registry.get(error.data.name, true); + if (handler) { + new (handler)(this, error).display(); + return; + } + + // Odoo custom exception: UserError, AccessError, ... + if (_.has(this.odooExceptionTitleMap, error.data.name)) { + error = _.extend({}, error, { + data: _.extend({}, error.data, { + message: error.data.arguments[0], + title: this.odooExceptionTitleMap[error.data.name], + }), + }); + this.show_warning(error); + return; + } + + // Any other Python exception + this.show_error(error); + }, + show_warning: function (error, options) { + if (!active) { + return; + } + var message = error.data ? error.data.message : error.message; + var title = _t("Something went wrong !"); + if (error.type) { + title = _.str.capitalize(error.type); + } else if (error.data && error.data.title) { + title = _.str.capitalize(error.data.title); + } + return this._displayWarning(message, title, options); + }, + show_error: function (error) { + if (!active) { + return; + } + error.traceback = error.data.debug; + var dialogClass = error.data.context && ErrorDialogRegistry.get(error.data.context.exception_class) || ErrorDialog; + var dialog = new dialogClass(this, { + title: _.str.capitalize(error.type) || _t("Odoo Error"), + }, error); + + + // When the dialog opens, initialize the copy feature and destroy it when the dialog is closed + var $clipboardBtn; + var clipboard; + dialog.opened(function () { + // When the full traceback is shown, scroll it to the end (useful for better python error reporting) + dialog.$(".o_error_detail").on("shown.bs.collapse", function (e) { + e.target.scrollTop = e.target.scrollHeight; + }); + + $clipboardBtn = dialog.$(".o_clipboard_button"); + $clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"}); + clipboard = new window.ClipboardJS($clipboardBtn[0], { + text: function () { + return (_t("Error") + ":\n" + error.message + "\n\n" + error.data.debug).trim(); + }, + // Container added because of Bootstrap modal that give the focus to another element. + // We need to give to correct focus to ClipboardJS (see in ClipboardJS doc) + // https://github.com/zenorocha/clipboard.js/issues/155 + container: dialog.el, + }); + clipboard.on("success", function (e) { + _.defer(function () { + $clipboardBtn.tooltip("show"); + _.delay(function () { + $clipboardBtn.tooltip("hide"); + }, 800); + }); + }); + }); + dialog.on("closed", this, function () { + $clipboardBtn.tooltip('dispose'); + clipboard.destroy(); + }); + + return dialog.open(); + }, + show_message: function(exception) { + return this.show_error({ + type: _t("Odoo Client Error"), + message: exception, + data: {debug: ""} + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} message + * @param {string} title + * @param {Object} options + */ + _displayWarning: function (message, title, options) { + return new WarningDialog(this, Object.assign({}, options, { + title, + }), { + message, + }).open(); + }, +}); + +/** + * An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry. +*/ +var ExceptionHandler = { + /** + * @param parent The parent. + * @param error The error object as returned by the JSON-RPC implementation. + */ + init: function(parent, error) {}, + /** + * Called to inform to display the widget, if necessary. A typical way would be to implement + * this interface in a class extending instance.web.Dialog and simply display the dialog in this + * method. + */ + display: function() {}, +}; + + +/** + * Handle redirection warnings, which behave more or less like a regular + * warning, with an additional redirection button. + */ +var RedirectWarningHandler = Widget.extend(ExceptionHandler, { + init: function(parent, error) { + this._super(parent); + this.error = error; + }, + display: function() { + var self = this; + var error = this.error; + var additional_context = _.extend({}, this.context, error.data.arguments[3]); + + new WarningDialog(this, { + title: _.str.capitalize(error.type) || _t("Odoo Warning"), + buttons: [ + {text: error.data.arguments[2], classes : "btn-primary", click: function() { + self.do_action( + error.data.arguments[1], + { + additional_context: additional_context, + }); + self.destroy(); + }, close: true}, + {text: _t("Cancel"), click: function() { self.destroy(); }, close: true} + ] + }, { + message: error.data.arguments[0], + }).open(); + } +}); + +core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler); + +function session_expired(cm) { + return { + display: function () { + const notif = { + type: _t("Odoo Session Expired"), + message: _t("Your Odoo session expired. The current page is about to be refreshed."), + }; + const options = { + buttons: [{ + text: _t("Ok"), + click: () => window.location.reload(true), + close: true + }], + }; + cm.show_warning(notif, options); + } + }; +} +core.crash_registry.add('odoo.http.SessionExpiredException', session_expired); +core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired); + +core.crash_registry.add('504', function (cm) { + return { + display: function () { + cm.show_warning({ + type: _t("Request timeout"), + message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")}); + } + }; +}); + +return { + CrashManager: CrashManager, + ErrorDialog: ErrorDialog, + WarningDialog: WarningDialog, + disable: () => active = false, +}; +}); diff --git a/addons/web/static/src/js/services/crash_manager_service.js b/addons/web/static/src/js/services/crash_manager_service.js new file mode 100644 index 00000000..44182561 --- /dev/null +++ b/addons/web/static/src/js/services/crash_manager_service.js @@ -0,0 +1,9 @@ +odoo.define('crash_manager.service', function (require) { +'use strict'; + +const core = require('web.core'); +const CrashManager = require('web.CrashManager').CrashManager; + +core.serviceRegistry.add('crash_manager', CrashManager); + +}); diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js new file mode 100644 index 00000000..f5bb482a --- /dev/null +++ b/addons/web/static/src/js/services/data_manager.js @@ -0,0 +1,225 @@ +odoo.define('web.DataManager', function (require) { +"use strict"; + +var config = require('web.config'); +var core = require('web.core'); +var rpc = require('web.rpc'); +var session = require('web.session'); +var utils = require('web.utils'); + +return core.Class.extend({ + init: function () { + this._init_cache(); + core.bus.on('clear_cache', this, this.invalidate.bind(this)); + }, + + _init_cache: function () { + this._cache = { + actions: {}, + filters: {}, + views: {}, + }; + }, + + /** + * Invalidates the whole cache + * Suggestion: could be refined to invalidate some part of the cache + */ + invalidate: function () { + session.invalidateCacheKey('load_menus'); + this._init_cache(); + }, + + /** + * Loads an action from its id or xmlid. + * + * @param {int|string} [action_id] the action id or xmlid + * @param {Object} [additional_context] used to load the action + * @return {Promise} resolved with the action whose id or xmlid is action_id + */ + load_action: function (action_id, additional_context) { + var self = this; + var key = this._gen_key(action_id, additional_context || {}); + + if (config.isDebug('assets') || !this._cache.actions[key]) { + this._cache.actions[key] = rpc.query({ + route: "/web/action/load", + params: { + action_id: action_id, + additional_context: additional_context, + }, + }).then(function (action) { + self._cache.actions[key] = action.no_cache ? null : self._cache.actions[key]; + return action; + }).guardedCatch(() => this._invalidate('actions', key)); + } + + return this._cache.actions[key].then(function (action) { + return $.extend(true, {}, action); + }); + }, + + /** + * Loads various information concerning views: fields_view for each view, + * the fields of the corresponding model, and optionally the filters. + * + * @param {Object} params + * @param {String} params.model + * @param {Object} params.context + * @param {Array} params.views_descr array of [view_id, view_type] + * @param {Object} [options={}] dictionary of various options: + * - options.load_filters: whether or not to load the filters, + * - options.action_id: the action_id (required to load filters), + * - options.toolbar: whether or not a toolbar will be displayed, + * @return {Promise} resolved with the requested views information + */ + load_views: async function ({ model, context, views_descr } , options = {}) { + const viewsKey = this._gen_key(model, views_descr, options, context); + const filtersKey = this._gen_key(model, options.action_id); + const withFilters = Boolean(options.load_filters); + const shouldLoadViews = config.isDebug('assets') || !this._cache.views[viewsKey]; + const shouldLoadFilters = config.isDebug('assets') || ( + withFilters && !this._cache.filters[filtersKey] + ); + if (shouldLoadViews) { + // Views info should be loaded + options.load_filters = shouldLoadFilters; + this._cache.views[viewsKey] = rpc.query({ + args: [], + kwargs: { context, options, views: views_descr }, + model, + method: 'load_views', + }).then(result => { + // Freeze the fields dict as it will be shared between views and + // no one should edit it + utils.deepFreeze(result.fields); + for (const [viewId, viewType] of views_descr) { + const fvg = result.fields_views[viewType]; + fvg.viewFields = fvg.fields; + fvg.fields = result.fields; + } + + // Insert filters, if any, into the filters cache + if (shouldLoadFilters) { + this._cache.filters[filtersKey] = Promise.resolve(result.filters); + } + return result.fields_views; + }).guardedCatch(() => this._invalidate('views', viewsKey)); + } + const result = await this._cache.views[viewsKey]; + if (withFilters && result.search) { + if (shouldLoadFilters) { + await this.load_filters({ + actionId: options.action_id, + context, + forceReload: false, + modelName: model, + }); + } + result.search.favoriteFilters = await this._cache.filters[filtersKey]; + } + return result; + }, + + /** + * Loads the filters of a given model and optional action id. + * + * @param {Object} params + * @param {number} params.actionId + * @param {Object} params.context + * @param {boolean} [params.forceReload=true] can be set to false to prevent forceReload + * @param {string} params.modelName + * @return {Promise} resolved with the requested filters + */ + load_filters: function (params) { + const key = this._gen_key(params.modelName, params.actionId); + const forceReload = params.forceReload !== false && config.isDebug('assets'); + if (forceReload || !this._cache.filters[key]) { + this._cache.filters[key] = rpc.query({ + args: [params.modelName, params.actionId], + kwargs: { + context: params.context || {}, + // get_context() de dataset + }, + model: 'ir.filters', + method: 'get_filters', + }).guardedCatch(() => this._invalidate('filters', key)); + } + return this._cache.filters[key]; + }, + + /** + * Calls 'create_or_replace' on 'ir_filters'. + * + * @param {Object} [filter] the filter description + * @return {Promise} resolved with the id of the created or replaced filter + */ + create_filter: function (filter) { + return rpc.query({ + args: [filter], + model: 'ir.filters', + method: 'create_or_replace', + }) + .then(filterId => { + const filtersKey = this._gen_key(filter.model_id, filter.action_id); + this._invalidate('filters', filtersKey); + return filterId; + }); + }, + + /** + * Calls 'unlink' on 'ir_filters'. + * + * @param {integer} filterId Id of the filter to remove + * @return {Promise} + */ + delete_filter: function (filterId) { + return rpc.query({ + args: [filterId], + model: 'ir.filters', + method: 'unlink', + }) + // Invalidate the whole cache since we have no idea where the filter came from. + .then(() => this._invalidate('filters')); + }, + + /** + * Private function that generates a cache key from its arguments + */ + _gen_key: function () { + return _.map(Array.prototype.slice.call(arguments), function (arg) { + if (!arg) { + return false; + } + return _.isObject(arg) ? JSON.stringify(arg) : arg; + }).join(','); + }, + + /** + * Invalidate a cache entry or a whole cache section. + * + * @private + * @param {string} section + * @param {string} key + */ + _invalidate(section, key) { + if (key) { + delete this._cache[section][key]; + } else { + this._cache[section] = {}; + } + }, +}); + +}); + +odoo.define('web.data_manager', function (require) { +"use strict"; + +var DataManager = require('web.DataManager'); + +var data_manager = new DataManager(); + +return data_manager; + +}); diff --git a/addons/web/static/src/js/services/local_storage_service.js b/addons/web/static/src/js/services/local_storage_service.js new file mode 100644 index 00000000..694dc6da --- /dev/null +++ b/addons/web/static/src/js/services/local_storage_service.js @@ -0,0 +1,20 @@ +odoo.define('web.LocalStorageService', function (require) { +'use strict'; + +/** + * This module defines a service to access the localStorage object. + */ + +var AbstractStorageService = require('web.AbstractStorageService'); +var core = require('web.core'); +var localStorage = require('web.local_storage'); + +var LocalStorageService = AbstractStorageService.extend({ + storage: localStorage, +}); + +core.serviceRegistry.add('local_storage', LocalStorageService); + +return LocalStorageService; + +}); diff --git a/addons/web/static/src/js/services/notification_service.js b/addons/web/static/src/js/services/notification_service.js new file mode 100644 index 00000000..295b49d7 --- /dev/null +++ b/addons/web/static/src/js/services/notification_service.js @@ -0,0 +1,111 @@ +odoo.define('web.NotificationService', function (require) { +'use strict'; + +var AbstractService = require('web.AbstractService'); +var Notification = require('web.Notification'); +var core = require('web.core'); + +var id = 0; + +/** + * Notification Service + * + * The Notification Service is simply a service used to display notifications in + * the top/right part of the screen. + * + * If you want to display such a notification, you probably do not want to do it + * by using this file. The proper way is to use the do_warn or do_notify + * methods on the Widget class. + */ +var NotificationService = AbstractService.extend({ + custom_events: { + close: '_onCloseNotification', + }, + + /** + * @override + */ + start: function () { + this._super.apply(this, arguments); + this.notifications = {}; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * It may sometimes be useful to close programmatically a notification. For + * example, when there is a sticky notification warning the user about some + * condition (connection lost), but the condition does not apply anymore. + * + * @param {number} notificationId + * @param {boolean} [silent=false] if true, the notification does not call + * onClose callback + */ + close: function (notificationId, silent) { + var notification = this.notifications[notificationId]; + if (!notification) { + return; + } + notification.close(silent); + }, + /** + * Display a notification at the appropriate location, and returns the + * reference id to the same widget. + * + * Note that this method does not wait for the appendTo method to complete. + * + * @param {Object} params + * @param {function} [params.Notification] javascript class of a notification + * to instantiate by default use 'web.Notification' + * @param {string} params.title notification title + * @param {string} params.subtitle notification subtitle + * @param {string} params.message notification main message + * @param {string} params.type 'notification' or 'warning' + * @param {boolean} [params.sticky=false] if true, the notification will stay + * visible until the user clicks on it. + * @param {string} [params.className] className to add on the dom + * @param {function} [params.onClose] callback when the user click on the x + * or when the notification is auto close (no sticky) + * @param {Object[]} params.buttons + * @param {function} params.buttons[0].click callback on click + * @param {Boolean} [params.buttons[0].primary] display the button as primary + * @param {string} [params.buttons[0].text] button label + * @param {string} [params.buttons[0].icon] font-awsome className or image src + * @returns {Number} notification id + */ + notify: function (params) { + if (!this.$el) { + this.$el = $('<div class="o_notification_manager"/>'); + this.$el.prependTo('body'); + } + var NotificationWidget = params.Notification || Notification; + var notification = this.notifications[++id] = new NotificationWidget(this, params); + notification.appendTo(this.$el); + return id; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} ev + */ + _onCloseNotification: function (ev) { + ev.stopPropagation(); + for (var notificationId in this.notifications) { + if (this.notifications[notificationId] === ev.target) { + delete this.notifications[notificationId]; + break; + } + } + }, +}); + +core.serviceRegistry.add('notification', NotificationService); + +return NotificationService; +}); diff --git a/addons/web/static/src/js/services/report_service.js b/addons/web/static/src/js/services/report_service.js new file mode 100644 index 00000000..ed4907a6 --- /dev/null +++ b/addons/web/static/src/js/services/report_service.js @@ -0,0 +1,35 @@ +odoo.define('web.ReportService', function (require) { +"use strict"; + +/** + * This file defines the service for the report generation in Odoo. + */ + +var AbstractService = require('web.AbstractService'); +var core = require('web.core'); + +var ReportService = AbstractService.extend({ + dependencies: ['ajax'], + + /** + * Checks the state of the installation of wkhtmltopdf on the server. + * Implements an internal cache to do the request only once. + * + * @returns {Promise} resolved with the state of wkhtmltopdf on the server + * (possible values are 'ok', 'broken', 'install', 'upgrade', 'workers'). + */ + checkWkhtmltopdf: function () { + if (!this.wkhtmltopdfState) { + this.wkhtmltopdfState = this._rpc({ + route:'/report/check_wkhtmltopdf' + }); + } + return this.wkhtmltopdfState; + }, +}); + +core.serviceRegistry.add('report', ReportService); + +return ReportService; + +}); diff --git a/addons/web/static/src/js/services/session.js b/addons/web/static/src/js/services/session.js new file mode 100644 index 00000000..35720f81 --- /dev/null +++ b/addons/web/static/src/js/services/session.js @@ -0,0 +1,12 @@ +odoo.define('web.session', function (require) { +"use strict"; + +var Session = require('web.Session'); +var modules = odoo._modules; + +var session = new Session(undefined, undefined, {modules: modules, use_cors: false}); +session.is_bound = session.session_bind(); + +return session; + +}); diff --git a/addons/web/static/src/js/services/session_storage_service.js b/addons/web/static/src/js/services/session_storage_service.js new file mode 100644 index 00000000..41c47af3 --- /dev/null +++ b/addons/web/static/src/js/services/session_storage_service.js @@ -0,0 +1,20 @@ +odoo.define('web.SessionStorageService', function (require) { +'use strict'; + +/** + * This module defines a service to access the sessionStorage object. + */ + +var AbstractStorageService = require('web.AbstractStorageService'); +var core = require('web.core'); +var sessionStorage = require('web.sessionStorage'); + +var SessionStorageService = AbstractStorageService.extend({ + storage: sessionStorage, +}); + +core.serviceRegistry.add('session_storage', SessionStorageService); + +return SessionStorageService; + +}); |
