[FIX] problem in database manager when db list is desactivated
[odoo/odoo.git] / addons / web / static / src / js / chrome.js
index ee8ca5d..de49b11 100644 (file)
@@ -42,6 +42,16 @@ instance.web.Notification =  instance.web.Widget.extend({
     }
 });
 
+instance.web.action_notify = function(element, action) {
+    element.do_notify(action.params.title, action.params.text, action.params.sticky);
+};
+instance.web.client_actions.add("action_notify", "instance.web.action_notify");
+
+instance.web.action_warn = function(element, action) {
+    element.do_warn(action.params.title, action.params.text, action.params.sticky);
+};
+instance.web.client_actions.add("action_warn", "instance.web.action_warn");
+
 /**
  * The very minimal function everything should call to create a dialog
  * in OpenERP Web Client.
@@ -237,6 +247,13 @@ instance.web.CrashManager = instance.web.Class.extend({
         if (!this.active) {
             return;
         }
+        // yes, exception handling is shitty
+        if (error.code === 300 && error.data && error.data.type == "client_exception" && error.data.debug.match("SessionExpiredException")) {
+            this.show_warning({type: "Session Expired", data: {
+                fault_code: _t("Your OpenERP session expired. Please refresh the current web page.")
+            }});
+            return;
+        }
         if (error.data.fault_code) {
             var split = ("" + error.data.fault_code).split('\n')[0].split(' -- ');
             if (split.length > 1) {
@@ -289,7 +306,7 @@ instance.web.CrashManager = instance.web.Class.extend({
 });
 
 instance.web.Loading = instance.web.Widget.extend({
-    template: 'Loading',
+    template: _t("Loading"),
     init: function(parent) {
         this._super(parent);
         this.count = 0;
@@ -363,7 +380,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
         var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
             self.lang_list = result;
         });
-        return $.when(fetch_db, fetch_langs).done(self.do_render);
+        return $.when(fetch_db, fetch_langs).always(self.do_render);
     },
     do_render: function() {
         var self = this;
@@ -390,11 +407,11 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
         self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
         self.$el.find("form[name=change_pwd_form]").validate({
             messages: {
-                old_pwd: "Please enter your previous password",
-                new_pwd: "Please enter your new password",
+                old_pwd: _t("Please enter your previous password"),
+                new_pwd: _t("Please enter your new password"),
                 confirm_pwd: {
-                    required: "Please confirm your new password",
-                    equalTo: "The confirmation does not match the password"
+                    required: _t("Please confirm your new password"),
+                    equalTo: _t("The confirmation does not match the password")
                 }
             },
             submitHandler: self.do_change_password
@@ -463,9 +480,14 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
                     'login': 'admin',
                     'password': form_obj['create_admin_pwd'],
                     'login_successful': function() {
-                        self.do_action("reload");
+                        var url = '/?db=' + form_obj['db_name'];
+                        if (self.session.debug) {
+                            url += '&debug';
+                        }
+                        instance.web.redirect(url);
                     },
                 },
+                _push_me: false,
             };
             self.do_action(client_action);
         });
@@ -478,7 +500,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
                 self.display_error(result);
                 return;
             }
-            self.do_notify("Duplicating database", "The database has been duplicated.");
+            self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
             self.start();
         });
     },
@@ -488,7 +510,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
             fields = $form.serializeArray(),
             $db_list = $form.find('[name=drop_db]'),
             db = $db_list.val();
-        if (!db || !confirm("Do you really want to delete the database: " + db + " ?")) {
+        if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
             return;
         }
         self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
@@ -496,7 +518,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
                 self.display_error(result);
                 return;
             }
-            self.do_notify("Dropping database", "The database '" + db + "' has been dropped");
+            self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
             self.start();
         });
     },
@@ -511,7 +533,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
             error: function(error){
                if(error){
                   self.display_error({
-                        title: 'Backup Database',
+                        title: _t("Backup Database"),
                         error: 'AccessDenied'
                   });
                }
@@ -534,13 +556,13 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
 
                 if (body.indexOf('403 Forbidden') !== -1) {
                     self.display_error({
-                        title: 'Access Denied',
-                        error: 'Incorrect super-administrator password'
+                        title: _t("Access Denied"),
+                        error: _t("Incorrect super-administrator password")
                     });
                 } else {
                     self.display_error({
-                        title: 'Restore Database',
-                        error: 'Could not restore the database'
+                        title: _t("Restore Database"),
+                        error: _t("Could not restore the database")
                     });
                 }
             },
@@ -560,13 +582,12 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
                 return;
             }
             self.unblockUI();
-            self.do_notify("Changed Password", "Password has been changed successfully");
+            self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
         });
     },
     do_exit: function () {
         this.$el.remove();
-        instance.webclient.toggle_bars(false);
-        this.do_action('login');
+        instance.webclient.show_login();
     }
 });
 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
@@ -574,6 +595,11 @@ instance.web.client_actions.add("database_manager", "instance.web.DatabaseManage
 instance.web.Login =  instance.web.Widget.extend({
     template: "Login",
     remember_credentials: true,
+    events: {
+        'change input[name=db],select[name=db]': function(ev) {
+            this.set('database_selector', $(ev.currentTarget).val());
+        },
+    },
 
     init: function(parent, action) {
         this._super(parent);
@@ -585,18 +611,18 @@ instance.web.Login =  instance.web.Widget.extend({
         if (_.isEmpty(this.params)) {
             this.params = $.bbq.getState(true);
         }
+        if (action && action.params && action.params.db) {
+            this.params.db = action.params.db;
+        } else if ($.deparam.querystring().db) {
+            this.params.db = $.deparam.querystring().db;
+        }
+        if (this.params.db) {
+            this.selected_db = this.params.db;
+        }
 
         if (this.params.login_successful) {
             this.on('login_successful', this, this.params.login_successful);
         }
-
-        if (this.has_local_storage && this.remember_credentials) {
-            this.selected_db = localStorage.getItem('last_db_login_success');
-            this.selected_login = localStorage.getItem('last_login_login_success');
-            if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
-                this.selected_password = localStorage.getItem('last_password_login_success');
-            }
-        }
     },
     start: function() {
         var self = this;
@@ -604,24 +630,55 @@ instance.web.Login =  instance.web.Widget.extend({
         self.$el.find('.oe_login_manage_db').click(function() {
             self.do_action("database_manager");
         });
+        self.on('change:database_selector', this, function() {
+            this.database_selected(this.get('database_selector'));
+        });
         var d = $.when();
-        if ($.deparam.querystring().db) {
-            self.params.db = $.deparam.querystring().db;
+        if ($.param.fragment().token) {
+            self.params.token = $.param.fragment().token;
         }
         // used by dbmanager.do_create via internal client action
         if (self.params.db && self.params.login && self.params.password) {
             d = self.do_login(self.params.db, self.params.login, self.params.password);
         } else {
-            if (self.params.db) {
-                self.on_db_loaded([self.params.db])
-            } else {
-                d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed);
-            }
+            d = self.rpc("/web/database/get_list", {})
+                .done(self.on_db_loaded)
+                .fail(self.on_db_failed)
+                .always(function() {
+                    if (self.selected_db && self.has_local_storage && self.remember_credentials) {
+                        self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
+                        if (self.session.debug) {
+                            self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || '');
+                        }
+                    }
+                });
         }
         return d;
     },
+    remember_last_used_database: function(db) {
+        // This cookie will be used server side in order to avoid db reloading on first visit
+        var ttl = 24 * 60 * 60 * 365;
+        document.cookie = [
+            'last_used_database=' + db,
+            'path=/',
+            'max-age=' + ttl,
+            'expires=' + new Date(new Date().getTime() + ttl * 1000).toGMTString()
+        ].join(';');
+    },
+    database_selected: function(db) {
+        var params = $.deparam.querystring();
+        params.db = db;
+        this.remember_last_used_database(db);
+        this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
+        this.$('[name=login], [name=password]').prop('readonly', true);
+        instance.web.redirect('/?' + $.param(params));
+    },
     on_db_loaded: function (result) {
+        var self = this;
         this.db_list = result;
+        if (!this.selected_db) {
+            this.selected_db = result[0];
+        }
         this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
         if(this.db_list.length === 0) {
             this.do_action("database_manager");
@@ -642,7 +699,7 @@ instance.web.Login =  instance.web.Widget.extend({
         }
         var db = this.$("form [name=db]").val();
         if (!db) {
-            this.do_warn("Login", "No database selected !");
+            this.do_warn(_t("Login"), _t("No database selected !"));
             return false;
         }
         var login = this.$("form input[name=login]").val();
@@ -662,23 +719,17 @@ instance.web.Login =  instance.web.Widget.extend({
         self.hide_error();
         self.$(".oe_login_pane").fadeOut("slow");
         return this.session.session_authenticate(db, login, password).then(function() {
-            if (self.has_local_storage) {
-                if(self.remember_credentials) {
-                    localStorage.setItem('last_db_login_success', db);
-                    localStorage.setItem('last_login_login_success', login);
-                    if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
-                        localStorage.setItem('last_password_login_success', password);
-                    }
-                } else {
-                    localStorage.setItem('last_db_login_success', '');
-                    localStorage.setItem('last_login_login_success', '');
-                    localStorage.setItem('last_password_login_success', '');
+            self.remember_last_used_database(db);
+            if (self.has_local_storage && self.remember_credentials) {
+                localStorage.setItem(db + '|last_login', login);
+                if (self.session.debug) {
+                    localStorage.setItem(db + '|last_password', password);
                 }
             }
             self.trigger('login_successful');
         }, function () {
             self.$(".oe_login_pane").fadeIn("fast", function() {
-                self.show_error("Invalid username or password");
+                self.show_error(_t("Invalid username or password"));
             });
         });
     },
@@ -692,6 +743,7 @@ instance.web.Login =  instance.web.Widget.extend({
 });
 instance.web.client_actions.add("login", "instance.web.Login");
 
+
 /**
  * Redirect to url by replacing window.location
  * If wait is true, sleep 1s and wait for the server i.e. after a restart.
@@ -729,6 +781,9 @@ instance.web.Reload = function(parent, action) {
 
     var sobj = $.deparam(l.search.substr(1));
     sobj.ts = new Date().getTime();
+    if (params.url_search) {
+        sobj = _.extend(sobj, params.url_search);
+    }
     var search = '?' + $.param(sobj);
 
     var hash = l.hash;
@@ -765,7 +820,7 @@ instance.web.ChangePassword =  instance.web.Widget.extend({
     template: "ChangePassword",
     start: function() {
         var self = this;
-        this.getParent().dialog_title = "Change Password";
+        this.getParent().dialog_title = _t("Change Password");
         var $button = self.$el.find('.oe_form_button');
         $button.appendTo(this.getParent().$buttons);
         $button.eq(2).click(function(){
@@ -799,10 +854,23 @@ instance.web.client_actions.add("change_password", "instance.web.ChangePassword"
 instance.web.Menu =  instance.web.Widget.extend({
     template: 'Menu',
     init: function() {
+        var self = this;
         this._super.apply(this, arguments);
         this.has_been_loaded = $.Deferred();
         this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
         this.data = {data:{children:[]}};
+        this.on("menu_loaded", this, function (menu_data) {
+            self.reflow();
+            // launch the fetch of needaction counters, asynchronous
+            if (!_.isEmpty(menu_data.all_menu_ids)) {
+                this.do_load_needaction(menu_data.all_menu_ids);
+            }
+        });
+        var lazyreflow = _.debounce(this.reflow.bind(this), 200);
+        instance.web.bus.on('resize', this, function() {
+            self.$el.height(0);
+            lazyreflow();
+        });
     },
     start: function() {
         this._super.apply(this, arguments);
@@ -818,16 +886,10 @@ instance.web.Menu =  instance.web.Widget.extend({
     },
     menu_loaded: function(data) {
         var self = this;
-        this.data = data;
+        this.data = {data: data};
         this.renderElement();
-        this.limit_entries();
-        // Hide toplevel item if there is only one
-        var $toplevel = this.$("li")
-        if($toplevel.length == 1) {
-            $toplevel.hide();
-        }
         this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
-        this.$el.on('click', 'a[data-menu]', this.on_menu_click);
+        this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
         // Hide second level submenus
         this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
         if (self.current_menu) {
@@ -835,40 +897,57 @@ instance.web.Menu =  instance.web.Widget.extend({
         }
         this.trigger('menu_loaded', data);
         this.has_been_loaded.resolve();
-        // Now launch the fetch of needaction counters, asynchronous
-        this.rpc("web/menu/load_needaction", {menu_ids: false}).done(function(r) {
+    },
+    do_load_needaction: function (menu_ids) {
+        var self = this;
+        menu_ids = _.compact(menu_ids);
+        if (_.isEmpty(menu_ids)) {
+            return $.when();
+        }
+        return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
             self.on_needaction_loaded(r);
         });
     },
     on_needaction_loaded: function(data) {
         var self = this;
         this.needaction_data = data;
-        _.each(this.needaction_data.data, function (item, menu_id) {
+        _.each(this.needaction_data, function (item, menu_id) {
             var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
-            $item.remove('oe_menu_counter');
+            $item.find('.oe_menu_counter').remove();
             if (item.needaction_counter && item.needaction_counter > 0) {
-                $item.append('<div class="oe_tag oe_tag_dark oe_menu_counter">' + item.needaction_counter + '</div>');
+                $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
             }
         });
     },
-    limit_entries: function() {
-        var maximum_visible_links = this.maximum_visible_links;
-        if (maximum_visible_links === 'auto') {
-            maximum_visible_links = this.auto_limit_entries();
-        }
-        if (maximum_visible_links < this.data.data.children.length) {
-            var $more = $(QWeb.render('Menu.more')),
-                $index = this.$el.find('li').eq(maximum_visible_links - 1);
-            $index.after($more);
-            //$('.oe_topbar').append($more);
-            $more.find('.oe_menu_more').append($index.next().nextAll());
+    /**
+     * Reflow the menu items and dock overflowing items into a "More" menu item.
+     * Automatically called when 'menu_loaded' event is triggered and on window resizing.
+     */
+    reflow: function() {
+        var self = this;
+        this.$el.height('auto').show();
+        var $more_container = this.$('.oe_menu_more_container').hide();
+        var $more = this.$('.oe_menu_more');
+        $more.children('li').insertBefore($more_container);
+        var $toplevel_items = this.$el.children('li').not($more_container).hide();
+        $toplevel_items.each(function() {
+            var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
+            self.$el.parent().children(':visible').each(function() {
+                remaining_space -= $(this).outerWidth();
+            });
+            if ($(this).width() > remaining_space) {
+                return false;
+            }
+            $(this).show();
+        });
+        $more.append($toplevel_items.filter(':hidden').show());
+        $more_container.toggle(!!$more.children().length);
+        // Hide toplevel item if there is only one
+        var $toplevel = this.$el.children("li:visible");
+        if ($toplevel.length === 1) {
+            $toplevel.hide();
         }
     },
