From 9b9829416ba0b09901a3de1578ff172024ab4f35 Mon Sep 17 00:00:00 2001
From: DramixDw <rve@odoo.com>
Date: Thu, 23 Apr 2020 07:19:13 +0000
Subject: [PATCH] [IMP] website: simplify website menu
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Some apps, once installed, automatically create a menuitem in website.
What complexify the UI and create useless menu withtout plusvalue.

It is not because you install livechat to make support online, that you want
a link in your menu to show stats e.g.

Now we remove the default menu created, and help user to find it when he create
a link. The autocomplete suggest most of the main App's controllers

task-2189613

closes odoo/odoo#49081

Related: odoo/enterprise#9733
Signed-off-by: Jérémy Kersten (jke) <jke@openerp.com>
---
 addons/website/controllers/main.py            | 39 ++++++++++++++
 addons/website/models/website.py              | 12 ++++-
 addons/website/static/src/js/utils.js         | 53 +++++++++++++++----
 addons/website/tests/__init__.py              |  1 +
 addons/website/tests/test_controllers.py      | 44 +++++++++++++++
 addons/website_blog/models/website.py         |  6 +++
 .../data/crm_partner_assign_data.xml          | 10 ----
 .../models/__init__.py                        |  1 +
 .../models/website.py                         | 14 +++++
 addons/website_customer/__manifest__.py       |  1 -
 .../data/website_customer_data.xml            | 13 -----
 addons/website_customer/models/__init__.py    |  1 +
 addons/website_customer/models/website.py     | 14 +++++
 addons/website_event/models/__init__.py       |  1 +
 addons/website_event/models/website.py        | 14 +++++
 addons/website_forum/models/website.py        |  8 ++-
 .../data/config_data.xml                      |  6 ---
 .../website_hr_recruitment/models/__init__.py |  1 +
 .../website_hr_recruitment/models/website.py  | 14 +++++
 addons/website_livechat/controllers/main.py   |  1 +
 .../data/website_livechat_data.xml            |  7 ---
 addons/website_livechat/models/website.py     |  8 ++-
 .../views/website_livechat.xml                | 20 +++----
 .../data/mail_template_data.xml               |  9 ----
 .../website_mail_channel/models/__init__.py   |  1 +
 addons/website_mail_channel/models/website.py | 14 +++++
 addons/website_membership/__manifest__.py     |  1 -
 .../data/membership_data.xml                  | 13 -----
 addons/website_membership/models/__init__.py  |  1 +
 addons/website_membership/models/website.py   | 14 +++++
 addons/website_sale/models/website.py         |  8 ++-
 addons/website_slides/models/website.py       |  8 ++-
 32 files changed, 269 insertions(+), 89 deletions(-)
 create mode 100644 addons/website/tests/test_controllers.py
 create mode 100644 addons/website_crm_partner_assign/models/website.py
 delete mode 100644 addons/website_customer/data/website_customer_data.xml
 create mode 100644 addons/website_customer/models/website.py
 create mode 100644 addons/website_event/models/website.py
 create mode 100644 addons/website_hr_recruitment/models/website.py
 create mode 100644 addons/website_mail_channel/models/website.py
 delete mode 100644 addons/website_membership/data/membership_data.xml
 create mode 100644 addons/website_membership/models/website.py

diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 52d5f8e711f2..c4e0e20675a3 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -225,6 +225,45 @@ class Website(Home):
             raise werkzeug.exceptions.NotFound()
         return request.redirect(url)
 
