diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py
index ea15b2c22df..bbfe3a134a0 100644
--- a/addons/web/__manifest__.py
+++ b/addons/web/__manifest__.py
@@ -24,6 +24,7 @@ This module provides the core of the Odoo Web Client.
'qweb': [
"static/src/xml/base.xml",
"static/src/xml/kanban.xml",
+ "static/src/xml/menu.xml",
"static/src/xml/rainbow_man.xml",
"static/src/xml/report.xml",
"static/src/xml/web_calendar.xml",
diff --git a/addons/web/static/src/js/chrome/apps_menu.js b/addons/web/static/src/js/chrome/apps_menu.js
new file mode 100644
index 00000000000..bdc330b0e29
--- /dev/null
+++ b/addons/web/static/src/js/chrome/apps_menu.js
@@ -0,0 +1,104 @@
+odoo.define('web.AppsMenu', function (require) {
+"use strict";
+
+var Widget = require('web.Widget');
+
+var AppsMenu = Widget.extend({
+ template: 'AppsMenu',
+ events: {
+ 'click .o_app': '_onAppsMenuItemClicked',
+ },
+ /**
+ * @override
+ * @param {web.Widget} parent
+ * @param {Object} menuData
+ * @param {Object[]} menuData.children
+ */
+ init: function (parent, menuData) {
+ this._super.apply(this, arguments);
+ this._activeApp = undefined;
+ this._apps = _.map(menuData.children, function (appMenuData) {
+ return {
+ actionID: parseInt(appMenuData.action.split(',')[1]),
+ menuID: appMenuData.id,
+ name: appMenuData.name,
+ xmlID: appMenuData.xmlid,
+ };
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {Object}
+ */
+ getActiveApp: function () {
+ return this._activeApp || {};
+ },
+ /**
+ * @returns {Object[]}
+ */
+ getApps: function () {
+ return this._apps;
+ },
+ /**
+ * Open the given app
+ */
+ openApp: function () {},
+ /**
+ * Open the first app in the list of apps
+ */
+ openFirstApp: function () {
+ var firstApp = this._apps[0];
+ this._openApp(firstApp);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Object} app
+ */
+ _openApp: function (app) {
+ this._setActiveApp(app);
+ this.trigger_up('app_clicked', {
+ action_id: app.actionID,
+ menu_id: app.menuID,
+ });
+ },
+ /**
+ * @private
+ * @param {Object} app
+ */
+ _setActiveApp: function (app) {
+ this._activeApp = app;
+ this.renderElement();
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when clicking on an item in the apps menu.
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onAppsMenuItemClicked: function (ev) {
+ var $target = $(ev.currentTarget);
+ var actionID = $target.data('action-id');
+ var menuID = $target.data('menu-id');
+ var app = _.findWhere(this._apps, { actionID: actionID, menuID: menuID });
+ this._openApp(app);
+ },
+
+});
+
+return AppsMenu;
+
+});
diff --git a/addons/web/static/src/js/chrome/menu.js b/addons/web/static/src/js/chrome/menu.js
index c96d4b17f8c..7f60e2ea44f 100644
--- a/addons/web/static/src/js/chrome/menu.js
+++ b/addons/web/static/src/js/chrome/menu.js
@@ -1,164 +1,140 @@
odoo.define('web.Menu', function (require) {
"use strict";
+var AppsMenu = require('web.AppsMenu');
+var config = require('web.config');
var core = require('web.core');
var dom = require('web.dom');
-var session = require('web.session');
+var SystrayMenu = require('web.SystrayMenu');
+var UserMenu = require('web.UserMenu');
var Widget = require('web.Widget');
+UserMenu.prototype.sequence = 0; // force UserMenu to be the right-most item in the systray
+SystrayMenu.Items.push(UserMenu);
+
+var QWeb = core.qweb;
+
var Menu = Widget.extend({
- init: function() {
- this._super.apply(this, arguments);
- this.is_bound = $.Deferred();
- this.data = {data:{children:[]}};
- core.bus.on('change_menu_section', this, this.on_change_top_menu);
+ template: 'Menu',
+ menusTemplate: 'Menu.sections',
+ events: {
+ 'mouseover .o_menu_sections > li:not(.show)': '_onMouseOverMenu',
+ 'click .o_menu_brand': '_onAppNameClicked',
},
- start: function() {
- this._super.apply(this, arguments);
- return this.bind_menu();
- },
- do_reload: function() {
+
+ init: function (parent, menu_data) {
var self = this;
- self.bind_menu();
- },
- bind_menu: function() {
- var self = this;
- this.$secondary_menus = this.$el.parents().find('.o_sub_menu');
- this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
- this.$el.on('click', 'a[data-menu]', function (event) {
- event.preventDefault();
- var menu_id = $(event.currentTarget).data('menu');
- core.bus.trigger('change_menu_section', menu_id);
+ this._super.apply(this, arguments);
+
+ this.$menu_sections = {};
+ this.menu_data = menu_data;
+
+ // Prepare navbar's menus
+ var $menu_sections = $(QWeb.render(this.menusTemplate, {
+ menu_data: this.menu_data,
+ }));
+ $menu_sections.filter('section').each(function () {
+ self.$menu_sections[parseInt(this.className, 10)] = $(this).children('li');
});
- // Hide second level submenus
- this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').addClass('o_hidden');
- if (self.current_menu) {
- self.open_menu(self.current_menu);
- }
- this.trigger('menu_bound');
-
- dom.initAutoMoreMenu(this.$el);
-
- this.is_bound.resolve();
+ // Bus event
+ core.bus.on('change_menu_section', this, this._onChangeMenuSection);
},
- /**
- * Opens a given menu by id, as if a user had browsed to that menu by hand
- * except does not trigger any event on the way
- *
- * @param {Number} id database id of the terminal menu to select
- */
- open_menu: function (id) {
- this.current_menu = id;
- session.active_id = id;
- var $clicked_menu, $sub_menu, $main_menu;
- $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
- this.trigger('open_menu', id, $clicked_menu);
-
- if (this.$secondary_menus.has($clicked_menu).length) {
- $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
- $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
- } else {
- $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
- $main_menu = $clicked_menu;
- }
-
- this.current_primary_menu = $main_menu.data('menu');
-
- // Activate current main menu
- this.$el.find('.active').removeClass('active');
- $main_menu.parent().addClass('active');
-
- // Show current sub menu
- this.$secondary_menus.find('.oe_secondary_menu').hide();
- $sub_menu.show();
-
- // Hide/Show the leftbar menu depending of the presence of sub-items
- this.$secondary_menus.toggleClass('o_hidden', !$sub_menu.children().length);
-
- // Activate current menu item and show parents
- this.$secondary_menus.find('.active').removeClass('active');
- if ($main_menu !== $clicked_menu) {
- $clicked_menu.parents().removeClass('o_hidden');
- if ($clicked_menu.is('.oe_menu_toggler')) {
- $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggleClass('o_hidden');
- } else {
- $clicked_menu.parent().addClass('active');
- }
- }
- // add a tooltip to cropped menu items
- this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
- $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} : 'dispose');
- });
- },
- /**
- * Call open_menu on a menu_item that matches the action_id
- *
- * If `menuID` is a match on this action, open this menu_item.
- * Otherwise open the first menu_item that matches the action_id.
- *
- * @param {Number} id the action_id to match
- * @param {Number} [menuID] a menu ID that may match with provided action
- */
- open_action: function (id, menuID) {
- var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
- if (!(menuID && $menu.filter("[data-menu='" + menuID + "']").length)) {
- // menuID doesn't match action, so pick first menu_item
- menuID = $menu.data('menu');
- }
- if (menuID) {
- this.open_menu(menuID);
- }
- },
- /**
- * Process a click on a menu item
- *
- * @param {Number} id the menu_id
- */
- menu_click: function(id) {
- if (!id) { return; }
-
- // find back the menuitem in dom to get the action
- var $item = this.$el.find('a[data-menu=' + id + ']');
- if (!$item.length) {
- $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
- }
- var action_id = $item.data('action-id');
- // If first level menu doesnt have action trigger first leaf
- if (!action_id) {
- if(this.$el.has($item).length) {
- var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
- var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
- if($items.length) {
- action_id = $items.data('action-id');
- id = $items.data('menu');
- }
- }
- }
- if (action_id) {
- this.trigger('menu_click', {
- action_id: action_id,
- id: id,
- previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
- }, $item);
- } else {
- console.log('Menu no action found web test 04 will fail');
- }
- this.open_menu(id);
- },
-
- /**
- * Change the current top menu
- *
- * @param {int} [menu_id] the top menu id
- */
- on_change_top_menu: function(menu_id) {
+ start: function () {
var self = this;
- this.menu_click(menu_id);
+
+ this.$menu_apps = this.$('.o_menu_apps');
+ this.$menu_brand_placeholder = this.$('.o_menu_brand');
+ this.$section_placeholder = this.$('.o_menu_sections');
+
+ // Navbar's menus event handlers
+ var on_secondary_menu_click = function (ev) {
+ ev.preventDefault();
+ var menu_id = $(ev.currentTarget).data('menu');
+ var action_id = $(ev.currentTarget).data('action-id');
+ self._on_secondary_menu_click(menu_id, action_id);
+ };
+ var menu_ids = _.keys(this.$menu_sections);
+ var primary_menu_id, $section;
+ for (var i = 0; i < menu_ids.length; i++) {
+ primary_menu_id = menu_ids[i];
+ $section = this.$menu_sections[primary_menu_id];
+ $section.on('click', 'a[data-menu]', self, on_secondary_menu_click.bind(this));
+ }
+
+ // Apps Menu
+ this._appsMenu = new AppsMenu(self, this.menu_data);
+ this._appsMenu.appendTo(this.$menu_apps);
+
+ // Systray Menu
+ this.systray_menu = new SystrayMenu(this);
+ this.systray_menu.attachTo(this.$('.o_menu_systray'));
+
+ dom.initAutoMoreMenu(this.$section_placeholder, {
+ maxWidth: function () {
+ return self.$el.width() - (self.$menu_apps.outerWidth(true) + self.$menu_brand_placeholder.outerWidth(true) + self.systray_menu.$el.outerWidth(true));
+ },
+ sizeClass: 'SM',
+ });
+
+ return this._super.apply(this, arguments);
},
- on_menu_click: function(ev) {
- ev.preventDefault();
- this.menu_click($(ev.currentTarget).data('menu'));
+ _trigger_menu_clicked: function (menu_id, action_id) {
+ this.trigger_up('menu_clicked', {
+ id: menu_id,
+ action_id: action_id,
+ previous_menu_id: this.current_secondary_menu || this.current_primary_menu,
+ });
+ },
+ _on_secondary_menu_click: function (menu_id, action_id) {
+ var self = this;
+
+ // It is still possible that we don't have an action_id (for example, menu toggler)
+ if (action_id) {
+ self._trigger_menu_clicked(menu_id, action_id);
+ this.current_secondary_menu = menu_id;
+ }
+ },
+ /**
+ * Helpers used by web_client in order to restore the state from
+ * an url (by restore, read re-synchronize menu and action manager)
+ */
+ action_id_to_primary_menu_id: function (action_id) {
+ var primary_menu_id, found;
+ for (var i = 0; i < this.menu_data.children.length && !primary_menu_id; i++) {
+ found = this._action_id_in_subtree(this.menu_data.children[i], action_id);
+ if (found) {
+ primary_menu_id = this.menu_data.children[i].id;
+ }
+ }
+ return primary_menu_id;
+ },
+ _action_id_in_subtree: function (root, action_id) {
+ // action_id can be a string or an integer
+ if (root.action && root.action.split(',')[1] === String(action_id)) {
+ return true;
+ }
+ var found;
+ for (var i = 0; i < root.children.length && !found; i++) {
+ found = this._action_id_in_subtree(root.children[i], action_id);
+ }
+ return found;
+ },
+ menu_id_to_action_id: function (menu_id, root) {
+ if (!root) {
+ root = $.extend(true, {}, this.menu_data);
+ }
+
+ if (root.id === menu_id) {
+ return root.action.split(',')[1] ;
+ }
+ for (var i = 0; i < root.children.length; i++) {
+ var action_id = this.menu_id_to_action_id(menu_id, root.children[i]);
+ if (action_id !== undefined) {
+ return action_id;
+ }
+ }
+ return undefined;
},
//--------------------------------------------------------------------------
@@ -173,7 +149,73 @@ var Menu = Widget.extend({
getCurrentPrimaryMenu: function () {
return this.current_primary_menu;
},
+ /**
+ * Open the first app
+ */
+ openFirstApp: function () {
+ this._appsMenu.openFirstApp();
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * When clicking on app name, opens the first action of the app
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onAppNameClicked: function (ev) {
+ var actionID = parseInt(this.menu_id_to_action_id(this.current_primary_menu));
+ this._trigger_menu_clicked(this.current_primary_menu, actionID);
+ },
+ /**
+ * @private
+ * @param {integer} primaryMenuID
+ */
+ _onChangeMenuSection: function (primaryMenuID) {
+ if (!this.$menu_sections[primaryMenuID]) {
+ return; // unknown menu_id
+ }
+
+ if (this.current_primary_menu === primaryMenuID) {
+ return; // already in that menu
+ }
+
+ if (this.current_primary_menu) {
+ this.$menu_sections[this.current_primary_menu].detach();
+ }
+
+ // Get back the application name
+ for (var i = 0; i < this.menu_data.children.length; i++) {
+ if (this.menu_data.children[i].id === primaryMenuID) {
+ this.$menu_brand_placeholder.text(this.menu_data.children[i].name);
+ break;
+ }
+ }
+
+ this.$menu_sections[primaryMenuID].appendTo(this.$section_placeholder);
+ this.current_primary_menu = primaryMenuID;
+
+ core.bus.trigger('resize');
+ },
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onMouseOverMenu: function (ev) {
+ if (config.device.isMobile) {
+ return;
+ }
+ var $target = $(ev.currentTarget);
+ var $opened = $target.siblings('.show');
+ if ($opened.length) {
+ $target.find('[data-toggle="dropdown"]').dropdown('toggle');
+ }
+ },
});
return Menu;
+
});
diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js
index ca777d470ae..8b725423d0f 100644
--- a/addons/web/static/src/js/chrome/web_client.js
+++ b/addons/web/static/src/js/chrome/web_client.js
@@ -6,149 +6,164 @@ var config = require('web.config');
var core = require('web.core');
var data_manager = require('web.data_manager');
var dom = require('web.dom');
-var framework = require('web.framework');
var Menu = require('web.Menu');
var session = require('web.session');
-var SystrayMenu = require('web.SystrayMenu');
-var UserMenu = require('web.UserMenu');
return AbstractWebClient.extend({
- events: _.extend({}, AbstractWebClient.prototype.events, {
- 'click .oe_logo_edit_admin': 'logo_edit',
- 'click .oe_logo img': function(ev) {
- ev.preventDefault();
- return this.clear_uncommitted_changes().then(function() {
- framework.redirect("/web" + (config.debug ? "?debug" : ""));
- });
- },
+ custom_events: _.extend({}, AbstractWebClient.prototype.custom_events, {
+ app_clicked: 'on_app_clicked',
+ menu_clicked: 'on_menu_clicked',
}),
- show_application: function() {
- var self = this;
-
- this.toggle_bars(true);
- this.set_title();
- this.update_logo();
-
- // Menu is rendered server-side thus we don't want the widget to create any dom
- this.menu = new Menu(this);
- this.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
- this.menu.on('menu_click', this, this.on_menu_action);
-
- // Create the user menu (rendered client-side)
- this.user_menu = new UserMenu(this);
- var $user_menu_placeholder = $('body').find('.oe_user_menu_placeholder').show();
- var user_menu_loaded = this.user_menu.appendTo($user_menu_placeholder);
-
- // Create the systray menu (rendered server-side)
- this.systray_menu = new SystrayMenu(this);
- this.systray_menu.setElement(this.$el.parents().find('.oe_systray'));
- var systray_menu_loaded = this.systray_menu.start();
-
- // Start the menu once both systray and user menus are rendered
- // to prevent overflows while loading
- return $.when(systray_menu_loaded, user_menu_loaded).then(function() {
- self.bind_hashchange();
- self.menu.$el.removeClass('d-none');
- self.menu.start();
+ start: function () {
+ core.bus.on('change_menu_section', this, function (menuID) {
+ this.do_push_state(_.extend($.bbq.getState(), {
+ menu_id: menuID,
+ }));
});
+ return this._super.apply(this, arguments);
},
- toggle_bars: function(value) {
- this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
- },
- update_logo: function(reload) {
- var company = session.company_id;
- var img = session.url('/web/binary/company_logo' + '?db=' + session.db + (company ? '&company=' + company : ''));
- this.$('.o_sub_menu_logo img').attr('src', '').attr('src', img + (reload ? "&t=" + Date.now() : ''));
- this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', session.is_superuser);
- },
- logo_edit: function(ev) {
+ bind_events: function () {
var self = this;
- ev.preventDefault();
- this._rpc({
- model: 'res.users',
- method: 'read',
- args: [[session.uid], ['company_id']],
+ this._super.apply(this, arguments);
+
+ /*
+ Small patch to allow having a link with a href towards an anchor. Since odoo use hashtag
+ to represent the current state of the view, we can't easily distinguish between a link
+ towards an anchor and a link towards anoter view/state. If we want to navigate towards an
+ anchor, we must not change the hash of the url otherwise we will be redirected to the app
+ switcher instead.
+ To check if we have an anchor, first check if we have an href attributes starting with #.
+ Try to find a element in the DOM using JQuery selector.
+ If we have a match, it means that it is probably a link to an anchor, so we jump to that anchor.
+ */
+ this.$el.on('click', 'a', function (ev) {
+ var disable_anchor = ev.target.attributes.disable_anchor;
+ if (disable_anchor && disable_anchor.value === "true") {
+ return;
+ }
+
+ var href = ev.target.attributes.href;
+ if (href) {
+ if (href.value[0] === '#' && href.value.length > 1) {
+ if (self.$("[id='"+href.value.substr(1)+"']").length) {
+ ev.preventDefault();
+ self.trigger_up('scrollTo', {'selector': href.value});
+ }
+ }
+ }
+ });
+ },
+ load_menus: function () {
+ return this._rpc({
+ model: 'ir.ui.menu',
+ method: 'load_menus',
+ args: [config.debug],
+ context: session.user_context,
})
- .then(function(data) {
- self._rpc({
- route: '/web/action/load',
- params: { action_id: 'base.action_res_company_form' },
- })
- .done(function(result) {
- result.res_id = data[0].company_id[0];
- result.target = "new";
- result.views = [[false, 'form']];
- result.flags = {
- action_buttons: true,
- headless: true,
- };
- self.action_manager.doAction(result, {
- on_close: self.update_logo.bind(self, true),
- });
- });
+ .then(function (menuData) {
+ // Compute action_id if not defined on a top menu item
+ for (var i = 0; i < menuData.children.length; i++) {
+ var child = menuData.children[i];
+ if (child.action === false) {
+ while (child.children && child.children.length) {
+ child = child.children[0];
+ if (child.action) {
+ menuData.children[i].action = child.action;
+ break;
+ }
+ }
+ }
+ }
+ return menuData;
});
- return false;
},
- bind_hashchange: function() {
+ show_application: function () {
var self = this;
- $(window).bind('hashchange', this.on_hashchange);
- var didHashChanged = false;
- $(window).one('hashchange', function () {
- didHashChanged = true;
- });
+ this.set_title();
- var state = $.bbq.getState(true);
- if (_.isEmpty(state) || state.action === "login") {
- self.menu.is_bound.done(function() {
- self._rpc({
+ return this.instanciate_menu_widgets().then(function () {
+ $(window).bind('hashchange', self.on_hashchange);
+
+ // If the url's state is empty, we execute the user's home action if there is one (we
+ // show the first app if not)
+ if (_.isEmpty($.bbq.getState(true))) {
+ return self._rpc({
model: 'res.users',
method: 'read',
- args: [[session.uid], ['action_id']],
+ args: [session.uid, ["action_id"]],
})
- .done(function(result) {
- var amDef = self.action_manager.dp.current_def;
- if (didHashChanged || (amDef && amDef.state() === 'pending')) {
- return;
- }
+ .then(function (result) {
var data = result[0];
- if(data.action_id) {
- self.action_manager.doAction(data.action_id[0]);
- self.menu.open_action(data.action_id[0]);
+ if (data.action_id) {
+ return self.do_action(data.action_id[0]).then(function () {
+ self.menu.change_menu_section(self.menu.action_id_to_primary_menu_id(data.action_id[0]));
+ });
} else {
- var first_menu_id = self.menu.$el.find("a:first").data("menu");
- if(first_menu_id) {
- self.menu.menu_click(first_menu_id);
+ self.menu.openFirstApp();
+ }
+ });
+ } else {
+ return self.on_hashchange();
+ }
+ });
+ },
+
+ instanciate_menu_widgets: function () {
+ var self = this;
+ var defs = [];
+ return this.load_menus().then(function (menuData) {
+ self.menu_data = menuData;
+
+ // Here, we instanciate every menu widgets and we immediately append them into dummy
+ // document fragments, so that their `start` method are executed before inserting them
+ // into the DOM.
+ if (self.menu) {
+ self.menu.destroy();
+ }
+ self.menu = new Menu(self, menuData);
+ defs.push(self.menu.prependTo(self.$el));
+ return $.when.apply($, defs);
+ });
+ },
+
+ // --------------------------------------------------------------
+ // URL state handling
+ // --------------------------------------------------------------
+ on_hashchange: function (event) {
+ if (this._ignore_hashchange) {
+ this._ignore_hashchange = false;
+ return $.when();
+ }
+
+ var self = this;
+ return this.clear_uncommitted_changes().then(function () {
+ var stringstate = $.bbq.getState(false);
+ if (!_.isEqual(self._current_state, stringstate)) {
+ var state = $.bbq.getState(true);
+ if (state.action || (state.model && (state.view_type || state.id))) {
+ return self.action_manager.loadState(state, !!self._current_state).then(function () {
+ if (state.menu_id) {
+ if (state.menu_id !== self.menu.current_primary_menu) {
+ core.bus.trigger('change_menu_section', state.menu_id);
+ }
+ } else {
+ var action = self.action_manager.getCurrentAction();
+ if (action) {
+ var menu_id = self.menu.action_id_to_primary_menu_id(action.id);
+ if (menu_id) {
+ core.bus.trigger('change_menu_section', menu_id);
+ }
}
}
});
- });
- } else {
- $(window).trigger('hashchange');
- }
- },
- on_hashchange: function(event) {
- if (this._ignore_hashchange) {
- this._ignore_hashchange = false;
- return;
- }
-
- var self = this;
- this.clear_uncommitted_changes().then(function () {
- var stringstate = event.getState(false);
- if (!_.isEqual(self._current_state, stringstate)) {
- var state = event.getState(true);
- if(!state.action && state.menu_id) {
- self.menu.is_bound.done(function() {
- self.menu.menu_click(state.menu_id);
+ } else if (state.menu_id) {
+ var action_id = self.menu.menu_id_to_action_id(state.menu_id);
+ return self.do_action(action_id, {clear_breadcrumbs: true}).then(function () {
+ core.bus.trigger('change_menu_section', state.menu_id);
});
} else {
- self.action_manager.loadState(state, !!self._current_state).then(function () {
- var action = self.action_manager.getCurrentAction();
- if (action) {
- self.menu.open_action(action.id, state.menu_id);
- }
- });
+ self.menu.openFirstApp();
}
}
self._current_state = stringstate;
@@ -159,17 +174,88 @@ return AbstractWebClient.extend({
}
});
},
- on_menu_action: function(options) {
- this.action_manager.doAction(options.action_id, {
- clear_breadcrumbs: true,
- action_menu_id: options.id,
- });
+
+ // --------------------------------------------------------------
+ // Menu handling
+ // --------------------------------------------------------------
+ on_app_clicked: function (ev) {
+ var self = this;
+ return this.menu_dm.add(data_manager.load_action(ev.data.action_id))
+ .then(function (result) {
+ return self.action_mutex.exec(function () {
+ var completed = $.Deferred();
+ var options = _.extend({}, ev.data.options, {
+ clear_breadcrumbs: true,
+ action_menu_id: ev.data.menu_id,
+ });
+ $.when(self._openMenu(result, options)).fail(function () {
+ completed.resolve();
+ }).done(function () {
+ self._on_app_clicked_done(ev)
+ .then(completed.resolve.bind(completed))
+ .fail(completed.reject.bind(completed));
+ });
+ setTimeout(function () {
+ completed.resolve();
+ }, 2000);
+ return completed;
+ });
+ });
},
- toggle_fullscreen: function(fullscreen) {
- this._super(fullscreen);
- if (!fullscreen) {
- core.bus.trigger('resize');
- }
+ _on_app_clicked_done: function (ev) {
+ core.bus.trigger('change_menu_section', ev.data.menu_id);
+ return $.Deferred().resolve();
+ },
+ on_menu_clicked: function (ev) {
+ var self = this;
+ return this.menu_dm.add(data_manager.load_action(ev.data.action_id))
+ .then(function (result) {
+ return self.action_mutex.exec(function () {
+ var completed = $.Deferred();
+ $.when(self._openMenu(result, {
+ clear_breadcrumbs: true,
+ })).always(function () {
+ completed.resolve();
+ });
+
+ setTimeout(function () {
+ completed.resolve();
+ }, 2000);
+
+ return completed;
+ });
+ }).always(function () {
+ self.$el.removeClass('o_mobile_menu_opened');
+ });
+ },
+ /**
+ * Open the action linked to a menu.
+ * This function is mostly used to allow override in other modules.
+ *
+ * @private
+ * @param {Object} action
+ * @param {Object} options
+ * @returns {Deferred}
+ */
+ _openMenu: function (action, options) {
+ return this.do_action(action, options);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the left and top scroll positions of the main scrolling area
+ * (i.e. the action manager in desktop).
+ *
+ * @returns {Object} with keys left and top
+ */
+ getScrollPosition: function () {
+ return {
+ left: this.action_manager.el.scrollLeft,
+ top: this.action_manager.el.scrollTop,
+ };
},
//--------------------------------------------------------------------------
@@ -178,29 +264,22 @@ return AbstractWebClient.extend({
/**
* @override
+ * @private
*/
_onGetScrollPosition: function (ev) {
- ev.data.callback({
- left: this.action_manager.el.scrollLeft,
- top: this.action_manager.el.scrollTop,
- });
+ ev.data.callback(this.getScrollPosition());
},
/**
* @override
+ * @private
*/
_onScrollTo: function (ev) {
- var offset;
- if (ev.data.selector) {
+ var offset = {top: ev.data.top, left: ev.data.left || 0};
+ if (!offset.top) {
offset = dom.getPosition(document.querySelector(ev.data.selector));
- // substract the position of the ActionManager as it is the
- // scrolling element
- var actionManagerOffset = dom.getPosition(this.action_manager.el);
- offset.left -= actionManagerOffset.left;
- offset.top -= actionManagerOffset.top;
- } else {
- offset = {top: ev.data.top || 0, left: ev.data.left || 0};
+ // Substract the position of the action_manager as it is the scrolling part
+ offset.top -= dom.getPosition(this.action_manager.el).top;
}
-
this.action_manager.el.scrollTop = offset.top;
this.action_manager.el.scrollLeft = offset.left;
},
diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js
index dcdfc1a73bc..6f6022b021f 100644
--- a/addons/web/static/src/js/tools/debug_manager.js
+++ b/addons/web/static/src/js/tools/debug_manager.js
@@ -822,7 +822,7 @@ if (config.debug) {
*/
current_action_updated: function (action, controller) {
this._super.apply(this, arguments);
- var debugManager = _.find(this.systray_menu.widgets, function(item) {
+ var debugManager = _.find(this.menu.systray_menu.widgets, function(item) {
return item instanceof DebugManager;
});
debugManager.update('action', action, controller && controller.widget);
diff --git a/addons/web/static/src/js/tools/test_menus.js b/addons/web/static/src/js/tools/test_menus.js
index 6a8c9d2118b..acfcde87878 100644
--- a/addons/web/static/src/js/tools/test_menus.js
+++ b/addons/web/static/src/js/tools/test_menus.js
@@ -61,7 +61,7 @@
$listOfAppMenuItems = $(".o_app, .o_menuitem")
} else {
console.log("Odoo flavor: Community")
- $listOfAppMenuItems = $('#oe_main_menu_navbar a.oe_menu_leaf, #oe_main_menu_navbar a.oe_menu_toggler');
+ $listOfAppMenuItems = $('#oe_main_menu_navbar a.oe_menu_leaf, #oe_main_menu_navbar a.o_app');
}
console.log('Found ', $listOfAppMenuItems.length, 'apps to test');
@@ -94,12 +94,7 @@
testedApps.push(element.dataset.menuXmlid);
return testMenuItem(element).then(function () {
var $subMenuItems;
- var isEnterprise = odoo.session_info.server_version_info[5] === 'e';
- if (isEnterprise) {
- $subMenuItems = $('.o_menu_entry_lvl_1, .o_menu_entry_lvl_2, .o_menu_entry_lvl_3, .o_menu_entry_lvl_4');
- } else {
- $subMenuItems = $('div.o_sub_menu_content .oe_menu_leaf');
- }
+ $subMenuItems = $('.o_menu_entry_lvl_1, .o_menu_entry_lvl_2, .o_menu_entry_lvl_3, .o_menu_entry_lvl_4');
var testMenuDef = $.when();
testMenuDef = chainDeferred($subMenuItems, testMenuDef, testMenuItem);
return testMenuDef;
diff --git a/addons/web/static/src/scss/navbar.scss b/addons/web/static/src/scss/navbar.scss
index 2d865dd70d9..160a1b87558 100644
--- a/addons/web/static/src/scss/navbar.scss
+++ b/addons/web/static/src/scss/navbar.scss
@@ -28,6 +28,10 @@
}
}
+ .o_app {
+ cursor: pointer;
+ }
+
> .o_menu_brand {
display: block;
float: left;
@@ -37,13 +41,7 @@
font-size: 22px;
font-weight: 500;
line-height: $o-navbar-height;
-
- @include media-breakpoint-down(sm) {
- float: none;
- margin: 0;
- border-bottom: 1px solid $o-navbar-inverse-link-hover-bg;
- color: transparent;
- }
+ cursor: pointer;
}
> .o_menu_toggle {
@@ -146,79 +144,4 @@
transform: translateY(-2px);
}
}
-
- @include media-breakpoint-down(sm) {
- transition: height 200ms linear 0s;
-
- position: relative;
- height: $o-navbar-height;
- overflow: hidden;
-
- > ul {
- > li {
- float: none;
- .dropdown-backdrop {
- display: none;
- }
-
- .dropdown-menu.show {
- max-height: none;
- }
- }
-
- &.o_menu_sections {
- width: 100%;
-
- .dropdown-menu.show {
- position: static;
- float: none;
- background-color: transparent;
- box-shadow: none;
- border: none;
- overflow: visible;
-
- > .dropdown-item {
- background-color: transparent;
- color: inherit;
- }
- }
- }
-
- &.o_menu_systray {
- @include o-position-absolute(0px, $o-navbar-height, auto, $o-navbar-height);
- height: $o-navbar-height;
- text-align: right;
-
- > li {
- display: inline-block;
-
- .dropdown-menu.show {
- @include o-position-absolute($o-navbar-height, 0, 0, 0);
- position: fixed;
- width: auto;
- }
- }
-
- .o_user_menu .oe_topbar_name {
- display: none;
- }
- }
- }
- }
-}
-
-@include media-breakpoint-down(sm) {
- body.o_mobile_menu_opened > .o_main_navbar {
- height: 100%;
- overflow: auto;
- }
-}
-
-@include media-breakpoint-down(sm) {
- .o_switch_company_menu > .dropdown-menu {
- padding-top: 0px;
- .bg-info {
- padding: 10px;
- }
- }
}
diff --git a/addons/web/static/src/scss/navbar_mobile.scss b/addons/web/static/src/scss/navbar_mobile.scss
new file mode 100644
index 00000000000..6ef917b8517
--- /dev/null
+++ b/addons/web/static/src/scss/navbar_mobile.scss
@@ -0,0 +1,95 @@
+
+.o_main_navbar {
+ .o_app {
+ @include media-breakpoint-down(sm) {
+ float: none;
+ margin: 0;
+ border-bottom: 1px solid $o-navbar-inverse-link-hover-bg;
+ color: transparent !important;
+ }
+ }
+
+ > .o_menu_brand {
+ @include media-breakpoint-down(sm) {
+ float: none;
+ margin: 0;
+ border-bottom: 1px solid $o-navbar-inverse-link-hover-bg;
+ color: transparent !important;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ transition: height 200ms linear 0s;
+
+ position: relative;
+ height: $o-navbar-height;
+ overflow: hidden;
+
+ > ul {
+ > li {
+ float: none;
+ .dropdown-backdrop {
+ display: none;
+ }
+
+ .dropdown-menu.show {
+ max-height: none;
+ }
+ }
+
+ &.o_menu_sections {
+ width: 100%;
+
+ .dropdown-menu.show {
+ position: static;
+ float: none;
+ background-color: transparent;
+ box-shadow: none;
+ border: none;
+ overflow: visible;
+
+ > .dropdown-item {
+ background-color: transparent;
+ color: inherit;
+ }
+ }
+ }
+
+ &.o_menu_systray {
+ @include o-position-absolute(0px, $o-navbar-height, auto, $o-navbar-height);
+ height: $o-navbar-height;
+ text-align: right;
+
+ > li {
+ display: inline-block;
+
+ .dropdown-menu.show {
+ @include o-position-absolute($o-navbar-height, 0, 0, 0);
+ position: fixed;
+ width: auto;
+ }
+ }
+
+ .o_user_menu .oe_topbar_name {
+ display: none;
+ }
+ }
+ }
+ }
+}
+
+@include media-breakpoint-down(sm) {
+ body.o_mobile_menu_opened > .o_main_navbar {
+ height: 100%;
+ overflow: auto;
+ }
+}
+
+@include media-breakpoint-down(sm) {
+ .o_switch_company_menu > .dropdown-menu {
+ padding-top: 0px;
+ .bg-info {
+ padding: 10px;
+ }
+ }
+}
diff --git a/addons/web/static/src/scss/primary_variables.scss b/addons/web/static/src/scss/primary_variables.scss
index 7601c2225a7..e3b520e47f1 100644
--- a/addons/web/static/src/scss/primary_variables.scss
+++ b/addons/web/static/src/scss/primary_variables.scss
@@ -74,7 +74,7 @@ $o-avatar-size: 90px;
$o-statusbar-height: 33px;
$o-label-font-size-factor: 0.8;
-$o-navbar-height: 34px;
+$o-navbar-height: 46px;
$o-nb-calendar-colors: 24;
diff --git a/addons/web/static/src/scss/webclient_extra.scss b/addons/web/static/src/scss/webclient_extra.scss
index a79a9d34a2a..8e8e3630e70 100644
--- a/addons/web/static/src/scss/webclient_extra.scss
+++ b/addons/web/static/src/scss/webclient_extra.scss
@@ -24,17 +24,11 @@ select {
//------------------------------------------------------------------------------
// Loading
//------------------------------------------------------------------------------
+
.o_loading {
- position: fixed;
- top: 0;
- right: 50%;
- padding: 4px 12px;
- background-color: $o-enterprise-color; // FIXME probably supposed to be primary to be auto-adapted in enterprise ?
+ background-color: $o-brand-odoo;
color: white;
- text-align: center;
- border-top: none;
- border-bottom-right-radius: 8px;
- border-bottom-left-radius: 8px;
+ padding: 4px;
}
//------------------------------------------------------------------------------
diff --git a/addons/web/static/src/scss/webclient_layout.scss b/addons/web/static/src/scss/webclient_layout.scss
index 5fe5ab08c8b..c03e519021b 100644
--- a/addons/web/static/src/scss/webclient_layout.scss
+++ b/addons/web/static/src/scss/webclient_layout.scss
@@ -1,176 +1,90 @@
+// ------------------------------------------------------------------
+// Base layout rules, use the 'webclient.scss' file for styling
+// ------------------------------------------------------------------
+html {
+ height: 100%;
-.o_web_client {
- display: flex;
- flex-flow: column nowrap;
- overflow: hidden;
-
- &.o_fullscreen {
- #oe_main_menu_navbar {
- display: none;
- }
- .o_main .o_sub_menu {
- display: none;
- }
- }
-
-
- .navbar {
- flex: 0 0 auto;
-
- li > a {
- padding: 4px 8px;
- margin-top: 2px;
- font-size: 13px;
- }
-
- .oe_topbar_avatar {
- max-height: 18px;
- width: 18px;
- margin-top: -4px;
- }
-
- .oe_systray, .oe_user_menu_placeholder {
- > li {
- position: relative;
- padding: 4px 0px;
-
- > a {
- @include o-hover-text-color($navbar-dark-color, $navbar-dark-active-color);
- }
- }
- }
- }
-
- > .o_main {
- flex: 1 1 auto;
- display: flex;
- min-height: 0; // Allow flex to shrink this element as much as it wants
+ .o_web_client {
height: 100%;
+ display: flex;
+ flex-flow: column nowrap;
- .o_sub_menu {
- flex: 0 0 220px;
- display: flex;
- flex-flow: column nowrap;
- border-right: 1px solid darken($o-control-panel-background-color, 20%);
- background-color: $o-control-panel-background-color;
-
- .o_sub_menu_logo {
- flex: 0 0 auto;
- text-align: center;
- position: relative;
-
- > img {
- margin: 14px 0;
- }
-
- .oe_logo_edit {
- position: absolute;
- top: 1px;
- width: 100%;
- padding: 4px;
- margin: 14px 0;
- display: none;
- color: #eee;
- background: rgba(37,37,37,0.9);
- }
- &:hover .oe_logo_edit_admin {
- display: block;
- }
- }
-
- .o_sub_menu_content {
- flex: 1 1 auto;
- height:100%;
- overflow-y: auto;
-
- .oe_secondary_menu_section {
- font-weight: bold;
- margin-left: 8px;
- color: $o-brand-primary;
- }
- .oe_secondary_submenu {
- margin: 3px 0px 10px;
- line-height: 1.1em;
- >li:not(.active) > a {
- color: $o-main-text-color;
- }
- > li > a {
- padding: 4px 4px 4px 18px;
- > .badge {
- margin-top: -1px;
- }
- }
- .oe_secondary_submenu {
- > li > a {
- padding-left: 30px;
- }
- .oe_secondary_submenu > li > a {
- padding-left: 42px;
- }
- }
- .oe_menu_text {
- @include o-text-overflow;
- max-width: 85%;
- margin-top: 1px;
- }
- .oe_menu_toggler:before {
- display: inline-block;
- width: 0;
- height: 0;
- content: "&darr";
- opacity: 0.5;
- text-indent: -99999px;
- vertical-align: top;
- margin-left: -12px;
- margin-top: 4px;
- margin-right: 4px;
- border-top: 4px solid transparent;
- border-bottom: 4px solid transparent;
- border-left: 4px solid #4c4c4c;
- }
- .oe_menu_opened:before {
- margin-top: 6px;
- margin-left: -16px;
- margin-right: 4px;
- border-left: 4px solid transparent;
- border-right: 4px solid transparent;
- border-top: 4px solid #4c4c4c;
- }
- }
- }
-
- .o_sub_menu_footer {
- flex: 0 0 auto;
- text-align: center;
-
- > a {
- font-weight: bold;
- > span {
- color: $o-enterprise-color; // FIXME probably supposed to be primary to be auto-adapted in enterprise ?
- }
- }
- }
- }
-
- .o_main_content {
+ > .o_main {
flex: 1 1 auto;
display: flex;
- flex-flow: column nowrap;
+ min-height: 0; // Allow flex to shrink this element as much as it wants
+ height: 100%;
- .o_control_panel {
- flex: 0 0 auto;
- }
- .o_content {
+ .o_main_content {
flex: 1 1 auto;
- height: 100%;
- overflow: auto;
- position: relative;
+ display: flex;
+ flex-flow: column nowrap;
- > .o_view_controller {
- @include o-position-absolute(0, 0, 0, 0); // Get the 100% height of its flex parent
+ .o_control_panel {
+ flex: 0 0 auto;
+ }
+ .o_content {
+ flex: 1 1 auto;
height: 100%;
+ overflow: auto;
+ position: relative;
+
+ > .o_view_controller {
+ @include o-position-absolute(0, 0, 0, 0); // Get the 100% height of its flex parent
+ height: 100%;
+ }
}
}
}
+
+ &.o_fullscreen {
+ .o_main_navbar {
+ display: none;
+ }
+ }
+
+ .o_main_navbar {
+ flex: 0 0 auto;
+ }
+
+ .o_control_panel {
+ flex: 0 0 auto;
+ }
+
+ .o_content {
+ direction: ltr; //Define direction attribute here so when rtlcss preprocessor run, it converts it to rtl
+ flex: 1 1 auto;
+ position: relative; // Allow to redistribute the 100% height to its child
+
+ > .o_view_controller {
+ position: absolute; // Get the 100% height of its flex parent
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 100%;
+ direction: ltr;
+ }
+ }
+ }
+
+ .o_loading {
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ z-index: $zindex-modal + 1;
+ }
+}
+
+@media print {
+ html .o_web_client {
+ .o_main_navbar {
+ display: none;
+ }
+ .o_content {
+ position: static;
+ overflow: visible;
+ height: auto;
+ }
}
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index c79dc52ad42..3e8a8c10e71 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1531,7 +1531,7 @@
-
-
-
-
-
@@ -760,21 +703,7 @@
-
-
-