-    auto_limit_entries: function() {
-        // TODO: auto detect overflow and bind window on resize
-        var width = $(window).width();
-        return Math.floor(width / 125);
-    },
     /**
      * 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
@@ -960,11 +1039,38 @@ instance.web.Menu =  instance.web.Widget.extend({
         }
         this.open_menu(id);
     },
+    do_reload_needaction: function () {
+        var self = this;
+        if (self.current_menu) {
+            self.do_load_needaction([self.current_menu]).then(function () {
+                self.trigger("need_action_reloaded");
+            });
+        }
+    },
     /**
      * Jquery event handler for menu click
      *
      * @param {Event} ev the jquery event
      */
+    on_top_menu_click: function(ev) {
+        var self = this;
+        var id = $(ev.currentTarget).data('menu');
+        var menu_ids = [id];
+        var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
+        function add_menu_ids (menu) {
+            if (menu.children) {
+                _.each(menu.children, function (menu) {
+                    menu_ids.push(menu.id);
+                    add_menu_ids(menu);
+                });
+            }
+        };
+        add_menu_ids(menu);
+        self.do_load_needaction(menu_ids).then(function () {
+            self.trigger("need_action_reloaded");
+        });
+        this.on_menu_click(ev);
+    },
     on_menu_click: function(ev) {
         ev.preventDefault();
         var needaction = $(ev.target).is('div.oe_menu_counter');
@@ -997,20 +1103,15 @@ instance.web.UserMenu =  instance.web.Widget.extend({
             if (!self.session.uid)
                 return;
             var func = new instance.web.Model("res.users").get_func("read");
-            return func(self.session.uid, ["name", "company_id"]).then(function(res) {
+            return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
                 var topbar_name = res.name;
                 if(instance.session.debug)
                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
                 if(res.company_id[0] > 1)
                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
                 self.$el.find('.oe_topbar_name').text(topbar_name);
-                if(!instance.session.debug) {
-                    self.rpc("/web/database/get_list", {}).done( function(result) {
-                       if (result.length > 1) {
-                            topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
-                       }
-                        self.$el.find('.oe_topbar_name').text(topbar_name);
-                    });
+                if (!instance.session.debug) {
+                    topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
                 }
                 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
                 $avatar.attr('src', avatar_src);
@@ -1018,6 +1119,9 @@ instance.web.UserMenu =  instance.web.Widget.extend({
         };
         this.update_promise = this.update_promise.then(fct, fct);
     },
+    on_menu_help: function() {
+        window.open('http://help.openerp.com', '_blank');
+    },
     on_menu_logout: function() {
         this.trigger('user_logout');
     },
@@ -1114,16 +1218,21 @@ instance.web.Client = instance.web.Widget.extend({
 
 instance.web.WebClient = instance.web.Client.extend({
     _template: 'WebClient',
+    events: {
+        'click .oe_logo_edit_admin': 'logo_edit'
+    },
     init: function(parent) {
         this._super(parent);
         this._current_state = null;
+        this.menu_dm = new instance.web.DropMisordered();
+        this.action_mutex = new $.Mutex();
     },
     start: function() {
         var self = this;
         return $.when(this._super()).then(function() {
-            self.$(".oe_logo").attr("href", $.param.fragment("" + window.location, "", 2).slice(0, -1));
             if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
                 $("body").addClass("kitten-mode-activated");
+                $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
                 if ($.blockUI) {
                     $.blockUI.defaults.message = '<img src="http://www.amigrave.com/kitten.gif">';
                 }
@@ -1169,6 +1278,7 @@ instance.web.WebClient = instance.web.Client.extend({
     show_application: function() {
         var self = this;
         self.toggle_bars(true);
+        self.update_logo();
         self.menu = new instance.web.Menu(self);
         self.menu.replace(this.$el.find('.oe_menu_placeholder'));
         self.menu.on('menu_click', this, this.on_menu_action);
@@ -1180,28 +1290,60 @@ instance.web.WebClient = instance.web.Client.extend({
         self.set_title();
         self.check_timezone();
     },
-    check_timezone: function() {
+    update_logo: function() {
+        var img = this.session.url('/web/binary/company_logo');
+        this.$('.oe_logo img').attr('src', '').attr('src', img);
+        this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
+    },
+    logo_edit: function(ev) {
         var self = this;
-        var user_offset = instance.session.user_context.tz_offset;
-        var offset = -(new Date().getTimezoneOffset());
-        // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
-        var browser_offset = (offset < 0) ? "-" : "+";
-        browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
-        browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
-        if (browser_offset !== user_offset) {
-            var notification = this.do_warn(_t("Timezone"), QWeb.render('WebClient.timezone_notification', {
-                user_timezone: instance.session.user_context.tz || 'UTC',
-                user_offset: user_offset,
-                browser_offset: browser_offset,
-            }), true);
-            notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
-                notification.close();
-            }).find('a').on('click', function() {
-                notification.close();
-                self.user_menu.on_menu_settings();
-                return false;
+        self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
+            self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
+                result.res_id = res['company_id'][0];
+                result.target = "new";
+                result.views = [[false, 'form']];
+                result.flags = {
+                    action_buttons: true,
+                };
+                self.action_manager.do_action(result);
+                var form = self.action_manager.dialog_widget.views.form.controller;
+                form.on("on_button_cancel", self.action_manager.dialog, self.action_manager.dialog.close);
+                form.on('record_saved', self, function() {
+                    self.action_manager.dialog.close();
+                    self.update_logo();
+                });
             });
-        }
+        });
+        return false;
+    },
+    check_timezone: function() {
+        var self = this;
+        return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
+            var user_offset = result[0]['tz_offset'];
+            var offset = -(new Date().getTimezoneOffset());
+            // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
+            var browser_offset = (offset < 0) ? "-" : "+";
+            browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
+            browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
+            if (browser_offset !== user_offset) {
+                var $icon = $(QWeb.render('WebClient.timezone_systray'));
+                $icon.on('click', function() {
+                    var notification = self.do_warn(_t("Timezone mismatch"), QWeb.render('WebClient.timezone_notification', {
+                        user_timezone: instance.session.user_context.tz || 'UTC',
+                        user_offset: user_offset,
+                        browser_offset: browser_offset,
+                    }), true);
+                    notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
+                        notification.close();
+                    }).find('a').on('click', function() {
+                        notification.close();
+                        self.user_menu.on_menu_settings();
+                        return false;
+                    });
+                });
+                $icon.appendTo(self.$('.oe_systray'));
+            }
+        });
     },
     destroy_content: function() {
         _.each(_.clone(this.getChildren()), function(el) {
@@ -1252,8 +1394,9 @@ instance.web.WebClient = instance.web.Client.extend({
     },
     on_hashchange: function(event) {
         var self = this;
-        var state = event.getState(true);
-        if (!_.isEqual(this._current_state, state)) {
+        var stringstate = event.getState(false);
+        if (!_.isEqual(this._current_state, stringstate)) {
+            var state = event.getState(true);
             if(!state.action && state.menu_id) {
                 self.menu.has_been_loaded.done(function() {
                     self.menu.do_reload().done(function() {
@@ -1265,41 +1408,49 @@ instance.web.WebClient = instance.web.Client.extend({
                 this.action_manager.do_load_state(state, !!this._current_state);
             }
         }
-        this._current_state = state;
+        this._current_state = stringstate;
     },
     do_push_state: function(state) {
         this.set_title(state.title);
         delete state.title;
         var url = '#' + $.param(state);
-        this._current_state = _.clone(state);
+        this._current_state = $.deparam($.param(state), false);     // stringify all values
         $.bbq.pushState(url);
         this.trigger('state_pushed', state);
     },
     on_menu_action: function(options) {
         var self = this;
-        return this.rpc("/web/action/load", { action_id: options.action_id })
+        return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
             .then(function (result) {
-                if (options.needaction) {
-                    result.context = new instance.web.CompoundContext(
-                        result.context,
-                        {search_default_message_unread: true});
-                }
-                return $.when(self.action_manager.do_action(result, {
-                    clear_breadcrumbs: true,
-                    action_menu_id: self.menu.current_menu,
-                })).fail(function() {
-                    self.menu.open_menu(options.previous_menu_id);
+                return self.action_mutex.exec(function() {
+                    if (options.needaction) {
+                        result.context = new instance.web.CompoundContext(result.context, {
+                            search_default_message_unread: true,
+                            search_disable_custom_filters: true,
+                        });
+                    }
+                    var completed = $.Deferred();
+                    $.when(self.action_manager.do_action(result, {
+                        clear_breadcrumbs: true,
+                        action_menu_id: self.menu.current_menu,
+                    })).fail(function() {
+                        self.menu.open_menu(options.previous_menu_id);
+                    }).always(function() {
+                        completed.resolve();
+                    });
+                    setTimeout(function() {
+                        completed.resolve();
+                    }, 2000);
+                    // We block the menu when clicking on an element until the action has correctly finished
+                    // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
+                    return completed;
                 });
             });
     },
     set_content_full_screen: function(fullscreen) {
-        if (fullscreen) {
-            $(".oe_webclient", this.$el).addClass("oe_content_full_screen");
-            $("body").css({'overflow-y':'hidden'});
-        } else {
-            $(".oe_webclient", this.$el).removeClass("oe_content_full_screen");
-            $("body").css({'overflow-y':'scroll'});
-        }
+        $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
+        this.$('.oe_webclient').toggleClass(
+            'oe_content_full_screen', fullscreen);
     },
     has_uncommitted_changes: function() {
         var $e = $.Event('clear_uncommitted_changes');
@@ -1316,17 +1467,17 @@ instance.web.EmbeddedClient = instance.web.Client.extend({
     _template: 'EmbedClient',
     init: function(parent, origin, dbname, login, key, action_id, options) {
         this._super(parent, origin);
-
-        this.dbname = dbname;
-        this.login = login;
-        this.key = key;
+        this.bind_credentials(dbname, login, key);
         this.action_id = action_id;
         this.options = options || {};
     },
     start: function() {
         var self = this;
         return $.when(this._super()).then(function() {
-            return instance.session.session_authenticate(self.dbname, self.login, self.key, true).then(function() {
+            return self.authenticate().then(function() {
+                if (!self.action_id) {
+                    return;
+                }
                 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
                     var action = result;
                     action.flags = _.extend({
@@ -1337,11 +1488,31 @@ instance.web.EmbeddedClient = instance.web.Client.extend({
                         //pager : false
                     }, self.options, action.flags || {});
 
-                    self.action_manager.do_action(action);
+                    self.do_action(action);
                 });
             });
         });
     },
+
+    do_action: function(/*...*/) {
+        var am = this.action_manager;
+        return am.do_action.apply(am, arguments);
+    },
+
+    authenticate: function() {
+        var s = instance.session;
+        if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
+            return $.when();
+        }
+        return instance.session.session_authenticate(this.dbname, this.login, this.key, true);
+    },
+
+    bind_credentials: function(dbname, login, key) {
+        this.dbname = dbname;
+        this.login = login;
+        this.key = key;
+    },
+
 });
 
 instance.web.embed = function (origin, dbname, login, key, action, options) {