+    @http.route('/website/get_suggested_links', type='json', auth="user", website=True)
+    def get_suggested_link(self, needle, limit=10):
+        current_website = request.website
+
+        matching_pages = []
+        for page in current_website.search_pages(needle, limit=int(limit)):
+            matching_pages.append({
+                'value': page['loc'],
+                'label': 'name' in page and '%s (%s)' % (page['loc'], page['name']) or page['loc'],
+            })
+        matching_urls = set(map(lambda match: match['value'], matching_pages))
+
+        matching_last_modified = []
+        last_modified_pages = current_website.get_website_pages(order='write_date desc', limit=5)
+        for url, name in last_modified_pages.mapped(lambda p: (p.url, p.name)):
+            if needle.lower() in name.lower() or needle.lower() in url.lower() and url not in matching_urls:
+                matching_last_modified.append({
+                    'value': url,
+                    'label': '%s (%s)' % (url, name),
+                })
+
+        suggested_controllers = []
+        for name, url, mod in current_website.get_suggested_controllers():
+            if needle.lower() in name.lower() or needle.lower() in url.lower():
+                module = mod and request.env.ref('base.module_%s' % mod, False)
+                icon = mod and "<img src='%s' width='24px' class='mr-2 rounded' /> " % (module and module.icon or mod) or ''
+                suggested_controllers.append({
+                    'value': url,
+                    'label': '%s%s (%s)' % (icon, url, name),
+                })
+
+        return {
+            'matching_pages': sorted(matching_pages, key=lambda o: o['label']),
+            'others': [
+                dict(title=_('Last modified pages'), values=matching_last_modified),
+                dict(title=_('Apps url'), values=suggested_controllers),
+            ]
+        }
+
     # ------------------------------------------------------
     # Edit
     # ------------------------------------------------------
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 9eeb0a985896..d230000c23f9 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -13,7 +13,7 @@ from werkzeug.datastructures import OrderedMultiDict
 from werkzeug.exceptions import NotFound
 
 from odoo import api, fields, models, tools
-from odoo.addons.http_routing.models.ir_http import slugify, _guess_mimetype
+from odoo.addons.http_routing.models.ir_http import slugify, _guess_mimetype, url_for
 from odoo.addons.website.models.ir_http import sitemap_qs2dom
 from odoo.addons.portal.controllers.portal import pager
 from odoo.http import request
@@ -862,7 +862,7 @@ class Website(models.Model):
 
     def get_website_pages(self, domain=[], order='name', limit=None):
         domain += self.get_current_website().website_domain()
-        pages = self.env['website.page'].search(domain, order='name', limit=limit)
+        pages = self.env['website.page'].search(domain, order=order, limit=limit)
         return pages
 
     def search_pages(self, needle=None, limit=None):
@@ -874,6 +874,14 @@ class Website(models.Model):
                 break
         return res
 
+    def get_suggested_controllers(self):
+        """
+            Returns a tuple (name, url, icon).
+            Where icon can be a module name, or a path
+        """
+        suggested_controllers = [(_('Homepage'), url_for('/'), 'website')]
+        return suggested_controllers
+
     @api.model
     def image_url(self, record, field, size=None):
         """ Returns a local url that points to the image field of a given browse record. """
diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js
index d8e69e3eba4d..7947eb6f93c3 100644
--- a/addons/website/static/src/js/utils.js
+++ b/addons/website/static/src/js/utils.js
@@ -33,7 +33,35 @@ function loadAnchors(url) {
  * @param {jQuery} $input
  */
 function autocompleteWithPages(self, $input) {
-    $input.autocomplete({
+    $.widget("website.urlcomplete", $.ui.autocomplete, {
+        _create: function () {
+            this._super();
+            this.widget().menu("option", "items", "> :not(.ui-autocomplete-category)");
+        },
+        _renderMenu: function (ul, items) {
+            const self = this;
+            items.forEach(item => {
+                if (item.separator) {
+                    self._renderSeparator(ul, item);
+                }
+                else {
+                    self._renderItem(ul, item);
+                }
+            });
+        },
+        _renderSeparator: function (ul, item) {
+            return $("<li class='ui-autocomplete-category font-weight-bold text-capitalize m-2'>")
+                   .append(`<div>${item.separator}</div>`)
+                   .appendTo(ul);
+        },
+        _renderItem: function (ul, item) {
+            return $("<li>")
+                   .data('ui-autocomplete-item', item)
+                   .append(`<div>${item.label}</div>`)
+                   .appendTo(ul);
+        },
+    });
+    $input.urlcomplete({
         source: function (request, response) {
             if (request.term[0] === '#') {
                 loadAnchors(request.term).then(function (anchors) {
@@ -41,17 +69,22 @@ function autocompleteWithPages(self, $input) {
                 });
             } else {
                 return self._rpc({
-                    model: 'website',
-                    method: 'search_pages',
-                    args: [null, request.term],
-                    kwargs: {
+                    route: '/website/get_suggested_links',
+                    params: {
+                        needle: request.term,
                         limit: 15,
-                    },
-                }).then(function (exists) {
-                    var rs = _.map(exists, function (r) {
-                        return r.loc;
+                    }
+                }).then(function (res) {
+                    let choices = res.matching_pages;
+                    res.others.forEach(other => {
+                        if (other.values.length) {
+                            choices = choices.concat(
+                                [{separator: other.title}],
+                                other.values,
+                            );
+                        }
                     });
-                    response(rs.sort());
+                    response(choices);
                 });
             }
         },
diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py
index 99ac6f1bc574..a9fac1e745be 100644
--- a/addons/website/tests/__init__.py
+++ b/addons/website/tests/__init__.py
@@ -17,3 +17,4 @@ from . import test_views
 from . import test_website_favicon
 from . import test_website_reset_password
 from . import test_website_visitor
+from . import test_controllers
diff --git a/addons/website/tests/test_controllers.py b/addons/website/tests/test_controllers.py
new file mode 100644
index 000000000000..edfb65feeb06
--- /dev/null
+++ b/addons/website/tests/test_controllers.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import json
+
+from odoo import tests
+from odoo.tools import mute_logger
+
+
+@tests.tagged('post_install', '-at_install')
+class TestControllers(tests.HttpCase):
+
+    @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
+    def test_last_created_pages_autocompletion(self):
+        self.authenticate("admin", "admin")
+        Page = self.env['website.page']
+        last_5_url_edited = []
+        base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+        suggested_links_url = base_url + '/website/get_suggested_links'
+
+        for i in range(0, 10):
+            new_page = Page.create({
+                'name': 'Generic',
+                'type': 'qweb',
+                'arch': '''
+                    <div>content</div>
+                ''',
+                'key': "test.generic_view-%d" % i,
+                'url': "/generic-%d" % i,
+                'is_published': True,
+            })
+            if i % 2 == 0:
+                # mark as old
+                new_page._write({'write_date': '2020-01-01'})
+            else:
+                last_5_url_edited.append(new_page.url)
+
+        res = self.opener.post(url=suggested_links_url, json={'params': {'needle': '/'}})
+        resp = json.loads(res.content)
+        assert 'result' in resp
+        suggested_links = resp['result']
+        last_modified_history = next(o for o in suggested_links['others'] if o["title"] == "Last modified pages")
+        last_modified_values = map(lambda o: o['value'], last_modified_history['values'])
+        self.assertTrue(set(last_modified_values) == set(last_5_url_edited))
diff --git a/addons/website_blog/models/website.py b/addons/website_blog/models/website.py
index 17478434aa91..a647326c3405 100644
--- a/addons/website_blog/models/website.py
+++ b/addons/website_blog/models/website.py
@@ -2,6 +2,7 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 from odoo import api, models, _
+from odoo.addons.http_routing.models.ir_http import url_for
 
 
 class Website(models.Model):
@@ -56,3 +57,8 @@ class Website(models.Model):
             })
 
         return dep
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Blog'), url_for('/blog'), 'website_blog'))
+        return suggested_controllers
diff --git a/addons/website_crm_partner_assign/data/crm_partner_assign_data.xml b/addons/website_crm_partner_assign/data/crm_partner_assign_data.xml
index f88fc4a7231d..5ca6dee5ea01 100644
--- a/addons/website_crm_partner_assign/data/crm_partner_assign_data.xml
+++ b/addons/website_crm_partner_assign/data/crm_partner_assign_data.xml
@@ -120,14 +120,4 @@
             <field name="auto_delete" eval="True"/>
         </record>
     </data>
-
-    <data noupdate="1">
-        <record id="menu_resellers" model="website.menu">
-            <field name="name">Resellers</field>
-            <field name="url">/partners</field>
-            <field name="parent_id" ref="website.main_menu"/>
-            <field name="sequence">55</field>
-        </record>
-    </data>
-
 </odoo>
diff --git a/addons/website_crm_partner_assign/models/__init__.py b/addons/website_crm_partner_assign/models/__init__.py
index 7021ef01854d..f5fb173f8f53 100644
--- a/addons/website_crm_partner_assign/models/__init__.py
+++ b/addons/website_crm_partner_assign/models/__init__.py
@@ -3,3 +3,4 @@
 
 from . import crm_lead
 from . import res_partner
+from . import website
diff --git a/addons/website_crm_partner_assign/models/website.py b/addons/website_crm_partner_assign/models/website.py
new file mode 100644
index 000000000000..5910fb511d34
--- /dev/null
+++ b/addons/website_crm_partner_assign/models/website.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, _
+from odoo.addons.http_routing.models.ir_http import url_for
+
+
+class Website(models.Model):
+    _inherit = "website"
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Resellers'), url_for('/partners'), 'website_crm_partner_assign'))
+        return suggested_controllers
diff --git a/addons/website_customer/__manifest__.py b/addons/website_customer/__manifest__.py
index 3dde2763480a..ab0d19a695db 100644
--- a/addons/website_customer/__manifest__.py
+++ b/addons/website_customer/__manifest__.py
@@ -18,7 +18,6 @@ Publish your customers as business references on your website to attract new pot
         'data/res_partner_demo.xml',
     ],
     'data': [
-        'data/website_customer_data.xml',
         'views/website_customer_templates.xml',
         'views/res_partner_views.xml',
         'security/ir.model.access.csv',
diff --git a/addons/website_customer/data/website_customer_data.xml b/addons/website_customer/data/website_customer_data.xml
deleted file mode 100644
index 46d3e781292e..000000000000
--- a/addons/website_customer/data/website_customer_data.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-
-    <data noupdate="1">
-        <record id="menu_customers" model="website.menu">
-            <field name="name">References</field>
-            <field name="url">/customers</field>
-            <field name="parent_id" ref="website.main_menu"/>
-            <field name="sequence">55</field>
-        </record>
-    </data>
-
-</odoo>
diff --git a/addons/website_customer/models/__init__.py b/addons/website_customer/models/__init__.py
index 9ec15f238e28..79be1c95b6dd 100644
--- a/addons/website_customer/models/__init__.py
+++ b/addons/website_customer/models/__init__.py
@@ -2,3 +2,4 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 from . import res_partner
+from . import website
diff --git a/addons/website_customer/models/website.py b/addons/website_customer/models/website.py
new file mode 100644
index 000000000000..fe812512cc51
--- /dev/null
+++ b/addons/website_customer/models/website.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, _
+from odoo.addons.http_routing.models.ir_http import url_for
+
+
+class Website(models.Model):
+    _inherit = "website"
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('References'), url_for('/customers'), 'website_customer'))
+        return suggested_controllers
diff --git a/addons/website_event/models/__init__.py b/addons/website_event/models/__init__.py
index e972cdb2a6f9..67661173e038 100644
--- a/addons/website_event/models/__init__.py
+++ b/addons/website_event/models/__init__.py
@@ -1,3 +1,4 @@
 # -*- coding: utf-8 -*-
 
 from . import event_event
+from . import website
diff --git a/addons/website_event/models/website.py b/addons/website_event/models/website.py
new file mode 100644
index 000000000000..bb34800aa269
--- /dev/null
+++ b/addons/website_event/models/website.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, _
+from odoo.addons.http_routing.models.ir_http import url_for
+
+
+class Website(models.Model):
+    _inherit = "website"
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Events'), url_for('/event'), 'website_event'))
+        return suggested_controllers
diff --git a/addons/website_forum/models/website.py b/addons/website_forum/models/website.py
index 29a42fd8850f..dcd937bf8351 100644
--- a/addons/website_forum/models/website.py
+++ b/addons/website_forum/models/website.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
-from odoo import models, fields
+from odoo import models, fields, _
+from odoo.addons.http_routing.models.ir_http import url_for
 
 
 class Website(models.Model):
@@ -13,3 +14,8 @@ class Website(models.Model):
             website.forums_count = Forum.search_count(website.website_domain())
 
     forums_count = fields.Integer(compute=_compute_field_forums_count)
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Forum'), url_for('/forum'), 'website_forum'))
+        return suggested_controllers
diff --git a/addons/website_hr_recruitment/data/config_data.xml b/addons/website_hr_recruitment/data/config_data.xml
index 46ecb179e0b2..5cac1bc871b4 100644
--- a/addons/website_hr_recruitment/data/config_data.xml
+++ b/addons/website_hr_recruitment/data/config_data.xml
@@ -1,12 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <odoo>
     <data noupdate="1">
-        <record id="menu_jobs" model="website.menu">
-            <field name="name">Jobs</field>
-            <field name="url">/jobs</field>
-            <field name="parent_id" ref="website.main_menu"/>
-            <field name="sequence" type="int">50</field>
-        </record>
         <record id="action_open_website" model="ir.actions.act_url">
             <field name="name">Website Recruitment Form</field>
             <field name="target">self</field>
diff --git a/addons/website_hr_recruitment/models/__init__.py b/addons/website_hr_recruitment/models/__init__.py
index 7e9137491c9e..dad12c939ce6 100644
--- a/addons/website_hr_recruitment/models/__init__.py
+++ b/addons/website_hr_recruitment/models/__init__.py
@@ -2,3 +2,4 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 from . import hr_recruitment
+from . import website
diff --git a/addons/website_hr_recruitment/models/website.py b/addons/website_hr_recruitment/models/website.py
new file mode 100644
index 000000000000..5a2cd5d37d54
--- /dev/null
+++ b/addons/website_hr_recruitment/models/website.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, _
+from odoo.addons.http_routing.models.ir_http import url_for
+
+
+class Website(models.Model):
+    _inherit = "website"
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Jobs'), url_for('/jobs'), 'website_hr_recruitment'))
+        return suggested_controllers
diff --git a/addons/website_livechat/controllers/main.py b/addons/website_livechat/controllers/main.py
index 242d78857f97..340604cca80e 100644
--- a/addons/website_livechat/controllers/main.py
+++ b/addons/website_livechat/controllers/main.py
@@ -49,6 +49,7 @@ class WebsiteLivechat(LivechatController):
 
         # the value dict to render the template
         values = {
+            'main_object': channel,
             'channel': channel,
             'ratings': ratings,
             'team': channel.sudo().user_ids,
diff --git a/addons/website_livechat/data/website_livechat_data.xml b/addons/website_livechat/data/website_livechat_data.xml
index 554ff7694dd7..556079a5970a 100644
--- a/addons/website_livechat/data/website_livechat_data.xml
+++ b/addons/website_livechat/data/website_livechat_data.xml
@@ -5,13 +5,6 @@
              <field name="channel_id" ref="im_livechat.im_livechat_channel_data"></field>
          </record>
 
-         <record id="menu_livechat" model="website.menu">
-            <field name="name">Live Support</field>
-            <field name="url">/livechat</field>
-            <field name="parent_id" ref="website.main_menu"/>
-            <field name="sequence">55</field>
-        </record>
-
         <record id="im_livechat.im_livechat_channel_data" model="im_livechat.channel">
             <field name="is_published" eval="True"/>
         </record>
diff --git a/addons/website_livechat/models/website.py b/addons/website_livechat/models/website.py
index 31458961b737..a4bd8b1513ea 100644
--- a/addons/website_livechat/models/website.py
+++ b/addons/website_livechat/models/website.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
-from odoo import api, fields, models
+from odoo import fields, models, _
+from odoo.addons.http_routing.models.ir_http import url_for
 
 
 class Website(models.Model):
@@ -55,3 +56,8 @@ class Website(models.Model):
                     "type": "chat_request"
                 }
         return {}
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Live Support'), url_for('/livechat'), 'website_livechat'))
+        return suggested_controllers
diff --git a/addons/website_livechat/views/website_livechat.xml b/addons/website_livechat/views/website_livechat.xml
index 28860638f79a..23c27eadb104 100644
--- a/addons/website_livechat/views/website_livechat.xml
+++ b/addons/website_livechat/views/website_livechat.xml
@@ -68,12 +68,6 @@
             <t t-call="website.layout">
               <div id="wrap">
                 <div class="container">
-                    <!-- published button -->
-                    <t t-call="website.publish_management">
-                        <t t-set="object" t-value="channel"/>
-                        <t t-set="publish_edit" t-value="True"/>
-                    </t>
-
                     <h1><span>Livechat Channel</span>  <small t-field="channel.name" /></h1>
                     <div t-field="channel.website_description" class="oe_structure mt16" />
                     <div class="row mt32">
@@ -83,7 +77,7 @@
                                     <div class="col-lg-12 mb32">
                                         <h3>Statistics</h3>
                                         <div class="row">
-                                            <div class="col-lg-5">
+                                            <div class="col-lg-4 d-flex justify-content-end flex-column">
                                                 <div class="card bg-success text-white" t-attf-style="height: #{160 + int(percentage['great'])}px;">
                                                     <div class="card-body text-center">
                                                         <img src="/rating/static/src/img/rating_10.png" style="height:40px" alt="Happy face"/>
@@ -98,7 +92,7 @@
                                                     </div>
                                                 </div>
                                             </div>
-                                            <div class="col-lg-4">
+                                            <div class="col-lg-4 d-flex justify-content-end flex-column">
                                                 <div class="card bg-warning text-white" t-attf-style="height: #{160 + int(percentage['okay'])}px;">
                                                     <div class="card-body text-center">
                                                         <img src="/rating/static/src/img/rating_5.png" style="height:40px" alt="Neutral face"/>
@@ -113,7 +107,7 @@
                                                     </div>
                                                 </div>
                                             </div>
-                                            <div class="col-lg-3">
+                                            <div class="col-lg-4 d-flex justify-content-end flex-column">
                                                 <div class="card bg-danger text-white" t-attf-style="height: #{160 + int(percentage['bad'])}px;">
                                                     <div class="card-body text-center">
                                                         <img src="/rating/static/src/img/rating_1.png" style="height:40px" alt="Sad face"/>
@@ -134,7 +128,7 @@
                                 <div class="row">
                                     <div class="col-lg-12 mb32">
                                         <h3>The <t t-esc="len(ratings)"/> last feedbacks</h3>
-                                        <div style="text-align:center">
+                                        <div>
                                             <t t-foreach="ratings" t-as="rating">
                                                 <img t-attf-src='/rating/static/src/img/rating_#{int(rating.rating)}.png' t-att-alt="rating.res_name" width="48px" height="48px"/>
                                                 <t t-if="(rating_index+1) % 10 == 0">
@@ -200,7 +194,7 @@
                 <div id="wrap">
                     <div class="oe_structure" id="oe_structure_website_livechat_channel_list_1"/>
                     <div class="container">
-                        <h1>Livechat Support Channels</h1>
+                        <h1 class="pt-3">Livechat Support Channels</h1>
                         <div class="row mt32 mb32">
                             <t t-if="not len(channels)">
                                 <div class="col-lg-6 offset-lg-3">
@@ -208,13 +202,13 @@
                                 </div>
                             </t>
                             <t t-if="len(channels)">
-                                <div class="col-lg-6 offset-lg-3">
+                                <div class="col-lg-6">
                                     <t t-foreach="channels" t-as="channel">
                                         <div t-attf-class="media#{' mt-3' if channel_index else ''}">
                                             <a t-attf-href="/livechat/channel/#{ slug(channel)}">
                                                 <img t-att-src="channel.image_128 and image_data_uri(channel.image_128) or '/web/static/src/img/placeholder.png'" t-att-alt="channel.name" class="o_image_64_cover"/>
                                             </a>
-                                            <div class="media-body">
+                                            <div class="media-body h-100 my-auto">
                                                 <h4><t t-esc="channel.name"/></h4>
                                             </div>
                                         </div>
diff --git a/addons/website_mail_channel/data/mail_template_data.xml b/addons/website_mail_channel/data/mail_template_data.xml
index 0f3056ea2c48..52859d08aaef 100644
--- a/addons/website_mail_channel/data/mail_template_data.xml
+++ b/addons/website_mail_channel/data/mail_template_data.xml
@@ -175,13 +175,4 @@
         </record>
     </data>
 
-    <data noupdate="1">
-        <record id="menu_mailing_list" model="website.menu">
-            <field name="name">Mailing Lists</field>
-            <field name="url">/groups</field>
-            <field name="parent_id" ref="website.main_menu"/>
-            <field name="sequence">55</field>
-        </record>
-    </data>
-
 </odoo>
diff --git a/addons/website_mail_channel/models/__init__.py b/addons/website_mail_channel/models/__init__.py
index 5280d256a09d..f5c2bbb5e3c3 100644
--- a/addons/website_mail_channel/models/__init__.py
+++ b/addons/website_mail_channel/models/__init__.py
@@ -2,3 +2,4 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 from . import mail_channel
 from . import mail_mail
+from . import website
diff --git a/addons/website_mail_channel/models/website.py b/addons/website_mail_channel/models/website.py
new file mode 100644
index 000000000000..854e99bdbe83
--- /dev/null
+++ b/addons/website_mail_channel/models/website.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, _
+from odoo.addons.http_routing.models.ir_http import url_for
+
+
+class Website(models.Model):
+    _inherit = "website"
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Mailing Lists'), url_for('/groups'), 'website_mail_channel'))
+        return suggested_controllers
diff --git a/addons/website_membership/__manifest__.py b/addons/website_membership/__manifest__.py
index 7246cc2ced27..3b87b94f5b1a 100644
--- a/addons/website_membership/__manifest__.py
+++ b/addons/website_membership/__manifest__.py
@@ -11,7 +11,6 @@ Publish your members/association directory publicly.
     """,
     'depends': ['website_partner', 'website_google_map', 'association', 'website_sale'],
     'data': [
-        'data/membership_data.xml',
         'views/product_template_views.xml',
         'views/website_membership_templates.xml',
         'security/ir.model.access.csv',
diff --git a/addons/website_membership/data/membership_data.xml b/addons/website_membership/data/membership_data.xml
deleted file mode 100644
index b542857e5ece..000000000000
--- a/addons/website_membership/data/membership_data.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<odoo>
-
-    <data noupdate="1">
-        <record id="menu_members" model="website.menu">
-            <field name="name">Members</field>
-            <field name="url">/members</field>
-            <field name="parent_id" ref="website.main_menu"/>
-            <field name="sequence">55</field>
-        </record>
-    </data>
-
-</odoo>
diff --git a/addons/website_membership/models/__init__.py b/addons/website_membership/models/__init__.py
index cda923092907..6627227f59cf 100644
--- a/addons/website_membership/models/__init__.py
+++ b/addons/website_membership/models/__init__.py
@@ -2,3 +2,4 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 from . import membership
+from . import website
diff --git a/addons/website_membership/models/website.py b/addons/website_membership/models/website.py
new file mode 100644
index 000000000000..71540ac32b12
--- /dev/null
+++ b/addons/website_membership/models/website.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, _
+from odoo.addons.http_routing.models.ir_http import url_for
+
+
+class Website(models.Model):
+    _inherit = "website"
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Members'), url_for('/members'), 'website_membership'))
+        return suggested_controllers
diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py
index 9177367a4a27..deb6e7468281 100644
--- a/addons/website_sale/models/website.py
+++ b/addons/website_sale/models/website.py
@@ -3,10 +3,11 @@
 
 import logging
 
-from odoo import api, fields, models, tools, SUPERUSER_ID
+from odoo import api, fields, models, tools, SUPERUSER_ID, _
 
 from odoo.http import request
 from odoo.addons.website.models import ir_http
+from odoo.addons.http_routing.models.ir_http import url_for
 
 _logger = logging.getLogger(__name__)
 
@@ -364,3 +365,8 @@ class Website(models.Model):
         if self.env.user.has_group('sales_team.group_sale_salesman'):
             return self.env.ref('website.backend_dashboard').read()[0]
         return super(Website, self).action_dashboard_redirect()
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('eCommerce'), url_for('/shop'), 'website_sale'))
+        return suggested_controllers
diff --git a/addons/website_slides/models/website.py b/addons/website_slides/models/website.py
index 1896243fa8f6..7b559895b1ce 100644
--- a/addons/website_slides/models/website.py
+++ b/addons/website_slides/models/website.py
@@ -1,10 +1,16 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
-from odoo import api, fields, models
+from odoo import fields, models, _
+from odoo.addons.http_routing.models.ir_http import url_for
 
 
 class Website(models.Model):
     _inherit = "website"
 
     website_slide_google_app_key = fields.Char('Google Doc Key')
+
+    def get_suggested_controllers(self):
+        suggested_controllers = super(Website, self).get_suggested_controllers()
+        suggested_controllers.append((_('Courses'), url_for('/slides'), 'website_slides'))
+        return suggested_controllers
-- 
GitLab