From 7e3ad783bb50fd436bc9c81e0207cc8313f744a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20D=2E=20D=C3=ADaz?= Date: Mon, 13 Jun 2022 11:20:03 +0200 Subject: [PATCH] [IMP] web_pwa_oca: Added css classes to control the visibility of view fields --- web_pwa_oca/README.rst | 30 ++- web_pwa_oca/controllers/main.py | 20 +- web_pwa_oca/controllers/service_worker.py | 41 ++- web_pwa_oca/models/__init__.py | 1 + web_pwa_oca/models/base.py | 36 +++ web_pwa_oca/models/res_config_settings.py | 54 +++- web_pwa_oca/readme/DESCRIPTION.rst | 30 ++- web_pwa_oca/static/src/js/app_menus.js | 44 ++++ web_pwa_oca/static/src/js/pwa_manager.js | 38 ++- .../static/src/js/views/abstract_view.js | 38 +++ .../src/js/views/action_manager_act_window.js | 84 ++++++ .../static/src/js/views/basic_renderer.js | 118 +++++++++ web_pwa_oca/static/src/js/views/basic_view.js | 75 ++++++ .../static/src/js/views/data_manager.js | 22 ++ .../static/src/js/views/form_renderer.js | 249 ++++++++++++++++++ web_pwa_oca/static/src/js/webclient.js | 3 +- web_pwa_oca/static/src/js/worker/pwa.js | 28 +- web_pwa_oca/static/src/scss/main.scss | 9 + web_pwa_oca/templates/assets.xml | 57 +++- .../tests/test_web_pwa_oca_controller.py | 17 +- .../views/res_config_settings_views.xml | 8 + 21 files changed, 943 insertions(+), 59 deletions(-) create mode 100644 web_pwa_oca/models/base.py create mode 100644 web_pwa_oca/static/src/js/app_menus.js create mode 100644 web_pwa_oca/static/src/js/views/abstract_view.js create mode 100644 web_pwa_oca/static/src/js/views/action_manager_act_window.js create mode 100644 web_pwa_oca/static/src/js/views/basic_renderer.js create mode 100644 web_pwa_oca/static/src/js/views/basic_view.js create mode 100644 web_pwa_oca/static/src/js/views/data_manager.js create mode 100644 web_pwa_oca/static/src/js/views/form_renderer.js create mode 100644 web_pwa_oca/static/src/scss/main.scss diff --git a/web_pwa_oca/README.rst b/web_pwa_oca/README.rst index 7bac28323aea..1ecc2be065b8 100644 --- a/web_pwa_oca/README.rst +++ b/web_pwa_oca/README.rst @@ -37,14 +37,40 @@ If you're building a web app today, you're already on the path towards building + Developers Info. -The service worker is contructed using 'Odoo Class' to have the same class inheritance behaviour that in the 'user pages'. Be noticed -that 'Odoo Bootstrap' is not supported so, you can't use 'require' here. +The service worker is constructed using 'Odoo Class' to have the same class inheritance behaviour that in the 'user pages'. Be noticed +that 'Odoo Bootstrap' is supported so, you can use 'require' here. All service worker content can be found in 'static/src/js/worker'. The management between 'user pages' and service worker is done in 'pwa_manager.js'. The purpose of this module is give a base to make PWA applications. ++ CSS Classes to use in qweb templates + + - oe_pwa_standalone_invisible : Can be used in the "form" element to get a "empty" form (all is unavailable/invisible). + - oe_pwa_standalone_no_chatter : Can be used in the "form" element to avoid render chatter zone. + - oe_pwa_standalone_no_sheet : Can be used in the "form" element to don't use the sheet style. + - oe_pwa_standalone_visible : Can be used in a element when the form has the 'oe_pwa_standalone_invisible' class to make the element available. + - oe_pwa_standalone_omit : Can be used in elements to make them unavailable in standalone mode. + - oe_pwa_standalone_only : Can be used in elements to make them available only in standalone mode. + +What does 'unavailable/invisible' mean? +This 'invisible' state is not only the same as use "invisible=1" in qweb. Here invisible means that the client will not process the fields completely (no rpc calls will be made). + +** All ancestors of an element that is visible will also be visible. +** All invisible fields (invisible=1 in qweb definition) will be available by default. +** 'oe_pwa_standalone_omit' and 'oe_pwa_standalone_only' will be filtered on the server side too. + ++ XML Attributes to use in qweb templates + + - standalone-attrs : Used to extend the node attrs in standalone mode. + ++ Tips for developers to update templates with an standalone mode + + - Odoo gets the first element when no index is used in qweb xpath, so... duplicated fields must be added after "the original" to ensure that other modules changes the correct element. + - If you define duplicated fields use the 'oe_pwa_standalone_omit' class in the original and 'oe_pwa_standalone_only' class in the duplicated one to avoid problems. + - Set a a very low priority (high value) in your 'standalone view' template. + **Table of contents** .. contents:: diff --git a/web_pwa_oca/controllers/main.py b/web_pwa_oca/controllers/main.py index fdaad56c18de..116e01ee4b45 100644 --- a/web_pwa_oca/controllers/main.py +++ b/web_pwa_oca/controllers/main.py @@ -6,6 +6,8 @@ from odoo.http import Controller, request, route +from ..models.res_config_settings import PWA_ICON_SIZES + class PWA(Controller): def _get_pwa_scripts(self): @@ -38,14 +40,7 @@ def _get_pwa_params(self): def _get_pwa_manifest_icons(self, pwa_icon): icons = [] if not pwa_icon: - for size in [ - (128, 128), - (144, 144), - (152, 152), - (192, 192), - (256, 256), - (512, 512), - ]: + for size in PWA_ICON_SIZES: icons.append( { "src": "/web_pwa_oca/static/img/icons/icon-%sx%s.png" @@ -75,12 +70,11 @@ def _get_pwa_manifest_icons(self, pwa_icon): {"src": icon.url, "sizes": icon_size_name, "type": icon.mimetype} ) else: + icon_sizes = " ".join( + map(lambda size: "{}x{}".format(size[0], size[1]), PWA_ICON_SIZES,) + ) icons = [ - { - "src": pwa_icon.url, - "sizes": "128x128 144x144 152x152 192x192 256x256 512x512", - "type": pwa_icon.mimetype, - } + {"src": pwa_icon.url, "sizes": icon_sizes, "type": pwa_icon.mimetype} ] return icons diff --git a/web_pwa_oca/controllers/service_worker.py b/web_pwa_oca/controllers/service_worker.py index ffd8fd737b81..dd30a601fb56 100644 --- a/web_pwa_oca/controllers/service_worker.py +++ b/web_pwa_oca/controllers/service_worker.py @@ -6,6 +6,7 @@ class ServiceWorker(PWA): + _pwa_sw_version = "0.1.0" JS_PWA_CORE_EVENT_INSTALL = """ self.addEventListener('install', evt => {{ @@ -48,26 +49,54 @@ def _get_js_pwa_requires(self): def _get_js_pwa_init(self): return """ - const oca_pwa = new PWA({}); + let promise_start = Promise.resolve(); + if (typeof self.oca_pwa === "undefined") {{ + self.oca_pwa = new PWA({}); + promise_start = self.oca_pwa.start(); + if (self.serviceWorker.state === "activated") {{ + promise_start = promise_start.then( + () => self.oca_pwa.activateWorker(true)); + }} + }} """.format( self._get_pwa_params() ) def _get_js_pwa_core_event_install_impl(self): return """ - evt.waitUntil(oca_pwa.installWorker()); - self.skipWaiting(); + evt.waitUntil(promise_start.then(() => self.oca_pwa.installWorker())); """ def _get_js_pwa_core_event_activate_impl(self): return """ console.log('[ServiceWorker] Activating...'); - evt.waitUntil(oca_pwa.activateWorker()); - self.clients.claim(); + evt.waitUntil(promise_start.then(() => self.oca_pwa.activateWorker())); """ def _get_js_pwa_core_event_fetch_impl(self): - return "" + return """ + if (evt.request.url.startsWith(self.registration.scope)) { + evt.respondWith(promise_start.then( + () => self.oca_pwa.processRequest(evt.request))); + } + """ + + def _get_pwa_scripts(self): + """Scripts to be imported in the service worker (Order is important)""" + return [ + "/web/static/lib/underscore/underscore.js", + "/web_pwa_oca/static/src/js/worker/jquery-sw-compat.js", + "/web/static/src/js/promise_extension.js", + "/web/static/src/js/boot.js", + "/web/static/src/js/core/class.js", + "/web_pwa_oca/static/src/js/worker/pwa.js", + ] + + def _get_pwa_params(self): + """Get javascript PWA class initialzation params""" + return { + "sw_version": self._pwa_sw_version, + } @route("/service-worker.js", type="http", auth="public") def render_service_worker(self): diff --git a/web_pwa_oca/models/__init__.py b/web_pwa_oca/models/__init__.py index 0deb68c46806..2d817f97a54b 100644 --- a/web_pwa_oca/models/__init__.py +++ b/web_pwa_oca/models/__init__.py @@ -1 +1,2 @@ from . import res_config_settings +from . import base diff --git a/web_pwa_oca/models/base.py b/web_pwa_oca/models/base.py new file mode 100644 index 000000000000..f98cea54adef --- /dev/null +++ b/web_pwa_oca/models/base.py @@ -0,0 +1,36 @@ +# Copyright 2022 Tecnativa - Alexandre D. Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from lxml import etree + +from odoo import api, models + + +class BaseModel(models.BaseModel): + _inherit = "base" + + def _fields_view_get( + self, view_id=None, view_type="form", toolbar=False, submenu=False + ): + """Remove unused nodes depend on the standalone mode + This is necessary to avoid problems with duplicated fields + """ + res = super()._fields_view_get( + view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu + ) + is_client_standalone = self.env.context.get("client_standalone", False) + doc = etree.XML(res["arch"]) + if is_client_standalone: + for node in doc.xpath("//*[contains(@class, 'oe_pwa_standalone_omit')]"): + node.getparent().remove(node) + else: + for node in doc.xpath("//*[contains(@class, 'oe_pwa_standalone_only')]"): + node.getparent().remove(node) + res["arch"] = etree.tostring(doc, encoding="unicode") + return res + + @api.model + def load_views(self, views, options=None): + standalone = options.get("standalone", False) if options else False + return super( + BaseModel, self.with_context(client_standalone=standalone) + ).load_views(views, options=options) diff --git a/web_pwa_oca/models/res_config_settings.py b/web_pwa_oca/models/res_config_settings.py index 229c55b4cde9..f336305f9496 100644 --- a/web_pwa_oca/models/res_config_settings.py +++ b/web_pwa_oca/models/res_config_settings.py @@ -1,4 +1,5 @@ # Copyright 2020 Tecnativa - João Marques +# Copyright 2021 Tecnativa - Alexandre D. Díaz # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). import base64 import io @@ -9,6 +10,12 @@ from odoo import _, api, exceptions, fields, models from odoo.tools.mimetypes import guess_mimetype +DEFAULT_ICON_SIZE = 512 +PWA_ICON_SIZES = ( + (512, 512), + (192, 192), +) + class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" @@ -24,6 +31,12 @@ class ResConfigSettings(models.TransientModel): pwa_icon = fields.Binary("Icon", readonly=False) pwa_background_color = fields.Char("Background Color") pwa_theme_color = fields.Char("Theme Color") + pwa_action_id = fields.Many2one( + "ir.actions.actions", + string="Home Action", + help="If specified, this action will be opened at pwa opened, in addition " + "to the standard menu.", + ) @api.model def get_values(self): @@ -49,8 +62,20 @@ def get_values(self): res["pwa_theme_color"] = config_parameter_obj_sudo.get_param( "pwa.manifest.theme_color", default="#2E69B5" ) + action_id = config_parameter_obj_sudo.get_param( + "pwa.config.action_id", default=False + ) + res["pwa_action_id"] = action_id and int(action_id) return res + @api.model + def get_pwa_home_action(self): + return ( + self.env["ir.config_parameter"] + .sudo() + .get_param("pwa.config.action_id", default=False) + ) + def _unpack_icon(self, icon): # Wrap decoded_icon in BytesIO object decoded_icon = base64.b64decode(icon) @@ -103,12 +128,18 @@ def set_values(self): config_parameter_obj_sudo.set_param( "pwa.manifest.theme_color", self.pwa_theme_color ) + config_parameter_obj_sudo.set_param( + "pwa.config.action_id", self.pwa_action_id.id + ) # Retrieve previous value for pwa_icon from ir_attachment pwa_icon_ir_attachments = ( self.env["ir.attachment"] .sudo() .search([("url", "like", self._pwa_icon_url_base)]) ) + config_parameter_obj_sudo.set_param( + "pwa.manifest.custom_icon", True if self.pwa_icon else False + ) # Delete or ignore if no icon provided if not self.pwa_icon: if pwa_icon_ir_attachments: @@ -138,19 +169,20 @@ def set_values(self): self._write_icon_to_attachment(pwa_icon_extension, pwa_icon_mimetype) # write multiple sizes if not SVG if pwa_icon_extension != ".svg": - # Fail if provided PNG is smaller than 512x512 - if self._unpack_icon(self.pwa_icon).size < (512, 512): + # Fail if provided PNG is smaller than DEFAULT_ICON_SIZE + if self._unpack_icon(self.pwa_icon).size < ( + DEFAULT_ICON_SIZE, + DEFAULT_ICON_SIZE, + ): raise exceptions.UserError( - _("You can only upload PNG files bigger than 512x512") + _( + "You can only upload PNG files bigger " + + "than {icon_size}x{icon_size}".format( + icon_size=DEFAULT_ICON_SIZE + ) + ) ) - for size in [ - (128, 128), - (144, 144), - (152, 152), - (192, 192), - (256, 256), - (512, 512), - ]: + for size in PWA_ICON_SIZES: self._write_icon_to_attachment( pwa_icon_extension, pwa_icon_mimetype, size=size ) diff --git a/web_pwa_oca/readme/DESCRIPTION.rst b/web_pwa_oca/readme/DESCRIPTION.rst index 2b4b14ef4b31..8fdd2ebb54e7 100644 --- a/web_pwa_oca/readme/DESCRIPTION.rst +++ b/web_pwa_oca/readme/DESCRIPTION.rst @@ -7,10 +7,36 @@ If you're building a web app today, you're already on the path towards building + Developers Info. -The service worker is contructed using 'Odoo Class' to have the same class inheritance behaviour that in the 'user pages'. Be noticed -that 'Odoo Bootstrap' is not supported so, you can't use 'require' here. +The service worker is constructed using 'Odoo Class' to have the same class inheritance behaviour that in the 'user pages'. Be noticed +that 'Odoo Bootstrap' is supported so, you can use 'require' here. All service worker content can be found in 'static/src/js/worker'. The management between 'user pages' and service worker is done in 'pwa_manager.js'. The purpose of this module is give a base to make PWA applications. + ++ CSS Classes to use in qweb templates + + - oe_pwa_standalone_invisible : Can be used in the "form" element to get a "empty" form (all is unavailable/invisible). + - oe_pwa_standalone_no_chatter : Can be used in the "form" element to avoid render chatter zone. + - oe_pwa_standalone_no_sheet : Can be used in the "form" element to don't use the sheet style. + - oe_pwa_standalone_visible : Can be used in a element when the form has the 'oe_pwa_standalone_invisible' class to make the element available. + - oe_pwa_standalone_omit : Can be used in elements to make them unavailable in standalone mode. + - oe_pwa_standalone_only : Can be used in elements to make them available only in standalone mode. + +What does 'unavailable/invisible' mean? +This 'invisible' state is not only the same as use "invisible=1" in qweb. Here invisible means that the client will not process the fields completely (no rpc calls will be made). + +** All ancestors of an element that is visible will also be visible. +** All invisible fields (invisible=1 in qweb definition) will be available by default. +** 'oe_pwa_standalone_omit' and 'oe_pwa_standalone_only' will be filtered on the server side too. + ++ XML Attributes to use in qweb templates + + - standalone-attrs : Used to extend the node attrs in standalone mode. + ++ Tips for developers to update templates with an standalone mode + + - Odoo gets the first element when no index is used in qweb xpath, so... duplicated fields must be added after "the original" to ensure that other modules changes the correct element. + - If you define duplicated fields use the 'oe_pwa_standalone_omit' class in the original and 'oe_pwa_standalone_only' class in the duplicated one to avoid problems. + - Set a a very low priority (high value) in your 'standalone view' template. diff --git a/web_pwa_oca/static/src/js/app_menus.js b/web_pwa_oca/static/src/js/app_menus.js new file mode 100644 index 000000000000..a03b9aab3aad --- /dev/null +++ b/web_pwa_oca/static/src/js/app_menus.js @@ -0,0 +1,44 @@ +/* Copyright 2021 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("web_pwa_oca.AppsMenu", function(require) { + "use strict"; + + const AppsMenu = require("web.AppsMenu"); + require("web_pwa_oca.webclient"); + const WebClientObj = require("web.web_client"); + + // This is used to reload last action when "prefetching" is done + // to ensure display updated records + AppsMenu.include({ + /** + * @override + */ + openFirstApp: function() { + const is_standalone = WebClientObj.pwa_manager.isPWAStandalone(); + if (is_standalone) { + const _sup = this._super; + WebClientObj.menu_dp + .add( + this._rpc({ + model: "res.config.settings", + method: "get_pwa_home_action", + }) + ) + .then(action_id => { + if (action_id) { + return this.do_action(action_id).then(() => { + WebClientObj.menu.change_menu_section( + WebClientObj.menu.action_id_to_primary_menu_id( + action_id + ) + ); + }); + } + return _sup.apply(this, arguments); + }); + } else { + return this._super(this, arguments); + } + }, + }); +}); diff --git a/web_pwa_oca/static/src/js/pwa_manager.js b/web_pwa_oca/static/src/js/pwa_manager.js index c4f27135c7b3..da6e42990c2c 100644 --- a/web_pwa_oca/static/src/js/pwa_manager.js +++ b/web_pwa_oca/static/src/js/pwa_manager.js @@ -5,17 +5,34 @@ odoo.define("web_pwa_oca.PWAManager", function(require) { "use strict"; var core = require("web.core"); + var config = require("web.config"); var Widget = require("web.Widget"); var _t = core._t; + /** + * @returns {Boolean} + */ + function isPWAStandalone() { + return ( + window.navigator.standalone || + document.referrer.includes("android-app://") || + window.matchMedia("(display-mode: standalone)").matches + ); + } + + if (isPWAStandalone()) { + config.device.isMobile = true; + } + var PWAManager = Widget.extend({ /** * @override */ init: function() { this._super.apply(this, arguments); - if (!("serviceWorker" in navigator)) { + this._isServiceWorkerSupported = "serviceWorker" in navigator; + if (!this._isServiceWorkerSupported) { console.error( _t( "Service workers are not supported! Maybe you are not using HTTPS or you work in private mode." @@ -23,7 +40,9 @@ odoo.define("web_pwa_oca.PWAManager", function(require) { ); } else { this._service_worker = navigator.serviceWorker; - this.registerServiceWorker("/service-worker.js"); + this.registerServiceWorker("/service-worker.js", { + updateViaCache: "none", + }); } }, @@ -31,15 +50,22 @@ odoo.define("web_pwa_oca.PWAManager", function(require) { * @param {String} sw_script * @returns {Promise} */ - registerServiceWorker: function(sw_script) { + registerServiceWorker: function(sw_script, options) { return this._service_worker - .register(sw_script) - .then(this._onRegisterServiceWorker) + .register(sw_script, options) + .then(this._onRegisterServiceWorker.bind(this)) .catch(function(error) { console.log(_t("[ServiceWorker] Registration failed: "), error); }); }, + /** + * @returns {Boolean} + */ + isPWAStandalone: function() { + return isPWAStandalone(); + }, + /** * Need register some extra API? override this! * @@ -47,7 +73,7 @@ odoo.define("web_pwa_oca.PWAManager", function(require) { * @param {ServiceWorkerRegistration} registration */ _onRegisterServiceWorker: function(registration) { - console.log(_t("[ServiceWorker] Registered:"), registration); + console.log(_t("[ServiceWorker] Registered: "), registration); }, }); diff --git a/web_pwa_oca/static/src/js/views/abstract_view.js b/web_pwa_oca/static/src/js/views/abstract_view.js new file mode 100644 index 000000000000..70e39e44edd0 --- /dev/null +++ b/web_pwa_oca/static/src/js/views/abstract_view.js @@ -0,0 +1,38 @@ +/* Copyright 2022 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("web_pwa_oca.AbstractView", function(require) { + "use strict"; + + var WebClientObj = require("web.web_client"); + var AbstractView = require("web.AbstractView"); + + AbstractView.include({ + /** + * @override + */ + init: function() { + this._super.apply(this, arguments); + if (WebClientObj.pwa_manager.isPWAStandalone()) { + this._applyStandaloneChanges(this.arch); + } + }, + + /** + * Apply 'standalone' changes to the node + * + * @param {Object} node + */ + _applyStandaloneChanges: function(node) { + if (typeof node !== "object") { + return; + } + if ("attrs" in node && node.attrs["standalone-attrs"]) { + var standalone_attrs = JSON.parse(node.attrs["standalone-attrs"]); + _.extend(node.attrs, standalone_attrs); + } + for (var children of node.children) { + this._applyStandaloneChanges(children); + } + }, + }); +}); diff --git a/web_pwa_oca/static/src/js/views/action_manager_act_window.js b/web_pwa_oca/static/src/js/views/action_manager_act_window.js new file mode 100644 index 000000000000..8b433ec9459a --- /dev/null +++ b/web_pwa_oca/static/src/js/views/action_manager_act_window.js @@ -0,0 +1,84 @@ +/* Copyright 2022 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("web_pwa_oca.ActionManager", function(require) { + "use strict"; + + var WebClientObj = require("web.web_client"); + var ActionManager = require("web.ActionManager"); + require("web.ActWindowActionManager"); + + /** + * Here we try to force use 'formPWA' instead of 'form' in mobiles devices. + * Thanks to this we don't need define the new view in the actions. + */ + ActionManager.include({ + /** + * This is launched when switch the view + * (for example click on a record in a tree view). + * + * @override + */ + _onSwitchView: function(ev) { + var controller = this.controllers[ev.data.controllerID]; + var action = this.actions[controller.actionID]; + var view = _.findWhere(action.views, { + type: ev.data.view_type, + }); + // Disable/Enable pull to refresh feature + $(".o_web_client").toggleClass( + "disable-pull-refresh", + view.fieldsView.standalone || false + ); + return this._super.apply(this, arguments); + }, + + /** + * This is launched when switch the controller + * (for example click on a record in a kanban view). + * @override + */ + _switchController: function(action, viewType) { + var view = _.findWhere(action.views, { + type: viewType, + }); + // Disable/Enable pull to refresh feature + $(".o_web_client").toggleClass( + "disable-pull-refresh", + view.fieldsView.standalone || false + ); + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _executeWindowAction: function(action, options) { + // Force "kanban" view mode + if (WebClientObj.pwa_manager.isPWAStandalone()) { + var modes = (action.view_mode && action.view_mode.split(",")) || []; + if (modes.indexOf("kanban") !== -1) { + options.viewType = "kanban"; + } + } + + return this._super(action, options).then(function(result) { + if (action.target !== "new") { + var views = result.views; + // Select the first view to display, and optionally the main view + // which will be lazyloaded + var firstView = + options.viewType && + _.findWhere(views, {type: options.viewType}); + if (!firstView) { + firstView = views[0]; + } + + $(".o_web_client").toggleClass( + "disable-pull-refresh", + firstView.fieldsView.standalone || false + ); + } + }); + }, + }); +}); diff --git a/web_pwa_oca/static/src/js/views/basic_renderer.js b/web_pwa_oca/static/src/js/views/basic_renderer.js new file mode 100644 index 000000000000..56e6e814d383 --- /dev/null +++ b/web_pwa_oca/static/src/js/views/basic_renderer.js @@ -0,0 +1,118 @@ +/* Copyright 2022 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("web_pwa_oca.BasicRenderer", function(require) { + "use strict"; + + var BasicRenderer = require("web.BasicRenderer"); + var WebClientObj = require("web.web_client"); + + BasicRenderer.include({ + /** + * @override + */ + init: function() { + this._super.apply(this, arguments); + this._isPWAStandalone = WebClientObj.pwa_manager.isPWAStandalone(); + }, + + // /** + // * @override + // */ + // _renderWidget: function (record, node) { + // if (this._isPWAOmittedNode(node)) { + // return $(); + // } + // return this._super.apply(this, arguments); + // }, + + /** + * @override + */ + _renderFieldWidget: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _registerModifiers: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + _hasPWACSSClass: function(node, classname) { + if (typeof node !== "object") { + return false; + } + const node_classes = + ("attrs" in node && node.attrs.class && node.attrs.class.split(" ")) || + []; + if (node_classes.indexOf(classname) !== -1) { + return true; + } + for (const children of node.children) { + if (this._hasPWACSSClass(children, classname)) { + return true; + } + } + + return false; + }, + + /** + * Check if the tag needs to be omitted + * + * @param {Object} node + * @returns {Boolean} + */ + _isPWAOmittedNode: function(node) { + if ( + typeof node !== "object" || + (this._isPWAStandalone && + node.attrs && + (node.attrs.invisible === "1" || + (node.attrs.modifiers && + node.attrs.modifiers.invisible === true))) + ) { + return false; + } + var is_valid_element = [].indexOf(node.tag) !== -1; + var is_button_box = + is_valid_element && + node.tag === "div" && + node.attrs.name === "button_box"; + var is_default_invisible = + this._isPWAStandalone && + this.$el.hasClass("oe_pwa_standalone_invisible"); + var is_visible = + this._hasPWACSSClass(node, "oe_pwa_standalone_visible") && + !( + node.attr && + node.attr.modifiers && + node.attr.modifiers.invisible === true + ); + if ( + !is_valid_element && + !is_button_box && + "attrs" in node && + node.attrs.class + ) { + var node_classes = node.attrs.class.split(" "); + return ( + (is_default_invisible && !is_visible) || + (this._isPWAStandalone && + node_classes.indexOf("oe_pwa_standalone_omit") !== -1) || + (!this._isPWAStandalone && + node_classes.indexOf("oe_pwa_standalone_only") !== -1) + ); + } + + return is_default_invisible && !is_visible; + }, + }); +}); diff --git a/web_pwa_oca/static/src/js/views/basic_view.js b/web_pwa_oca/static/src/js/views/basic_view.js new file mode 100644 index 000000000000..3d315755e2af --- /dev/null +++ b/web_pwa_oca/static/src/js/views/basic_view.js @@ -0,0 +1,75 @@ +/* Copyright 2022 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("web_pwa_oca.BasicView", function(require) { + "use strict"; + + var WebClientObj = require("web.web_client"); + var BasicView = require("web.BasicView"); + + BasicView.include({ + /** + * This is necessary to ensure kanban mode + * + * @override + */ + _processField: function(viewType, field, attrs) { + if (WebClientObj.pwa_manager.isPWAStandalone() && attrs.mode) { + var modes = attrs.mode.split(","); + if (modes[0] === "tree" && modes.indexOf("kanban") !== -1) { + attrs.mode = "kanban"; + } + } + return this._super.apply(this, arguments); + }, + + /** + * This is necessary to avoid do rpc calls + * + * @override + */ + _processNode: function(node, fv) { + if (this._isPWAOmittedNode(node, fv)) { + return false; + } + return this._super.apply(this, arguments); + }, + + /** + * @param {Object} node + * @returns {Boolean} + */ + _isPWAOmittedNode: function(node, fv) { + if (typeof node === "object" && node.tag === "field") { + var is_pwa_standalone = WebClientObj.pwa_manager.isPWAStandalone(); + if ( + is_pwa_standalone && + node.attrs && + (node.attrs.invisible === "1" || + (node.attrs.modifiers && node.attrs.modifiers.invisible)) + ) { + return false; + } + var container_classes = + ("class" in fv.arch.attrs && fv.arch.attrs.class.split(" ")) || []; + var is_default_invisible = + container_classes && + is_pwa_standalone && + container_classes.indexOf("oe_pwa_standalone_invisible") !== -1; + if ("attrs" in node && node.attrs.class) { + var node_classes = node.attrs.class.split(" "); + return ( + (is_default_invisible && + node_classes.indexOf("oe_pwa_standalone_visible") === -1) || + (is_pwa_standalone && + node_classes.indexOf("oe_pwa_standalone_omit") !== -1) || + (!is_pwa_standalone && + node_classes.indexOf("oe_pwa_standalone_only") !== -1) + ); + } + return is_default_invisible; + } + + return false; + }, + }); +}); diff --git a/web_pwa_oca/static/src/js/views/data_manager.js b/web_pwa_oca/static/src/js/views/data_manager.js new file mode 100644 index 000000000000..1d7f4c839620 --- /dev/null +++ b/web_pwa_oca/static/src/js/views/data_manager.js @@ -0,0 +1,22 @@ +/* Copyright 2022 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("web_pwa_oca.DataManager", function(require) { + "use strict"; + + var WebClientObj = require("web.web_client"); + var DataManager = require("web.DataManager"); + + /** + * Communicate to the server if the client is in standalone mode or not + */ + DataManager.include({ + /** + * @override + */ + load_views: function(params, options) { + options = options || {}; + options.standalone = WebClientObj.pwa_manager.isPWAStandalone(); + return this._super(params, options); + }, + }); +}); diff --git a/web_pwa_oca/static/src/js/views/form_renderer.js b/web_pwa_oca/static/src/js/views/form_renderer.js new file mode 100644 index 000000000000..0eec9f88809e --- /dev/null +++ b/web_pwa_oca/static/src/js/views/form_renderer.js @@ -0,0 +1,249 @@ +/* Copyright 2022 Tecnativa - Alexandre D. Díaz + * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ +odoo.define("web_pwa_oca.FormRenderer", function(require) { + "use strict"; + + require("web_pwa_oca.BasicRenderer"); + var FormRenderer = require("web.FormRenderer"); + require("mail.form_renderer"); + + FormRenderer.include({ + /** + * @override + */ + _renderNode: function(node) { + if ( + this._isPWAOmittedNode(node) || + (this._isPWAStandalone && + node.tag === "div" && + node.attrs.class === "oe_chatter" && + this.$el.hasClass("oe_pwa_standalone_no_chatter")) + ) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderGenericTag: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderButtonBox: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderStatButton: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTagWidget: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTagButton: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTagHeader: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTagField: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTagNotebook: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTagSeparator: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderInnerGroupField: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderInnerGroupLabel: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderInnerGroup: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderOuterGroup: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderFieldWidget: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTagLabel: function(node) { + if ( + this._isPWAOmittedNode(node) || + (this._isPWAStandalone && + node.attrs && + node.attrs.modifiers && + node.attrs.modifiers.invisible === true) + ) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTagSheet: function() { + var $sheet = this._super.apply(this, arguments); + if ( + this._isPWAStandalone && + this.$el.hasClass("oe_pwa_standalone_no_sheet") + ) { + $sheet.attr("class", ""); + this.has_sheet = false; + } + return $sheet; + }, + + /** + * @override + */ + _renderTagForm: function() { + var $form = this._super.apply(this, arguments); + if (this._isPWAStandalone) { + $form.addClass("oe_pwa_standalone"); + } + return $form; + }, + + /** + * @override + */ + _renderTabHeader: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderTabPage: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderHeaderButton: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + _renderHeaderButtons: function(node) { + if (this._isPWAOmittedNode(node)) { + return $(); + } + return this._super.apply(this, arguments); + }, + }); +}); diff --git a/web_pwa_oca/static/src/js/webclient.js b/web_pwa_oca/static/src/js/webclient.js index 84611d0757bc..941a562b8cde 100644 --- a/web_pwa_oca/static/src/js/webclient.js +++ b/web_pwa_oca/static/src/js/webclient.js @@ -13,7 +13,8 @@ odoo.define("web_pwa_oca.webclient", function(require) { */ show_application: function() { this.pwa_manager = new PWAManager(this); - return this._super.apply(this, arguments); + const def = this.pwa_manager.start(); + return Promise.all([this._super.apply(this, arguments), def]); }, }); }); diff --git a/web_pwa_oca/static/src/js/worker/pwa.js b/web_pwa_oca/static/src/js/worker/pwa.js index abef47070b64..f77b1838079a 100644 --- a/web_pwa_oca/static/src/js/worker/pwa.js +++ b/web_pwa_oca/static/src/js/worker/pwa.js @@ -1,22 +1,21 @@ /* Copyright 2020 Tecnativa - Alexandre D. Díaz * License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */ -/** - * Services workers are a piece of software separated from the user page. - * Here can't use 'Odoo Bootstrap', so we can't work with 'require' system. - * When the service worker is called to be installed from the "pwa_manager" - * this class is instantiated. - */ - odoo.define("web_pwa_oca.PWA", function(require) { "use strict"; const OdooClass = require("web.Class"); const PWA = OdooClass.extend({ - // eslint-disable-next-line init: function(params) { - // To be overridden + this._sw_version = params.sw_version; + }, + + /** + * @returns {Promise} + */ + start: function() { + return Promise.resolve(); }, /** @@ -30,10 +29,19 @@ odoo.define("web_pwa_oca.PWA", function(require) { /** * @returns {Promise} */ - activateWorker: function() { + /* eslint-disable no-unused-vars */ + activateWorker: function(forced) { // To be overridden return Promise.resolve(); }, + + /** + * @returns {Promise} + */ + processRequest: function(request) { + // To be overridden + return fetch(request); + }, }); return PWA; diff --git a/web_pwa_oca/static/src/scss/main.scss b/web_pwa_oca/static/src/scss/main.scss new file mode 100644 index 000000000000..a31f073551f4 --- /dev/null +++ b/web_pwa_oca/static/src/scss/main.scss @@ -0,0 +1,9 @@ +.disable-pull-refresh { + overscroll-behavior: none; +} + +.o_form_nosheet { + .o_form_statusbar { + margin-bottom: 0px !important; + } +} diff --git a/web_pwa_oca/templates/assets.xml b/web_pwa_oca/templates/assets.xml index 37554c6ba71f..5f094644b847 100644 --- a/web_pwa_oca/templates/assets.xml +++ b/web_pwa_oca/templates/assets.xml @@ -15,10 +15,31 @@ t-set="pwa_name" t-value="request.env['ir.config_parameter'].sudo().get_param('pwa.manifest.name')" /> + + + + + + + + + + - + +