mirror of https://github.com/OCA/web.git
[IMP] web_pwa_oca: Refactor to use OdooClass
parent
366b35dc10
commit
a74fbb34c0
|
@ -78,10 +78,31 @@ Known issues / Roadmap
|
||||||
======================
|
======================
|
||||||
|
|
||||||
* Evaluate to extend ``FILES_TO_CACHE``
|
* Evaluate to extend ``FILES_TO_CACHE``
|
||||||
* Evaluate to use a normal JS file for service worker and download data from a normal JSON controller
|
|
||||||
* Integrate `Notification API <https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification>`_
|
* Integrate `Notification API <https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification>`_
|
||||||
* Integrate `Web Share API <https://web.dev/web-share/>`_
|
* Integrate `Web Share API <https://web.dev/web-share/>`_
|
||||||
* Create ``portal_pwa`` module, intended to be used by front-end users (customers, suppliers...)
|
* Create ``portal_pwa`` module, intended to be used by front-end users (customers, suppliers...)
|
||||||
|
* Current ``John Resig's inheritance`` doesn't support ``async`` functions. So we need use the following workaround:
|
||||||
|
- Natural 'async/await' example (This breaks "_super" call)
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
const MyClass = OdooClass.extend({
|
||||||
|
myFunc: async function() {
|
||||||
|
const mydata = await ...do await stuff...
|
||||||
|
return mydata;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
- Same code with the workaround:
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
const MyClass = OdooClass.extend({
|
||||||
|
myFunc: function() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const mydata = await ...do await stuff...
|
||||||
|
resolve(mydata);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Bug Tracker
|
Bug Tracker
|
||||||
===========
|
===========
|
||||||
|
@ -100,6 +121,7 @@ Authors
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
* TAKOBI
|
* TAKOBI
|
||||||
|
* Tecnativa
|
||||||
|
|
||||||
Contributors
|
Contributors
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
@ -108,6 +130,10 @@ Contributors
|
||||||
|
|
||||||
* Lorenzo Battistini
|
* Lorenzo Battistini
|
||||||
|
|
||||||
|
* `Tecnativa <https://tecnativa.com>`_:
|
||||||
|
|
||||||
|
* Alexandre D. Díaz
|
||||||
|
|
||||||
Maintainers
|
Maintainers
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# Copyright 2020 Lorenzo Battistini @ TAKOBI
|
# Copyright 2020 Lorenzo Battistini @ TAKOBI
|
||||||
|
# Copyright 2020 Tecnativa - Alexandre D. Díaz
|
||||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -8,7 +9,7 @@
|
||||||
"development_status": "Beta",
|
"development_status": "Beta",
|
||||||
"category": "Website",
|
"category": "Website",
|
||||||
"website": "https://github.com/OCA/web",
|
"website": "https://github.com/OCA/web",
|
||||||
"author": "TAKOBI, Odoo Community Association (OCA)",
|
"author": "TAKOBI, Tecnativa, Odoo Community Association (OCA)",
|
||||||
"maintainers": ["eLBati"],
|
"maintainers": ["eLBati"],
|
||||||
"license": "LGPL-3",
|
"license": "LGPL-3",
|
||||||
"application": True,
|
"application": True,
|
||||||
|
@ -18,7 +19,8 @@
|
||||||
'mail',
|
'mail',
|
||||||
],
|
],
|
||||||
"data": [
|
"data": [
|
||||||
"views/webclient_templates.xml",
|
"templates/assets.xml",
|
||||||
|
"templates/service_worker.xml",
|
||||||
],
|
],
|
||||||
'qweb': [
|
'qweb': [
|
||||||
'static/src/xml/pwa_install.xml',
|
'static/src/xml/pwa_install.xml',
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
|
# Copyright 2020 Lorenzo Battistini @ TAKOBI
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||||
from . import main
|
from . import main
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
|
# Copyright 2020 Lorenzo Battistini @ TAKOBI
|
||||||
|
# Copyright 2020 Tecnativa - Alexandre D. Díaz
|
||||||
|
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||||
|
import json
|
||||||
from odoo.http import request, Controller, route
|
from odoo.http import request, Controller, route
|
||||||
|
|
||||||
|
|
||||||
class PWA(Controller):
|
class PWA(Controller):
|
||||||
|
|
||||||
def get_asset_urls(self, asset_xml_id):
|
def _get_pwa_scripts(self):
|
||||||
qweb = request.env['ir.qweb'].sudo()
|
""" Scripts to be imported in the service worker (Order is important) """
|
||||||
assets = qweb._get_asset_nodes(asset_xml_id, {}, True, True)
|
return [
|
||||||
|
'/web/static/lib/underscore/underscore.js',
|
||||||
|
'/web_pwa_oca/static/src/js/worker/libs/class.js',
|
||||||
|
'/web_pwa_oca/static/src/js/worker/base/tools.js',
|
||||||
|
'/web_pwa_oca/static/src/js/worker/base/cache_manager.js',
|
||||||
|
'/web_pwa_oca/static/src/js/worker/pwa.js',
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_asset_urls(self, asset_xml_id):
|
||||||
|
"""Get all url's that have 'asset_xml_id'"""
|
||||||
|
qweb_sudo = request.env['ir.qweb'].sudo()
|
||||||
|
assets = qweb_sudo._get_asset_nodes(asset_xml_id, {}, True, True)
|
||||||
urls = []
|
urls = []
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
if asset[0] == 'link':
|
if asset[0] == 'link':
|
||||||
|
@ -14,62 +29,93 @@ class PWA(Controller):
|
||||||
urls.append(asset[1]['src'])
|
urls.append(asset[1]['src'])
|
||||||
return urls
|
return urls
|
||||||
|
|
||||||
@route('/service-worker.js', type='http', auth="public")
|
def _get_pwa_params(self):
|
||||||
def service_worker(self):
|
"""Get javascript PWA class initialzation params"""
|
||||||
qweb = request.env['ir.qweb'].sudo()
|
|
||||||
urls = []
|
urls = []
|
||||||
urls.extend(self.get_asset_urls("web.assets_common"))
|
urls.extend(self._get_asset_urls("web.assets_common"))
|
||||||
urls.extend(self.get_asset_urls("web.assets_backend"))
|
urls.extend(self._get_asset_urls("web.assets_backend"))
|
||||||
version_list = []
|
version_list = []
|
||||||
for url in urls:
|
for url in urls:
|
||||||
version_list.append(url.split('/')[3])
|
version_list.append(url.split('/')[3])
|
||||||
cache_version = '-'.join(version_list)
|
cache_version = '-'.join(version_list)
|
||||||
mimetype = 'text/javascript;charset=utf-8'
|
return [
|
||||||
content = qweb.render('web_pwa_oca.service_worker', {
|
cache_version,
|
||||||
'pwa_cache_name': cache_version,
|
urls
|
||||||
'pwa_files_to_cache': urls,
|
]
|
||||||
})
|
|
||||||
return request.make_response(content, [('Content-Type', mimetype)])
|
|
||||||
|
|
||||||
@route('/web_pwa_oca/manifest.json', type='http', auth="public")
|
@route('/service-worker.js', type='http', auth="public")
|
||||||
def manifest(self):
|
def render_service_worker(self):
|
||||||
qweb = request.env['ir.qweb'].sudo()
|
"""Route to register the service worker in the 'main' scope ('/')"""
|
||||||
config_param = request.env['ir.config_parameter'].sudo()
|
return request.render('web_pwa_oca.service_worker', {
|
||||||
pwa_name = config_param.get_param("pwa.manifest.name", "Odoo PWA")
|
'pwa_scripts': self._get_pwa_scripts(),
|
||||||
pwa_short_name = config_param.get_param("pwa.manifest.short_name", "Odoo PWA")
|
'pwa_params': self._get_pwa_params(),
|
||||||
icon128x128 = config_param.get_param(
|
}, headers=[('Content-Type', 'text/javascript;charset=utf-8')])
|
||||||
|
|
||||||
|
def _get_pwa_manifest(self):
|
||||||
|
"""Webapp manifest"""
|
||||||
|
config_param_sudo = request.env['ir.config_parameter'].sudo()
|
||||||
|
pwa_name = config_param_sudo.get_param("pwa.manifest.name", "Odoo PWA")
|
||||||
|
pwa_short_name = config_param_sudo.get_param(
|
||||||
|
"pwa.manifest.short_name", "Odoo PWA")
|
||||||
|
icon128x128 = config_param_sudo.get_param(
|
||||||
"pwa.manifest.icon128x128",
|
"pwa.manifest.icon128x128",
|
||||||
"/web_pwa_oca/static/img/icons/icon-128x128.png")
|
"/web_pwa_oca/static/img/icons/icon-128x128.png")
|
||||||
icon144x144 = config_param.get_param(
|
icon144x144 = config_param_sudo.get_param(
|
||||||
"pwa.manifest.icon144x144",
|
"pwa.manifest.icon144x144",
|
||||||
"/web_pwa_oca/static/img/icons/icon-144x144.png")
|
"/web_pwa_oca/static/img/icons/icon-144x144.png")
|
||||||
icon152x152 = config_param.get_param(
|
icon152x152 = config_param_sudo.get_param(
|
||||||
"pwa.manifest.icon152x152",
|
"pwa.manifest.icon152x152",
|
||||||
"/web_pwa_oca/static/img/icons/icon-152x152.png")
|
"/web_pwa_oca/static/img/icons/icon-152x152.png")
|
||||||
icon192x192 = config_param.get_param(
|
icon192x192 = config_param_sudo.get_param(
|
||||||
"pwa.manifest.icon192x192",
|
"pwa.manifest.icon192x192",
|
||||||
"/web_pwa_oca/static/img/icons/icon-192x192.png")
|
"/web_pwa_oca/static/img/icons/icon-192x192.png")
|
||||||
icon256x256 = config_param.get_param(
|
icon256x256 = config_param_sudo.get_param(
|
||||||
"pwa.manifest.icon256x256",
|
"pwa.manifest.icon256x256",
|
||||||
"/web_pwa_oca/static/img/icons/icon-256x256.png")
|
"/web_pwa_oca/static/img/icons/icon-256x256.png")
|
||||||
icon512x512 = config_param.get_param(
|
icon512x512 = config_param_sudo.get_param(
|
||||||
"pwa.manifest.icon512x512",
|
"pwa.manifest.icon512x512",
|
||||||
"/web_pwa_oca/static/img/icons/icon-512x512.png")
|
"/web_pwa_oca/static/img/icons/icon-512x512.png")
|
||||||
background_color = config_param.get_param(
|
background_color = config_param_sudo.get_param(
|
||||||
"pwa.manifest.background_color", "#2E69B5")
|
"pwa.manifest.background_color", "#2E69B5")
|
||||||
theme_color = config_param.get_param(
|
theme_color = config_param_sudo.get_param(
|
||||||
"pwa.manifest.theme_color", "#2E69B5")
|
"pwa.manifest.theme_color", "#2E69B5")
|
||||||
mimetype = 'application/json;charset=utf-8'
|
return {
|
||||||
content = qweb.render('web_pwa_oca.manifest', {
|
"name": pwa_name,
|
||||||
'pwa_name': pwa_name,
|
"short_name": pwa_short_name,
|
||||||
'pwa_short_name': pwa_short_name,
|
"icons": [{
|
||||||
'icon128x128': icon128x128,
|
"src": icon128x128,
|
||||||
'icon144x144': icon144x144,
|
"sizes": "128x128",
|
||||||
'icon152x152': icon152x152,
|
"type": "image/png"
|
||||||
'icon192x192': icon192x192,
|
}, {
|
||||||
'icon256x256': icon256x256,
|
"src": icon144x144,
|
||||||
'icon512x512': icon512x512,
|
"sizes": "144x144",
|
||||||
'background_color': background_color,
|
"type": "image/png"
|
||||||
'theme_color': theme_color,
|
}, {
|
||||||
})
|
"src": icon152x152,
|
||||||
return request.make_response(content, [('Content-Type', mimetype)])
|
"sizes": "152x152",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": icon192x192,
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": icon256x256,
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": icon512x512,
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}],
|
||||||
|
"start_url": "/web",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": background_color,
|
||||||
|
"theme_color": theme_color
|
||||||
|
}
|
||||||
|
|
||||||
|
@route('/web_pwa_oca/manifest.webmanifest', type='http', auth="public")
|
||||||
|
def pwa_manifest(self):
|
||||||
|
"""Returns the manifest used to install the page as app"""
|
||||||
|
return request.make_response(
|
||||||
|
json.dumps(self._get_pwa_manifest()),
|
||||||
|
headers=[('Content-Type', 'text/javascript;charset=utf-8')])
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
* `TAKOBI <https://takobi.online>`_:
|
* `TAKOBI <https://takobi.online>`_:
|
||||||
|
|
||||||
* Lorenzo Battistini
|
* Lorenzo Battistini
|
||||||
|
|
||||||
|
* `Tecnativa <https://tecnativa.com>`_:
|
||||||
|
|
||||||
|
* Alexandre D. Díaz
|
||||||
|
|
|
@ -1,5 +1,26 @@
|
||||||
* Evaluate to extend ``FILES_TO_CACHE``
|
* Evaluate to extend ``FILES_TO_CACHE``
|
||||||
* Evaluate to use a normal JS file for service worker and download data from a normal JSON controller
|
|
||||||
* Integrate `Notification API <https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification>`_
|
* Integrate `Notification API <https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification>`_
|
||||||
* Integrate `Web Share API <https://web.dev/web-share/>`_
|
* Integrate `Web Share API <https://web.dev/web-share/>`_
|
||||||
* Create ``portal_pwa`` module, intended to be used by front-end users (customers, suppliers...)
|
* Create ``portal_pwa`` module, intended to be used by front-end users (customers, suppliers...)
|
||||||
|
* Current ``John Resig's inheritance`` implementation doesn't support ``async`` functions because ``this._super`` can't be called inside a promise. So we need use the following workaround:
|
||||||
|
- Natural 'async/await' example (This breaks "_super" call)
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
var MyClass = OdooClass.extend({
|
||||||
|
myFunc: async function() {
|
||||||
|
const mydata = await ...do await stuff...
|
||||||
|
return mydata;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
- Same code with the workaround:
|
||||||
|
.. code-block:: javascript
|
||||||
|
|
||||||
|
var MyClass = OdooClass.extend({
|
||||||
|
myFunc: function() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const mydata = await ...do await stuff...
|
||||||
|
resolve(mydata);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -420,12 +420,50 @@ And like all other installed apps, it’s a top level app in the task switcher.<
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="known-issues-roadmap">
|
<div class="section" id="known-issues-roadmap">
|
||||||
<h1><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h1>
|
<h1><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h1>
|
||||||
<ul class="simple">
|
<ul>
|
||||||
<li>Evaluate to extend <tt class="docutils literal">FILES_TO_CACHE</tt></li>
|
<li><p class="first">Evaluate to extend <tt class="docutils literal">FILES_TO_CACHE</tt></p>
|
||||||
<li>Evaluate to use a normal JS file for service worker and download data from a normal JSON controller</li>
|
</li>
|
||||||
<li>Integrate <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification">Notification API</a></li>
|
<li><p class="first">Integrate <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification">Notification API</a></p>
|
||||||
<li>Integrate <a class="reference external" href="https://web.dev/web-share/">Web Share API</a></li>
|
</li>
|
||||||
<li>Create <tt class="docutils literal">portal_pwa</tt> module, intended to be used by front-end users (customers, suppliers…)</li>
|
<li><p class="first">Integrate <a class="reference external" href="https://web.dev/web-share/">Web Share API</a></p>
|
||||||
|
</li>
|
||||||
|
<li><p class="first">Create <tt class="docutils literal">portal_pwa</tt> module, intended to be used by front-end users (customers, suppliers…)</p>
|
||||||
|
</li>
|
||||||
|
<li><dl class="first docutils">
|
||||||
|
<dt>Current <tt class="docutils literal">John Resig's inheritance</tt> doesn’t support <tt class="docutils literal">async</tt> functions. So we need use the following workaround:</dt>
|
||||||
|
<dd><ul class="first last">
|
||||||
|
<li><dl class="first docutils">
|
||||||
|
<dt>Natural ‘async/await’ example (This breaks “_super” call)</dt>
|
||||||
|
<dd><pre class="code javascript first last literal-block">
|
||||||
|
<span class="kr">const</span> <span class="nx">MyClass</span> <span class="o">=</span> <span class="nx">OdooClass</span><span class="p">.</span><span class="nx">extend</span><span class="p">({</span>
|
||||||
|
<span class="nx">myFunc</span><span class="o">:</span> <span class="nx">async</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
|
||||||
|
<span class="kr">const</span> <span class="nx">mydata</span> <span class="o">=</span> <span class="nx">await</span> <span class="p">...</span><span class="k">do</span> <span class="nx">await</span> <span class="nx">stuff</span><span class="p">...</span>
|
||||||
|
<span class="k">return</span> <span class="nx">mydata</span><span class="p">;</span>
|
||||||
|
<span class="p">}</span>
|
||||||
|
<span class="p">});</span>
|
||||||
|
</pre>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
<li><dl class="first docutils">
|
||||||
|
<dt>Same code with the workaround:</dt>
|
||||||
|
<dd><pre class="code javascript first last literal-block">
|
||||||
|
<span class="kr">const</span> <span class="nx">MyClass</span> <span class="o">=</span> <span class="nx">OdooClass</span><span class="p">.</span><span class="nx">extend</span><span class="p">({</span>
|
||||||
|
<span class="nx">myFunc</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
|
||||||
|
<span class="k">return</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="nx">async</span> <span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="p">=></span> <span class="p">{</span>
|
||||||
|
<span class="kr">const</span> <span class="nx">mydata</span> <span class="o">=</span> <span class="nx">await</span> <span class="p">...</span><span class="k">do</span> <span class="nx">await</span> <span class="nx">stuff</span><span class="p">...</span>
|
||||||
|
<span class="nx">resolve</span><span class="p">(</span><span class="nx">mydata</span><span class="p">);</span>
|
||||||
|
<span class="p">});</span>
|
||||||
|
<span class="p">}</span>
|
||||||
|
<span class="p">});</span>
|
||||||
|
</pre>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="bug-tracker">
|
<div class="section" id="bug-tracker">
|
||||||
|
@ -442,6 +480,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
|
||||||
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
|
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
|
||||||
<ul class="simple">
|
<ul class="simple">
|
||||||
<li>TAKOBI</li>
|
<li>TAKOBI</li>
|
||||||
|
<li>Tecnativa</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="contributors">
|
<div class="section" id="contributors">
|
||||||
|
@ -451,6 +490,10 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
|
||||||
<li>Lorenzo Battistini</li>
|
<li>Lorenzo Battistini</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li><a class="reference external" href="https://tecnativa.com">Tecnativa</a>:<ul>
|
||||||
|
<li>Alexandre D. Díaz</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="section" id="maintainers">
|
<div class="section" id="maintainers">
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
|
/* Copyright 2020 Lorenzo Battistini @ TAKOBI
|
||||||
|
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
|
||||||
|
|
||||||
odoo.define('web_pwa_oca.systray.install', function(require) {
|
odoo.define('web_pwa_oca.systray.install', function(require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var core = require('web.core');
|
|
||||||
var session = require('web.session');
|
|
||||||
var UserMenu = require('web.UserMenu');
|
var UserMenu = require('web.UserMenu');
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
window.addEventListener('load', function () {
|
|
||||||
navigator.serviceWorker.register('/service-worker.js')
|
|
||||||
.then(function (reg) {
|
|
||||||
console.log('Service worker registered.', reg);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var deferredInstallPrompt = null;
|
var deferredInstallPrompt = null;
|
||||||
|
|
||||||
|
|
||||||
UserMenu.include({
|
UserMenu.include({
|
||||||
start: function() {
|
start: function() {
|
||||||
window.addEventListener('beforeinstallprompt', this.saveBeforeInstallPromptEvent);
|
window.addEventListener('beforeinstallprompt', this.saveBeforeInstallPromptEvent);
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/* Copyright 2020 Tecnativa - Alexandre D. Díaz
|
||||||
|
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
|
||||||
|
|
||||||
|
odoo.define("web_pwa_oca.PWAManager", function(require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var Class = require("web.Class");
|
||||||
|
var mixins = require("web.mixins");
|
||||||
|
|
||||||
|
|
||||||
|
var PWAManager = Class.extend(mixins.ParentedMixin, {
|
||||||
|
init: function() {
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
throw new Error(
|
||||||
|
"This browser is not compatible with service workers");
|
||||||
|
}
|
||||||
|
this._service_worker = navigator.serviceWorker;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} sw_script
|
||||||
|
* @param {Function} success_callback
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
registerServiceWorker: function(sw_script, success_callback) {
|
||||||
|
return this._service_worker.register(sw_script)
|
||||||
|
.then(success_callback)
|
||||||
|
.catch(function(error) {
|
||||||
|
console.log('[ServiceWorker] Registration failed: ', error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return PWAManager;
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
/* Copyright 2020 Tecnativa - Alexandre D. Díaz
|
||||||
|
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
|
||||||
|
|
||||||
|
odoo.define("web_pwa_oca.webclient", function(require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var WebClient = require("web.WebClient");
|
||||||
|
var PWAManager = require("web_pwa_oca.PWAManager");
|
||||||
|
|
||||||
|
WebClient.include({
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
show_application: function() {
|
||||||
|
this.pwa_manager = new PWAManager();
|
||||||
|
this.pwa_manager.setParent(this);
|
||||||
|
this.pwa_manager.registerServiceWorker('/service-worker.js', this._onRegisterServiceWorker);
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Need register some extra API? override this!
|
||||||
|
*
|
||||||
|
* @param {ServiceWorkerRegistration} registration
|
||||||
|
*/
|
||||||
|
_onRegisterServiceWorker: function(registration) {
|
||||||
|
console.log('[ServiceWorker] Registered:', registration);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
/* Copyright 2020 Tecnativa - Alexandre D. Díaz
|
||||||
|
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const CacheManager = OdooClass.extend({
|
||||||
|
init: function() {
|
||||||
|
this._cache = caches;
|
||||||
|
this._caches = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise[CacheStorage]}
|
||||||
|
*/
|
||||||
|
initCache: function(cache_name) {
|
||||||
|
if (_.has(this._caches, cache_name)) {
|
||||||
|
return Promise.resolve(this._caches[cache_name]);
|
||||||
|
}
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
this._caches[cache_name] = await this._cache.open(cache_name);
|
||||||
|
resolve(this._caches[cache_name]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {CacheStorage}
|
||||||
|
*/
|
||||||
|
get: function(cache_name) {
|
||||||
|
return this._caches[cache_name];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
clean: function() {
|
||||||
|
return this._cache.keys().then(keyList => {
|
||||||
|
return Promise.all(keyList.map(key => {
|
||||||
|
if (!_.has(this._caches, key)) {
|
||||||
|
console.log('[ServiceWorker] Removing old cache', key);
|
||||||
|
return this._cache.delete(key);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,17 @@
|
||||||
|
/* Copyright 2020 Tecnativa - Alexandre D. Díaz
|
||||||
|
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute various promises in sequential order and return a promise with all results
|
||||||
|
* Code from: https://decembersoft.com/posts/promises-in-serial-with-array-reduce/
|
||||||
|
*
|
||||||
|
* @param {Array[Promise]} tasks
|
||||||
|
* @returns {Promise[Array]}
|
||||||
|
*/
|
||||||
|
function execSequentialPromises(tasks) {
|
||||||
|
return tasks.reduce((promiseChain, currentTask) => {
|
||||||
|
return promiseChain.then(chainResults => currentTask.then(currentResult => [...chainResults, currentResult]));
|
||||||
|
}, Promise.resolve([]));
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
/**
|
||||||
|
* Improved John Resig's inheritance, based on:
|
||||||
|
*
|
||||||
|
* Simple JavaScript Inheritance
|
||||||
|
* By John Resig http://ejohn.org/
|
||||||
|
* MIT Licensed.
|
||||||
|
*
|
||||||
|
* Adds "include()"
|
||||||
|
*
|
||||||
|
* Defines The Class object. That object can be used to define and inherit classes using
|
||||||
|
* the extend() method.
|
||||||
|
*
|
||||||
|
* Example::
|
||||||
|
*
|
||||||
|
* var Person = Class.extend({
|
||||||
|
* init: function(isDancing){
|
||||||
|
* this.dancing = isDancing;
|
||||||
|
* },
|
||||||
|
* dance: function(){
|
||||||
|
* return this.dancing;
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* The init() method act as a constructor. This class can be instanced this way::
|
||||||
|
*
|
||||||
|
* var person = new Person(true);
|
||||||
|
* person.dance();
|
||||||
|
*
|
||||||
|
* The Person class can also be extended again:
|
||||||
|
*
|
||||||
|
* var Ninja = Person.extend({
|
||||||
|
* init: function(){
|
||||||
|
* this._super( false );
|
||||||
|
* },
|
||||||
|
* dance: function(){
|
||||||
|
* // Call the inherited version of dance()
|
||||||
|
* return this._super();
|
||||||
|
* },
|
||||||
|
* swingSword: function(){
|
||||||
|
* return true;
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* When extending a class, each re-defined method can use this._super() to call the previous
|
||||||
|
* implementation of that method.
|
||||||
|
*
|
||||||
|
* @class Class
|
||||||
|
*/
|
||||||
|
function OdooClass(){}
|
||||||
|
|
||||||
|
var initializing = false;
|
||||||
|
var fnTest = /xyz/.test(function(){xyz();}) ? /\b_super\b/ : /.*/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclass an existing class
|
||||||
|
*
|
||||||
|
* @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class
|
||||||
|
*/
|
||||||
|
OdooClass.extend = function() {
|
||||||
|
var _super = this.prototype;
|
||||||
|
// Support mixins arguments
|
||||||
|
var args = _.toArray(arguments);
|
||||||
|
args.unshift({});
|
||||||
|
var prop = _.extend.apply(_,args);
|
||||||
|
|
||||||
|
// Instantiate a web class (but only create the instance,
|
||||||
|
// don't run the init constructor)
|
||||||
|
initializing = true;
|
||||||
|
var This = this;
|
||||||
|
var prototype = new This();
|
||||||
|
initializing = false;
|
||||||
|
|
||||||
|
// Copy the properties over onto the new prototype
|
||||||
|
_.each(prop, function(val, name) {
|
||||||
|
// Check if we're overwriting an existing function
|
||||||
|
prototype[name] = typeof prop[name] == "function" &&
|
||||||
|
fnTest.test(prop[name]) ?
|
||||||
|
(function(name, fn) {
|
||||||
|
return function() {
|
||||||
|
var tmp = this._super;
|
||||||
|
|
||||||
|
// Add a new ._super() method that is the same
|
||||||
|
// method but on the super-class
|
||||||
|
this._super = _super[name];
|
||||||
|
|
||||||
|
// The method only need to be bound temporarily, so
|
||||||
|
// we remove it when we're done executing
|
||||||
|
var ret = fn.apply(this, arguments);
|
||||||
|
this._super = tmp;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
})(name, prop[name]) :
|
||||||
|
prop[name];
|
||||||
|
});
|
||||||
|
|
||||||
|
// The dummy class constructor
|
||||||
|
function Class() {
|
||||||
|
if(this.constructor !== OdooClass){
|
||||||
|
throw new Error("You can only instanciate objects with the 'new' operator");
|
||||||
|
}
|
||||||
|
// All construction is actually done in the init method
|
||||||
|
this._super = null;
|
||||||
|
if (!initializing && this.init) {
|
||||||
|
var ret = this.init.apply(this, arguments);
|
||||||
|
if (ret) { return ret; }
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
Class.include = function (properties) {
|
||||||
|
_.each(properties, function(val, name) {
|
||||||
|
if (typeof properties[name] !== 'function'
|
||||||
|
|| !fnTest.test(properties[name])) {
|
||||||
|
prototype[name] = properties[name];
|
||||||
|
} else if (typeof prototype[name] === 'function'
|
||||||
|
&& prototype.hasOwnProperty(name)) {
|
||||||
|
prototype[name] = (function (name, fn, previous) {
|
||||||
|
return function () {
|
||||||
|
var tmp = this._super;
|
||||||
|
this._super = previous;
|
||||||
|
var ret = fn.apply(this, arguments);
|
||||||
|
this._super = tmp;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
})(name, properties[name], prototype[name]);
|
||||||
|
} else if (typeof _super[name] === 'function') {
|
||||||
|
prototype[name] = (function (name, fn) {
|
||||||
|
return function () {
|
||||||
|
var tmp = this._super;
|
||||||
|
this._super = _super[name];
|
||||||
|
var ret = fn.apply(this, arguments);
|
||||||
|
this._super = tmp;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
})(name, properties[name]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Populate our constructed prototype object
|
||||||
|
Class.prototype = prototype;
|
||||||
|
|
||||||
|
// Enforce the constructor to be what we expect
|
||||||
|
Class.constructor = Class;
|
||||||
|
|
||||||
|
// And make this class extendable
|
||||||
|
Class.extend = this.extend;
|
||||||
|
|
||||||
|
return Class;
|
||||||
|
};
|
|
@ -0,0 +1,62 @@
|
||||||
|
/* Copyright 2020 Tecnativa - Alexandre D. Díaz
|
||||||
|
* License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var PWA = OdooClass.extend({
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} cache_name
|
||||||
|
* @param {Array[String]} prefetched_urls
|
||||||
|
*/
|
||||||
|
init: function (cache_name, prefetched_urls) {
|
||||||
|
this._cache_name = cache_name;
|
||||||
|
this._prefetched_urls = prefetched_urls;
|
||||||
|
this._cache = new CacheManager();
|
||||||
|
this._cacheLoadPromise = this._cache.initCache(this._cache_name);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
installWorker: function () {
|
||||||
|
return this._prefetchData();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
activateWorker: function () {
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
await this._cacheLoadPromise;
|
||||||
|
await this._cache.clean();
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise[Response]}
|
||||||
|
*/
|
||||||
|
processRequest: function (request) {
|
||||||
|
if (request.method === 'GET' && (request.cache !== 'only-if-cached' || request.mode === 'same-origin')) {
|
||||||
|
// Strategy: Cache First
|
||||||
|
return this._cache.get(this._cache_name).match(request).then(function(response){
|
||||||
|
return response || fetch(request);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Default browser behaviour
|
||||||
|
return fetch(request);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
_prefetchData: function() {
|
||||||
|
// Prefetch URL's
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this._cacheLoadPromise.then(async () => {
|
||||||
|
return resolve(await this._cache.get(this._cache_name).addAll(this._prefetched_urls));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<template id="web_layout_pwa" name="Web layout PWA" inherit_id="web.layout">
|
||||||
|
<xpath expr="//meta[@name='viewport']" position="after">
|
||||||
|
<!-- Add link rel manifest -->
|
||||||
|
<link rel="manifest" href="/web_pwa_oca/manifest.webmanifest"/>
|
||||||
|
<!-- Add iOS meta tags and icons -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Odoo PWA"/>
|
||||||
|
<link rel="apple-touch-icon" href="/web_pwa_oca/static/img/icons/icon-152x152.png"/>
|
||||||
|
<!-- Add meta theme-color -->
|
||||||
|
<meta name="theme-color" content="#2E69B5" />
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
<template id="assets_backend" name="web service worker assets" inherit_id="web.assets_backend">
|
||||||
|
<xpath expr="." position="inside">
|
||||||
|
<script type="text/javascript" src="/web_pwa_oca/static/src/js/pwa_manager.js"></script>
|
||||||
|
<script type="text/javascript" src="/web_pwa_oca/static/src/js/pwa_install.js"></script>
|
||||||
|
<script type="text/javascript" src="/web_pwa_oca/static/src/js/webclient.js"></script>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<template id="pwa_init" name="PWA Initialization">
|
||||||
|
const oca_pwa = new PWA(...<t t-esc="str(pwa_params)"/>);
|
||||||
|
</template>
|
||||||
|
<template id="pwa_core_events" name="PWA Core Events">
|
||||||
|
<t t-set="evt_install">
|
||||||
|
evt.waitUntil(oca_pwa.installWorker());
|
||||||
|
self.skipWaiting();
|
||||||
|
</t>
|
||||||
|
self.addEventListener('install', evt => {
|
||||||
|
console.log('[ServiceWorker] Installing...');
|
||||||
|
<t t-raw="evt_install" />
|
||||||
|
});
|
||||||
|
|
||||||
|
<t t-set="evt_fetch">
|
||||||
|
evt.respondWith(oca_pwa.processRequest(evt.request));
|
||||||
|
</t>
|
||||||
|
self.addEventListener('fetch', evt => {
|
||||||
|
console.log('[ServiceWorker] Fetching...');
|
||||||
|
<t t-raw="evt_fetch" />
|
||||||
|
});
|
||||||
|
|
||||||
|
<t t-set="evt_activate">
|
||||||
|
console.log('[ServiceWorker] Activating...');
|
||||||
|
evt.waitUntil(oca_pwa.activateWorker());
|
||||||
|
self.clients.claim();
|
||||||
|
</t>
|
||||||
|
self.addEventListener('activate', evt => {
|
||||||
|
<t t-raw="evt_activate" />
|
||||||
|
});
|
||||||
|
</template>
|
||||||
|
<template id="service_worker" name="PWA Service Worker">
|
||||||
|
self.importScripts(...<t t-esc="str(pwa_scripts)" />);
|
||||||
|
<t t-call="web_pwa_oca.pwa_init" />
|
||||||
|
<t t-call="web_pwa_oca.pwa_core_events" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
|
@ -1,105 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<template id="web_layout_pwa" name="Web layout PWA" inherit_id="web.layout">
|
|
||||||
<xpath expr="//meta[@name='viewport']" position="after">
|
|
||||||
<!-- Add link rel manifest -->
|
|
||||||
<link rel="manifest" href="/web_pwa_oca/manifest.json"/>
|
|
||||||
<!-- Add iOS meta tags and icons -->
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
|
|
||||||
<meta name="apple-mobile-web-app-title" content="Odoo PWA"/>
|
|
||||||
<link rel="apple-touch-icon" href="/web_pwa_oca/static/img/icons/icon-152x152.png"/>
|
|
||||||
<!-- Add meta theme-color -->
|
|
||||||
<meta name="theme-color" content="#2E69B5" />
|
|
||||||
</xpath>
|
|
||||||
</template>
|
|
||||||
<template id="assets_backend" name="PWA assets" inherit_id="web.assets_backend">
|
|
||||||
<xpath expr="." position="inside">
|
|
||||||
<script type="text/javascript" src="/web_pwa_oca/static/src/js/pwa_install.js"/>
|
|
||||||
</xpath>
|
|
||||||
</template>
|
|
||||||
<template id="service_worker" name="PWA service worker">
|
|
||||||
'use strict';
|
|
||||||
const CACHE_NAME = '<t t-esc="pwa_cache_name"/>';
|
|
||||||
const FILES_TO_CACHE = [
|
|
||||||
<t t-foreach="pwa_files_to_cache" t-as="file_to_cache">
|
|
||||||
'<t t-esc="file_to_cache"/>',
|
|
||||||
</t>
|
|
||||||
];
|
|
||||||
self.addEventListener('install', function (evt) {
|
|
||||||
console.log('[ServiceWorker] Install');
|
|
||||||
evt.waitUntil(
|
|
||||||
caches.open(CACHE_NAME).then(function (cache) {
|
|
||||||
console.log('[ServiceWorker] Pre-caching offline page');
|
|
||||||
return cache.addAll(FILES_TO_CACHE);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
self.skipWaiting();
|
|
||||||
});
|
|
||||||
self.addEventListener('activate', function(evt) {
|
|
||||||
console.log('[ServiceWorker] Activate');
|
|
||||||
evt.waitUntil(
|
|
||||||
caches.keys().then(function(keyList) {
|
|
||||||
return Promise.all(keyList.map(function(key) {
|
|
||||||
if (key !== CACHE_NAME) {
|
|
||||||
console.log('[ServiceWorker] Removing old cache', key);
|
|
||||||
return caches.delete(key);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
self.clients.claim();
|
|
||||||
});
|
|
||||||
self.addEventListener('fetch', function(evt) {
|
|
||||||
if (evt.request.cache === 'only-if-cached' && evt.request.mode !== 'same-origin') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('[ServiceWorker] Fetch', evt.request.url);
|
|
||||||
evt.respondWith(
|
|
||||||
caches.open(CACHE_NAME).then(function(cache) {
|
|
||||||
return cache.match(evt.request)
|
|
||||||
.then(function(response) {
|
|
||||||
return response || fetch(evt.request);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template id="manifest" name="PWA manifest">
|
|
||||||
{
|
|
||||||
"name": "<t t-esc="pwa_name"/>",
|
|
||||||
"short_name": "<t t-esc="pwa_short_name"/>",
|
|
||||||
"icons": [{
|
|
||||||
"src": "<t t-esc="icon128x128"/>",
|
|
||||||
"sizes": "128x128",
|
|
||||||
"type": "image/png"
|
|
||||||
}, {
|
|
||||||
"src": "<t t-esc="icon144x144"/>",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image/png"
|
|
||||||
}, {
|
|
||||||
"src": "<t t-esc="icon152x152"/>",
|
|
||||||
"sizes": "152x152",
|
|
||||||
"type": "image/png"
|
|
||||||
}, {
|
|
||||||
"src": "<t t-esc="icon192x192"/>",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
}, {
|
|
||||||
"src": "<t t-esc="icon256x256"/>",
|
|
||||||
"sizes": "256x256",
|
|
||||||
"type": "image/png"
|
|
||||||
}, {
|
|
||||||
"src": "<t t-esc="icon512x512"/>",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}],
|
|
||||||
"start_url": "/web",
|
|
||||||
"display": "standalone",
|
|
||||||
"background_color": "<t t-esc="background_color"/>",
|
|
||||||
"theme_color": "<t t-esc="theme_color"/>"
|
|
||||||
}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</odoo>
|
|
Loading…
Reference in New Issue