[MERGE] Foward-port saas-5 up to ee4df1e
[odoo/odoo.git] / addons / web / static / src / js / chrome.js
1 /*---------------------------------------------------------
2  * OpenERP Web chrome
3  *---------------------------------------------------------*/
4 (function() {
5
6 var instance = openerp;
7 openerp.web.chrome = {};
8
9 var QWeb = instance.web.qweb,
10     _t = instance.web._t;
11
12 instance.web.Notification =  instance.web.Widget.extend({
13     template: 'Notification',
14     init: function() {
15         this._super.apply(this, arguments);
16         instance.web.notification = this;
17     },
18     start: function() {
19         this._super.apply(this, arguments);
20         this.$el.notify({
21             speed: 500,
22             expires: 2500
23         });
24     },
25     notify: function(title, text, sticky) {
26         sticky = !!sticky;
27         var opts = {};
28         if (sticky) {
29             opts.expires = false;
30         }
31         return this.$el.notify('create', {
32             title: title,
33             text: text
34         }, opts);
35     },
36     warn: function(title, text, sticky) {
37         sticky = !!sticky;
38         var opts = {};
39         if (sticky) {
40             opts.expires = false;
41         }
42         return this.$el.notify('create', 'oe_notification_alert', {
43             title: title,
44             text: text
45         }, opts);
46     }
47 });
48
49 var opened_modal = [];
50
51 instance.web.action_notify = function(element, action) {
52     element.do_notify(action.params.title, action.params.text, action.params.sticky);
53 };
54 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
55
56 instance.web.action_warn = function(element, action) {
57     element.do_warn(action.params.title, action.params.text, action.params.sticky);
58 };
59 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
60
61 /**
62     A useful class to handle dialogs.
63
64     Attributes:
65     - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
66     during the lifecycle of the dialog.
67 */
68 instance.web.Dialog = instance.web.Widget.extend({
69     dialog_title: "",
70     /**
71         Constructor.
72
73         @param {Widget} parent
74         @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
75             dictionary can contain the following keys:
76             - size: one of the following: 'large', 'medium', 'small'
77             - dialogClass: class to add to the body of dialog
78             - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
79                 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
80                 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
81                 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
82                 feature does not allow that kind of personalization.
83             - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
84         @param {jQuery object} content Some content to replace this.$el .
85     */
86     init: function (parent, options, content) {
87         var self = this;
88         this._super(parent);
89         this.content_to_set = content;
90         this.dialog_options = {
91             destroy_on_close: true,
92             size: 'large', //'medium', 'small'
93             buttons: null,
94         };
95         if (options) {
96             _.extend(this.dialog_options, options);
97         }
98         this.on("closing", this, this._closing);
99         this.$buttons = $('<div class="modal-footer"><span class="oe_dialog_custom_buttons"/></div>');
100     },
101     renderElement: function() {
102         if (this.content_to_set) {
103             this.setElement(this.content_to_set);
104         } else if (this.template) {
105             this._super();
106         }
107     },
108     /**
109         Opens the popup. Inits the dialog if it is not already inited.
110
111         @return this
112     */
113     open: function() {
114         if (!this.dialog_inited) {
115             this.init_dialog();
116         }
117         this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
118         $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal is opened
119         //add to list of currently opened modal
120         opened_modal.push(this.$dialog_box);
121         return this;
122     },
123     _add_buttons: function(buttons) {
124         var self = this;
125         var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
126         _.each(buttons, function(fn, text) {
127             // buttons can be object or array
128             var oe_link_class = fn.oe_link_class;
129             if (!_.isFunction(fn)) {
130                 text = fn.text;
131                 fn = fn.click;
132             }
133             var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {'class': oe_link_class} }}}));
134             $customButons.append($but);
135             $but.on('click', function(ev) {
136                 fn.call(self.$el, ev);
137             });
138         });
139     },
140     /**
141         Initializes the popup.
142
143         @return The result returned by start().
144     */
145     init_dialog: function() {
146         var self = this;
147         var options = _.extend({}, this.dialog_options);
148         options.title = options.title || this.dialog_title;
149         if (options.buttons) {
150             this._add_buttons(options.buttons);
151             delete(options.buttons);
152         }
153         this.renderElement();
154
155         this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
156         this.$el.modal({
157             'backdrop': false,
158             'keyboard': true,
159         });
160         if (options.size !== 'large'){
161             var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
162             if (options.size === 'small'){
163                 dialog_class_size.addClass('modal-sm');
164             }
165         }
166
167         this.$el.appendTo(this.$dialog_box.find(".modal-body"));
168         var $dialog_content = this.$dialog_box.find('.modal-content');
169         if (options.dialogClass){
170             $dialog_content.find(".modal-body").addClass(options.dialogClass);
171         }
172         $dialog_content.openerpClass();
173
174         this.$dialog_box.on('hidden.bs.modal', this, function() {
175             self.close();
176         });
177         this.$dialog_box.modal('show');
178
179         this.dialog_inited = true;
180         var res = this.start();
181         return res;
182     },
183     /**
184         Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
185     */
186     close: function(reason) {
187         if (this.dialog_inited && !this.__tmp_dialog_hiding) {
188             $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
189             if (this.$el.is(":data(bs.modal)")) {     // may have been destroyed by closing signal
190                 this.__tmp_dialog_hiding = true;
191                 this.$dialog_box.modal('hide');
192                 this.__tmp_dialog_hiding = undefined;
193             }
194             this.trigger("closing", reason);
195         }
196     },
197     _closing: function() {
198         if (this.__tmp_dialog_destroying)
199             return;
200         if (this.dialog_options.destroy_on_close) {
201             this.__tmp_dialog_closing = true;
202             this.destroy();
203             this.__tmp_dialog_closing = undefined;
204         }
205     },
206     /**
207         Destroys the popup, also closes it.
208     */
209     destroy: function (reason) {
210         this.$buttons.remove();
211         var self = this;
212         _.each(this.getChildren(), function(el) {
213             el.destroy();
214         });
215         if (! this.__tmp_dialog_closing) {
216             this.__tmp_dialog_destroying = true;
217             this.close(reason);
218             this.__tmp_dialog_destroying = undefined;
219         }
220         if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
221             //we need this to put the instruction to remove modal from DOM at the end
222             //of the queue, otherwise it might already have been removed before the modal-backdrop
223             //is removed when pressing escape key
224             var $element = this.$dialog_box;
225             setTimeout(function () {
226                 //remove modal from list of opened modal since we just destroy it
227                 var modal_list_index = $.inArray($element, opened_modal);
228                 if (modal_list_index > -1){
229                     opened_modal.splice(modal_list_index,1)[0].remove();
230                 }
231                 if (opened_modal.length > 0){
232                     //we still have other opened modal so we should focus it
233                     opened_modal[opened_modal.length-1].focus();
234                 }
235             },0);
236         }
237         this._super();
238     }
239 });
240
241 instance.web.CrashManager = instance.web.Class.extend({
242     init: function() {
243         this.active = true;
244     },
245
246     rpc_error: function(error) {
247         if (!this.active) {
248             return;
249         }
250         var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
251         if (handler) {
252             new (handler)(this, error).display();
253             return;
254         }
255         if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
256             this.show_warning({type: "Session Expired", data: { message: _t("Your Odoo session expired. Please refresh the current web page.") }});
257             return;
258         }
259         if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") {
260             this.show_warning(error);
261         } else {
262             this.show_error(error);
263         }
264     },
265     show_warning: function(error) {
266         if (!this.active) {
267             return;
268         }
269         if (error.data.exception_type === "except_osv") {
270             error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
271         }
272         new instance.web.Dialog(this, {
273             size: 'medium',
274             title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
275             buttons: [
276                 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
277             ],
278         }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
279     },
280     show_error: function(error) {
281         if (!this.active) {
282             return;
283         }
284         var buttons = {};
285         buttons[_t("Ok")] = function() {
286             this.parents('.modal').modal('hide');
287         };
288         new instance.web.Dialog(this, {
289             title: "Odoo " + _.str.capitalize(error.type),
290             buttons: buttons
291         }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
292     },
293     show_message: function(exception) {
294         this.show_error({
295             type: _t("Client Error"),
296             message: exception,
297             data: {debug: ""}
298         });
299     },
300 });
301
302 /**
303     An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
304 */
305 instance.web.ExceptionHandler = {
306     /**
307         @param parent The parent.
308         @param error The error object as returned by the JSON-RPC implementation.
309     */
310     init: function(parent, error) {},
311     /**
312         Called to inform to display the widget, if necessary. A typical way would be to implement
313         this interface in a class extending instance.web.Dialog and simply display the dialog in this
314         method.
315     */
316     display: function() {},
317 };
318
319 /**
320     The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
321     instance.web.ExceptionHandler.
322 */
323 instance.web.crash_manager_registry = new instance.web.Registry();
324
325 /**
326  * Handle redirection warnings, which behave more or less like a regular
327  * warning, with an additional redirection button.
328  */
329 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
330     init: function(parent, error) {
331         this._super(parent);
332         this.error = error;
333     },
334     display: function() {
335         var self = this;
336         error = this.error;
337         error.data.message = error.data.arguments[0];
338
339         new instance.web.Dialog(this, {
340             size: 'medium',
341             title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
342             buttons: [
343                 {text: _t("Ok"), click: function() { self.$el.parents('.modal').modal('hide');  self.destroy();}},
344                 {text: error.data.arguments[2],
345                  oe_link_class: 'oe_link',
346                  click: function() {
347                     window.location.href='#action='+error.data.arguments[1];
348                     self.destroy();
349                 }}
350             ],
351         }, QWeb.render('CrashManager.warning', {error: error})).open();
352     }
353 });
354 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
355
356 instance.web.Loading = instance.web.Widget.extend({
357     template: _t("Loading"),
358     init: function(parent) {
359         this._super(parent);
360         this.count = 0;
361         this.blocked_ui = false;
362         this.session.on("request", this, this.request_call);
363         this.session.on("response", this, this.response_call);
364         this.session.on("response_failed", this, this.response_call);
365     },
366     destroy: function() {
367         this.on_rpc_event(-this.count);
368         this._super();
369     },
370     request_call: function() {
371         this.on_rpc_event(1);
372     },
373     response_call: function() {
374         this.on_rpc_event(-1);
375     },
376     on_rpc_event : function(increment) {
377         var self = this;
378         if (!this.count && increment === 1) {
379             // Block UI after 3s
380             this.long_running_timer = setTimeout(function () {
381                 self.blocked_ui = true;
382                 instance.web.blockUI();
383             }, 3000);
384         }
385
386         this.count += increment;
387         if (this.count > 0) {
388             if (instance.session.debug) {
389                 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
390             } else {
391                 this.$el.text(_t("Loading"));
392             }
393             this.$el.show();
394             this.getParent().$el.addClass('oe_wait');
395         } else {
396             this.count = 0;
397             clearTimeout(this.long_running_timer);
398             // Don't unblock if blocked by somebody else
399             if (self.blocked_ui) {
400                 this.blocked_ui = false;
401                 instance.web.unblockUI();
402             }
403             this.$el.fadeOut();
404             this.getParent().$el.removeClass('oe_wait');
405         }
406     }
407 });
408
409 instance.web.DatabaseManager = instance.web.Widget.extend({
410     init: function(parent) {
411         this._super(parent);
412         this.unblockUIFunction = instance.web.unblockUI;
413         $.validator.addMethod('matches', function (s, _, re) {
414             return new RegExp(re).test(s);
415         }, _t("Invalid database name"));
416     },
417     start: function() {
418         var self = this;
419         $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
420         var fetch_db = this.rpc("/web/database/get_list", {}).then(
421             function(result) {
422                 self.db_list = result;
423             },
424             function (_, ev) {
425                 ev.preventDefault();
426                 self.db_list = null;
427             });
428         var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
429             self.lang_list = result;
430         });
431         return $.when(fetch_db, fetch_langs).always(self.do_render);
432     },
433     do_render: function() {
434         var self = this;
435         instance.webclient.toggle_bars(true);
436         self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
437         $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
438         $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
439         $('ul.oe_secondary_submenu > li:first').addClass('active');
440         $('ul.oe_secondary_submenu > li').bind('click', function (event) {
441             var menuitem = $(this);
442             menuitem.addClass('active').siblings().removeClass('active');
443             var form_id =menuitem.find('a').attr('href');
444             $(form_id).show().siblings().hide();
445             event.preventDefault();
446         });
447         $('#back-to-login').click(self.do_exit);
448         self.$el.find("td").addClass("oe_form_group_cell");
449         self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
450         self.$el.find("label").addClass("oe_form_label");
451         self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
452         self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
453         self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
454         self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
455         self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
456         self.$el.find("form[name=change_pwd_form]").validate({
457             messages: {
458                 old_pwd: _t("Please enter your previous password"),
459                 new_pwd: _t("Please enter your new password"),
460                 confirm_pwd: {
461                     required: _t("Please confirm your new password"),
462                     equalTo: _t("The confirmation does not match the password")
463                 }
464             },
465             submitHandler: self.do_change_password
466         });
467     },
468     destroy: function () {
469         this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
470         this._super();
471     },
472     /**
473      * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
474      * from unblocking the UI
475      */
476     blockUI: function () {
477         instance.web.blockUI();
478         instance.web.unblockUI = function () {};
479     },
480     /**
481      * Reinstates $.unblockUI so third parties can play with blockUI, and
482      * unblocks the UI
483      */
484     unblockUI: function () {
485         instance.web.unblockUI = this.unblockUIFunction;
486         instance.web.unblockUI();
487     },
488     /**
489      * Displays an error dialog resulting from the various RPC communications
490      * failing over themselves
491      *
492      * @param {Object} error error description
493      * @param {String} error.title title of the error dialog
494      * @param {String} error.error message of the error dialog
495      */
496     display_error: function (error) {
497         return new instance.web.Dialog(this, {
498             size: 'medium',
499             title: error.title,
500             buttons: [
501                 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
502             ]
503         }, $('<div>').html(error.error)).open();
504     },
505     do_create: function(form) {
506         var self = this;
507         var fields = $(form).serializeArray();
508         self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
509             if (result) {
510                 instance.web.redirect('/web');
511             } else {
512                 alert("Failed to create database");
513             }
514         });
515     },
516     do_duplicate: function(form) {
517         var self = this;
518         var fields = $(form).serializeArray();
519         self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
520             if (result.error) {
521                 self.display_error(result);
522                 return;
523             }
524             self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
525             self.start();
526         });
527     },
528     do_drop: function(form) {
529         var self = this;
530         var $form = $(form),
531             fields = $form.serializeArray(),
532             $db_list = $form.find('[name=drop_db]'),
533             db = $db_list.val();
534         if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
535             return;
536         }
537         self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
538             if (result.error) {
539                 self.display_error(result);
540                 return;
541             }
542             self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
543             self.start();
544         });
545     },
546     do_backup: function(form) {
547         var self = this;
548         self.blockUI();
549         self.session.get_file({
550             form: form,
551             success: function () {
552                 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
553             },
554             error: function(error){
555                if(error){
556                   self.display_error({
557                         title: _t("Backup Database"),
558                         error: 'AccessDenied'
559                   });
560                }
561             },
562             complete: function() {
563                 self.unblockUI();
564             }
565         });
566     },
567     do_restore: function(form) {
568         var self = this;
569         self.blockUI();
570         $(form).ajaxSubmit({
571             url: '/web/database/restore',
572             type: 'POST',
573             resetForm: true,
574             success: function (body) {
575                 // If empty body, everything went fine
576                 if (!body) { return; }
577
578                 if (body.indexOf('403 Forbidden') !== -1) {
579                     self.display_error({
580                         title: _t("Access Denied"),
581                         error: _t("Incorrect super-administrator password")
582                     });
583                 } else {
584                     self.display_error({
585                         title: _t("Restore Database"),
586                         error: _t("Could not restore the database")
587                     });
588                 }
589             },
590             complete: function() {
591                 self.unblockUI();
592                 self.do_notify(_t("Restored"), _t("Database restored successfully"));
593             }
594         });
595     },
596     do_change_password: function(form) {
597         var self = this;
598         self.rpc("/web/database/change_password", {
599             'fields': $(form).serializeArray()
600         }).done(function(result) {
601             if (result.error) {
602                 self.display_error(result);
603                 return;
604             }
605             self.unblockUI();
606             self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
607         });
608     },
609     do_exit: function () {
610         this.$el.remove();
611         instance.web.redirect('/web');
612     }
613 });
614 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
615
616 instance.web.login = function() {
617     instance.web.redirect('/web/login');
618 };
619 instance.web.client_actions.add("login", "instance.web.login");
620
621 instance.web.logout = function() {
622     instance.web.redirect('/web/session/logout');
623 };
624 instance.web.client_actions.add("logout", "instance.web.logout");
625
626
627 /**
628  * Redirect to url by replacing window.location
629  * If wait is true, sleep 1s and wait for the server i.e. after a restart.
630  */
631 instance.web.redirect = function(url, wait) {
632     // Dont display a dialog if some xmlhttprequest are in progress
633     if (instance.client && instance.client.crashmanager) {
634         instance.client.crashmanager.active = false;
635     }
636
637     var load = function() {
638         var old = "" + window.location;
639         var old_no_hash = old.split("#")[0];
640         var url_no_hash = url.split("#")[0];
641         location.assign(url);
642         if (old_no_hash === url_no_hash) {
643             location.reload(true);
644         }
645     };
646
647     var wait_server = function() {
648         instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
649             setTimeout(wait_server, 250);
650         });
651     };
652
653     if (wait) {
654         setTimeout(wait_server, 1000);
655     } else {
656         load();
657     }
658 };
659
660 /**
661  * Client action to reload the whole interface.
662  * If params.menu_id, it opens the given menu entry.
663  * If params.wait, reload will wait the openerp server to be reachable before reloading
664  */
665 instance.web.Reload = function(parent, action) {
666     var params = action.params || {};
667     var menu_id = params.menu_id || false;
668     var l = window.location;
669
670     var sobj = $.deparam(l.search.substr(1));
671     if (params.url_search) {
672         sobj = _.extend(sobj, params.url_search);
673     }
674     var search = '?' + $.param(sobj);
675
676     var hash = l.hash;
677     if (menu_id) {
678         hash = "#menu_id=" + menu_id;
679     }
680     var url = l.protocol + "//" + l.host + l.pathname + search + hash;
681
682     instance.web.redirect(url, params.wait);
683 };
684 instance.web.client_actions.add("reload", "instance.web.Reload");
685
686 /**
687  * Client action to refresh the session context (making sure
688  * HTTP requests will have the right one) then reload the
689  * whole interface.
690  */
691 instance.web.ReloadContext = function(parent, action) {
692     // side-effect of get_session_info is to refresh the session context
693     instance.session.rpc("/web/session/get_session_info", {}).then(function() {
694         instance.web.Reload(parent, action);
695     });
696 }
697 instance.web.client_actions.add("reload_context", "instance.web.ReloadContext");
698
699 /**
700  * Client action to go back in breadcrumb history.
701  * If can't go back in history stack, will go back to home.
702  */
703 instance.web.HistoryBack = function(parent) {
704     if (!parent.history_back()) {
705         instance.web.Home(parent);
706     }
707 };
708 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
709
710 /**
711  * Client action to go back home.
712  */
713 instance.web.Home = function(parent, action) {
714     var url = '/' + (window.location.search || '');
715     instance.web.redirect(url, action && action.params && action.params.wait);
716 };
717 instance.web.client_actions.add("home", "instance.web.Home");
718
719 instance.web.ChangePassword =  instance.web.Widget.extend({
720     template: "ChangePassword",
721     start: function() {
722         var self = this;
723         this.getParent().dialog_title = _t("Change Password");
724         var $button = self.$el.find('.oe_form_button');
725         $button.appendTo(this.getParent().$buttons);
726         $button.eq(2).click(function(){
727            self.$el.parents('.modal').modal('hide');
728         });
729         $button.eq(0).click(function(){
730           self.rpc("/web/session/change_password",{
731                'fields': $("form[name=change_password_form]").serializeArray()
732           }).done(function(result) {
733                if (result.error) {
734                   self.display_error(result);
735                   return;
736                } else {
737                    instance.webclient.on_logout();
738                }
739           });
740        });
741     },
742     display_error: function (error) {
743         return new instance.web.Dialog(this, {
744             size: 'medium',
745             title: error.title,
746             buttons: [
747                 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
748             ]
749         }, $('<div>').html(error.error)).open();
750     },
751 });
752 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
753
754 instance.web.Menu =  instance.web.Widget.extend({
755     init: function() {
756         var self = this;
757         this._super.apply(this, arguments);
758         this.is_bound = $.Deferred();
759         this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
760         this.data = {data:{children:[]}};
761         this.on("menu_bound", this, function() {
762             // launch the fetch of needaction counters, asynchronous
763             var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
764             var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
765             if (!_.isEmpty(all_menu_ids)) {
766                 this.do_load_needaction(all_menu_ids);
767             }
768         });
769     },
770     start: function() {
771         this._super.apply(this, arguments);
772         return this.bind_menu();
773     },
774     do_reload: function() {
775         var self = this;
776         self.bind_menu();
777     },
778     bind_menu: function() {
779         var self = this;
780         this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container')
781         this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
782         this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
783         // Hide second level submenus
784         this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
785         if (self.current_menu) {
786             self.open_menu(self.current_menu);
787         }
788         this.trigger('menu_bound');
789
790         var lazyreflow = _.debounce(this.reflow.bind(this), 200);
791         instance.web.bus.on('resize', this, function() {
792             if (parseInt(self.$el.parent().css('width')) <= 768 ) {
793                 lazyreflow('all_outside');
794             } else {
795                 lazyreflow();
796             }
797         });
798         instance.web.bus.trigger('resize');
799
800         this.is_bound.resolve();
801     },
802     do_load_needaction: function (menu_ids) {
803         var self = this;
804         menu_ids = _.compact(menu_ids);
805         if (_.isEmpty(menu_ids)) {
806             return $.when();
807         }
808         return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
809             self.on_needaction_loaded(r);
810         });
811     },
812     on_needaction_loaded: function(data) {
813         var self = this;
814         this.needaction_data = data;
815         _.each(this.needaction_data, function (item, menu_id) {
816             var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
817             $item.find('.badge').remove();
818             if (item.needaction_counter && item.needaction_counter > 0) {
819                 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
820             }
821         });
822     },
823     /**
824      * Reflow the menu items and dock overflowing items into a "More" menu item.
825      * Automatically called when 'menu_bound' event is triggered and on window resizing.
826      *
827      * @param {string} behavior If set to 'all_outside', all the items are displayed. If set to
828      * 'all_inside', all the items are hidden under the more item. If not set, only the 
829      * overflowing items are hidden.
830      */
831     reflow: function(behavior) {
832         var self = this;
833         var $more_container = this.$('#menu_more_container').hide();
834         var $more = this.$('#menu_more');
835         var $systray = this.$el.parents().find('.oe_systray');
836
837         $more.children('li').insertBefore($more_container);  // Pull all the items out of the more menu
838         
839         // 'all_outside' beahavior should display all the items, so hide the more menu and exit
840         if (behavior === 'all_outside') {
841             this.$el.find('li').show();
842             $more_container.hide();
843             return;
844         }
845
846         var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
847         $toplevel_items.each(function() {
848             // In all inside mode, we do not compute to know if we must hide the items, we hide them all
849             if (behavior === 'all_inside') {
850                 return false;
851             }
852             var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
853             self.$el.parent().children(':visible').each(function() {
854                 remaining_space -= $(this).outerWidth();
855             });
856
857             if ($(this).width() > remaining_space) {
858                 return false;
859             }
860             $(this).show();
861         });
862         $more.append($toplevel_items.filter(':hidden').show());
863         $more_container.toggle(!!$more.children().length || behavior === 'all_inside');
864         // Hide toplevel item if there is only one
865         var $toplevel = this.$el.children("li:visible");
866         if ($toplevel.length === 1 && behavior != 'all_inside') {
867             $toplevel.hide();
868         }
869     },
870     /**
871      * Opens a given menu by id, as if a user had browsed to that menu by hand
872      * except does not trigger any event on the way
873      *
874      * @param {Number} id database id of the terminal menu to select
875      */
876     open_menu: function (id) {
877         this.current_menu = id;
878         this.session.active_id = id;
879         var $clicked_menu, $sub_menu, $main_menu;
880         $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
881         this.trigger('open_menu', id, $clicked_menu);
882
883         if (this.$secondary_menus.has($clicked_menu).length) {
884             $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
885             $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
886         } else {
887             $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
888             $main_menu = $clicked_menu;
889         }
890
891         // Activate current main menu
892         this.$el.find('.active').removeClass('active');
893         $main_menu.parent().addClass('active');
894
895         // Show current sub menu
896         this.$secondary_menus.find('.oe_secondary_menu').hide();
897         $sub_menu.show();
898
899         // Hide/Show the leftbar menu depending of the presence of sub-items
900         this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
901
902         // Activate current menu item and show parents
903         this.$secondary_menus.find('.active').removeClass('active');
904         if ($main_menu !== $clicked_menu) {
905             $clicked_menu.parents().show();
906             if ($clicked_menu.is('.oe_menu_toggler')) {
907                 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
908             } else {
909                 $clicked_menu.parent().addClass('active');
910             }
911         }
912         // add a tooltip to cropped menu items
913         this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
914             $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
915        });
916     },
917     /**
918      * Call open_menu with the first menu_item matching an action_id
919      *
920      * @param {Number} id the action_id to match
921      */
922     open_action: function (id) {
923         var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
924         var menu_id = $menu.data('menu');
925         if (menu_id) {
926             this.open_menu(menu_id);
927         }
928     },
929     /**
930      * Process a click on a menu item
931      *
932      * @param {Number} id the menu_id
933      * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
934      */
935     menu_click: function(id, needaction) {
936         if (!id) { return; }
937
938         // find back the menuitem in dom to get the action
939         var $item = this.$el.find('a[data-menu=' + id + ']');
940         if (!$item.length) {
941             $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
942         }
943         var action_id = $item.data('action-id');
944         // If first level menu doesnt have action trigger first leaf
945         if (!action_id) {
946             if(this.$el.has($item).length) {
947                 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
948                 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
949                 if($items.length) {
950                     action_id = $items.data('action-id');
951                     id = $items.data('menu');
952                 }
953             }
954         }
955         if (action_id) {
956             this.trigger('menu_click', {
957                 action_id: action_id,
958                 needaction: needaction,
959                 id: id,
960                 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
961             }, $item);
962         } else {
963             console.log('Menu no action found web test 04 will fail');
964         }
965         this.open_menu(id);
966     },
967     do_reload_needaction: function () {
968         var self = this;
969         if (self.current_menu) {
970             self.do_load_needaction([self.current_menu]).then(function () {
971                 self.trigger("need_action_reloaded");
972             });
973         }
974     },
975     /**
976      * Jquery event handler for menu click
977      *
978      * @param {Event} ev the jquery event
979      */
980     on_top_menu_click: function(ev) {
981         ev.preventDefault();
982         var self = this;
983         var id = $(ev.currentTarget).data('menu');
984
985         // Fetch the menu leaves ids in order to check if they need a 'needaction'
986         var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
987         var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
988         var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
989
990         self.do_load_needaction(menu_ids).then(function () {
991             self.trigger("need_action_reloaded");
992         });
993
994         this.on_menu_click(ev);
995     },
996     on_menu_click: function(ev) {
997         ev.preventDefault();
998         var needaction = $(ev.target).is('div#menu_counter');
999         this.menu_click($(ev.currentTarget).data('menu'), needaction);
1000     },
1001 });
1002
1003 instance.web.UserMenu =  instance.web.Widget.extend({
1004     template: "UserMenu",
1005     init: function(parent) {
1006         this._super(parent);
1007         this.update_promise = $.Deferred().resolve();
1008     },
1009     start: function() {
1010         var self = this;
1011         this._super.apply(this, arguments);
1012         this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
1013             ev.preventDefault();
1014             var f = self['on_menu_' + $(this).data('menu')];
1015             if (f) {
1016                 f($(this));
1017             }
1018         });
1019         this.$el.parent().show()
1020     },
1021     do_update: function () {
1022         var self = this;
1023         var fct = function() {
1024             var $avatar = self.$el.find('.oe_topbar_avatar');
1025             $avatar.attr('src', $avatar.data('default-src'));
1026             if (!self.session.uid)
1027                 return;
1028             var func = new instance.web.Model("res.users").get_func("read");
1029             return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1030                 var topbar_name = res.name;
1031                 if(instance.session.debug)
1032                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1033                 if(res.company_id[0] > 1)
1034                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1035                 self.$el.find('.oe_topbar_name').text(topbar_name);
1036                 if (!instance.session.debug) {
1037                     topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1038                 }
1039                 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1040                 $avatar.attr('src', avatar_src);
1041
1042                 openerp.web.bus.trigger('resize');  // Re-trigger the reflow logic
1043             });
1044         };
1045         this.update_promise = this.update_promise.then(fct, fct);
1046     },
1047     on_menu_help: function() {
1048         window.open('http://help.openerp.com', '_blank');
1049     },
1050     on_menu_logout: function() {
1051         this.trigger('user_logout');
1052     },
1053     on_menu_settings: function() {
1054         var self = this;
1055         if (!this.getParent().has_uncommitted_changes()) {
1056             self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1057                 result.res_id = instance.session.uid;
1058                 self.getParent().action_manager.do_action(result);
1059             });
1060         }
1061     },
1062     on_menu_account: function() {
1063         var self = this;
1064         if (!this.getParent().has_uncommitted_changes()) {
1065             var P = new instance.web.Model('ir.config_parameter');
1066             P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1067                 var state = {
1068                             'd': instance.session.db,
1069                             'u': window.location.protocol + '//' + window.location.host,
1070                         };
1071                 var params = {
1072                     response_type: 'token',
1073                     client_id: dbuuid || '',
1074                     state: JSON.stringify(state),
1075                     scope: 'userinfo',
1076                 };
1077                 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1078             });
1079         }
1080     },
1081     on_menu_about: function() {
1082         var self = this;
1083         self.rpc("/web/webclient/version_info", {}).done(function(res) {
1084             var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1085             $help.find('a.oe_activate_debug_mode').click(function (e) {
1086                 e.preventDefault();
1087                 window.location = $.param.querystring( window.location.href, 'debug');
1088             });
1089             new instance.web.Dialog(this, {
1090                 size: 'medium',
1091                 dialogClass: 'oe_act_window',
1092                 title: _t("About"),
1093             }, $help).open();
1094         });
1095     },
1096 });
1097
1098 instance.web.FullscreenWidget = instance.web.Widget.extend({
1099     /**
1100      * Widgets extending the FullscreenWidget will be displayed fullscreen,
1101      * and will have a fixed 1:1 zoom level on mobile devices.
1102      */
1103     start: function(){
1104         if(!$('#oe-fullscreenwidget-viewport').length){
1105             $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1106         }
1107         instance.webclient.set_content_full_screen(true);
1108         return this._super();
1109     },
1110     destroy: function(){
1111         instance.webclient.set_content_full_screen(false);
1112         $('#oe-fullscreenwidget-viewport').remove();
1113         return this._super();
1114     },
1115
1116 });
1117
1118 instance.web.Client = instance.web.Widget.extend({
1119     init: function(parent, origin) {
1120         instance.client = instance.webclient = this;
1121         this.client_options = {};
1122         this._super(parent);
1123         this.origin = origin;
1124     },
1125     start: function() {
1126         var self = this;
1127         return instance.session.session_bind(this.origin).then(function() {
1128             self.bind_events();
1129             return self.show_common();
1130         });
1131     },
1132     bind_events: function() {
1133         var self = this;
1134         $('.oe_systray').show();
1135         this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1136             $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1137         });
1138         this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1139             ev.preventDefault();
1140             var $toggle = $(this);
1141             var doc_width = $(document).width();
1142             var $menu = $toggle.siblings('.oe_dropdown_menu');
1143             $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1144             var state = $menu.is('.oe_opened');
1145             setTimeout(function() {
1146                 // Do not alter propagation
1147                 $toggle.add($menu).toggleClass('oe_opened', !state);
1148                 if (!state) {
1149                     // Move $menu if outside window's edge
1150                     var offset = $menu.offset();
1151                     var menu_width = $menu.width();
1152                     var x = doc_width - offset.left - menu_width - 2;
1153                     if (x < 0) {
1154                         $menu.offset({ left: offset.left + x }).width(menu_width);
1155                     }
1156                 }
1157             }, 0);
1158         });
1159         instance.web.bus.on('click', this, function(ev) {
1160             $('.tooltip').remove();
1161             if (!$(ev.target).is('input[type=file]')) {
1162                 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1163             }
1164         });
1165     },
1166     show_common: function() {
1167         var self = this;
1168         this.crashmanager =  new instance.web.CrashManager();
1169         instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1170         self.notification = new instance.web.Notification(this);
1171         self.notification.appendTo(self.$el);
1172         self.loading = new instance.web.Loading(self);
1173         self.loading.appendTo(self.$('.openerp_webclient_container'));
1174         self.action_manager = new instance.web.ActionManager(self);
1175         self.action_manager.appendTo(self.$('.oe_application'));
1176     },
1177     toggle_bars: function(value) {
1178         this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1179     },
1180     has_uncommitted_changes: function() {
1181         return false;
1182     },
1183 });
1184
1185 instance.web.WebClient = instance.web.Client.extend({
1186     init: function(parent, client_options) {
1187         this._super(parent);
1188         if (client_options) {
1189             _.extend(this.client_options, client_options);
1190         }
1191         this._current_state = null;
1192         this.menu_dm = new instance.web.DropMisordered();
1193         this.action_mutex = new $.Mutex();
1194         this.set('title_part', {"zopenerp": "Odoo"});
1195     },
1196     start: function() {
1197         var self = this;
1198         this.on("change:title_part", this, this._title_changed);
1199         this._title_changed();
1200
1201         return $.when(this._super()).then(function() {
1202             if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1203                 self.to_kitten();
1204             }
1205             if (self.session.session_is_valid()) {
1206                 self.show_application();
1207             }
1208             if (self.client_options.action) {
1209                 self.action_manager.do_action(self.client_options.action);
1210                 delete(self.client_options.action);
1211             }
1212         });
1213     },
1214     to_kitten: function() {
1215         this.kitten = true;
1216         $("body").addClass("kitten-mode-activated");
1217         $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1218         if ($.blockUI) {
1219             var imgkit = Math.floor(Math.random() * 2 + 1);
1220             $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1221         }
1222     },
1223     /**
1224         Sets the first part of the title of the window, dedicated to the current action.
1225     */
1226     set_title: function(title) {
1227         this.set_title_part("action", title);
1228     },
1229     /**
1230         Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1231         a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1232         title of the window separated by '-'.
1233     */
1234     set_title_part: function(part, title) {
1235         var tmp = _.clone(this.get("title_part"));
1236         tmp[part] = title;
1237         this.set("title_part", tmp);
1238     },
1239     _title_changed: function() {
1240         var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1241         var tmp = "";
1242         _.each(parts, function(part) {
1243             var str = this.get("title_part")[part];
1244             if (str) {
1245                 tmp = tmp ? tmp + " - " + str : str;
1246             }
1247         }, this);
1248         document.title = tmp;
1249     },
1250     show_common: function() {
1251         var self = this;
1252         this._super();
1253         window.onerror = function (message, file, line) {
1254             self.crashmanager.show_error({
1255                 type: _t("Client Error"),
1256                 message: message,
1257                 data: {debug: file + ':' + line}
1258             });
1259         };
1260     },
1261     show_application: function() {
1262         var self = this;
1263         self.toggle_bars(true);
1264
1265         self.update_logo();
1266         this.$('.oe_logo_edit_admin').click(function(ev) {
1267             self.logo_edit(ev);
1268         });
1269
1270         // Menu is rendered server-side thus we don't want the widget to create any dom
1271         self.menu = new instance.web.Menu(self);
1272         self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
1273         self.menu.start();
1274         self.menu.on('menu_click', this, this.on_menu_action);
1275         self.user_menu = new instance.web.UserMenu(self);
1276         self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
1277         self.user_menu.on('user_logout', self, self.on_logout);
1278         self.user_menu.do_update();
1279         self.bind_hashchange();
1280         self.set_title();
1281         self.check_timezone();
1282         if (self.client_options.action_post_login) {
1283             self.action_manager.do_action(self.client_options.action_post_login);
1284             delete(self.client_options.action_post_login);
1285         }
1286     },
1287     update_logo: function() {
1288         var img = this.session.url('/web/binary/company_logo');
1289         this.$('.oe_logo img').attr('src', '').attr('src', img);
1290         this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1291     },
1292     logo_edit: function(ev) {
1293         var self = this;
1294         ev.preventDefault();
1295         self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1296             self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1297                 result.res_id = res['company_id'][0];
1298                 result.target = "new";
1299                 result.views = [[false, 'form']];
1300                 result.flags = {
1301                     action_buttons: true,
1302                 };
1303                 self.action_manager.do_action(result);
1304                 var form = self.action_manager.dialog_widget.views.form.controller;
1305                 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1306                 form.on('record_saved', self, function() {
1307                     self.action_manager.dialog_stop();
1308                     self.update_logo();
1309                 });
1310             });
1311         });
1312         return false;
1313     },
1314     check_timezone: function() {
1315         var self = this;
1316         return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1317             var user_offset = result[0]['tz_offset'];
1318             var offset = -(new Date().getTimezoneOffset());
1319             // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1320             var browser_offset = (offset < 0) ? "-" : "+";
1321             browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1322             browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1323             if (browser_offset !== user_offset) {
1324                 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1325                 $icon.on('click', function() {
1326                     var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1327                         user_timezone: instance.session.user_context.tz || 'UTC',
1328                         user_offset: user_offset,
1329                         browser_offset: browser_offset,
1330                     }), true);
1331                     notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1332                         notification.close();
1333                     }).find('a').on('click', function() {
1334                         notification.close();
1335                         self.user_menu.on_menu_settings();
1336                         return false;
1337                     });
1338                 });
1339                 $icon.prependTo(window.$('.oe_systray'));
1340             }
1341         });
1342     },
1343     destroy_content: function() {
1344         _.each(_.clone(this.getChildren()), function(el) {
1345             el.destroy();
1346         });
1347         this.$el.children().remove();
1348     },
1349     do_reload: function() {
1350         var self = this;
1351         return this.session.session_reload().then(function () {
1352             instance.session.load_modules(true).then(
1353                 self.menu.proxy('do_reload')); });
1354     },
1355     do_notify: function() {
1356         var n = this.notification;
1357         return n.notify.apply(n, arguments);
1358     },
1359     do_warn: function() {
1360         var n = this.notification;
1361         return n.warn.apply(n, arguments);
1362     },
1363     on_logout: function() {
1364         var self = this;
1365         if (!this.has_uncommitted_changes()) {
1366             self.action_manager.do_action('logout');
1367         }
1368     },
1369     bind_hashchange: function() {
1370         var self = this;
1371         $(window).bind('hashchange', this.on_hashchange);
1372
1373         var state = $.bbq.getState(true);
1374         if (_.isEmpty(state) || state.action == "login") {
1375             self.menu.is_bound.done(function() {
1376                 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1377                     if(data.action_id) {
1378                         self.action_manager.do_action(data.action_id[0]);
1379                         self.menu.open_action(data.action_id[0]);
1380                     } else {
1381                         var first_menu_id = self.menu.$el.find("a:first").data("menu");
1382                         if(first_menu_id) {
1383                             self.menu.menu_click(first_menu_id);
1384                         }                    }
1385                 });
1386             });
1387         } else {
1388             $(window).trigger('hashchange');
1389         }
1390     },
1391     on_hashchange: function(event) {
1392         var self = this;
1393         var stringstate = event.getState(false);
1394         if (!_.isEqual(this._current_state, stringstate)) {
1395             var state = event.getState(true);
1396             if(!state.action && state.menu_id) {
1397                 self.menu.is_bound.done(function() {
1398                     self.menu.menu_click(state.menu_id);
1399                 });
1400             } else {
1401                 state._push_me = false;  // no need to push state back...
1402                 this.action_manager.do_load_state(state, !!this._current_state);
1403             }
1404         }
1405         this._current_state = stringstate;
1406     },
1407     do_push_state: function(state) {
1408         this.set_title(state.title);
1409         delete state.title;
1410         var url = '#' + $.param(state);
1411         this._current_state = $.deparam($.param(state), false);     // stringify all values
1412         $.bbq.pushState(url);
1413         this.trigger('state_pushed', state);
1414     },
1415     on_menu_action: function(options) {
1416         var self = this;
1417         return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1418             .then(function (result) {
1419                 return self.action_mutex.exec(function() {
1420                     if (options.needaction) {
1421                         result.context = new instance.web.CompoundContext(result.context, {
1422                             search_default_message_unread: true,
1423                             search_disable_custom_filters: true,
1424                         });
1425                     }
1426                     var completed = $.Deferred();
1427                     $.when(self.action_manager.do_action(result, {
1428                         clear_breadcrumbs: true,
1429                         action_menu_id: self.menu.current_menu,
1430                     })).fail(function() {
1431                         self.menu.open_menu(options.previous_menu_id);
1432                     }).always(function() {
1433                         completed.resolve();
1434                     });
1435                     setTimeout(function() {
1436                         completed.resolve();
1437                     }, 2000);
1438                     // We block the menu when clicking on an element until the action has correctly finished
1439                     // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1440                     return completed;
1441                 });
1442             });
1443     },
1444     set_content_full_screen: function(fullscreen) {
1445         $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1446         this.$('.oe_webclient').toggleClass(
1447             'oe_content_full_screen', fullscreen);
1448     },
1449     has_uncommitted_changes: function() {
1450         var $e = $.Event('clear_uncommitted_changes');
1451         instance.web.bus.trigger('clear_uncommitted_changes', $e);
1452         if ($e.isDefaultPrevented()) {
1453             return true;
1454         } else {
1455             return this._super.apply(this, arguments);
1456         }
1457     },
1458 });
1459
1460 instance.web.EmbeddedClient = instance.web.Client.extend({
1461     _template: 'EmbedClient',
1462     init: function(parent, origin, dbname, login, key, action_id, options) {
1463         this._super(parent, origin);
1464         this.bind_credentials(dbname, login, key);
1465         this.action_id = action_id;
1466         this.options = options || {};
1467     },
1468     start: function() {
1469         var self = this;
1470         return $.when(this._super()).then(function() {
1471             return self.authenticate().then(function() {
1472                 if (!self.action_id) {
1473                     return;
1474                 }
1475                 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1476                     var action = result;
1477                     action.flags = _.extend({
1478                         //views_switcher : false,
1479                         search_view : false,
1480                         action_buttons : false,
1481                         sidebar : false
1482                         //pager : false
1483                     }, self.options, action.flags || {});
1484
1485                     self.do_action(action);
1486                 });
1487             });
1488         });
1489     },
1490
1491     do_action: function(/*...*/) {
1492         var am = this.action_manager;
1493         return am.do_action.apply(am, arguments);
1494     },
1495
1496     authenticate: function() {
1497         var s = instance.session;
1498         if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1499             return $.when();
1500         }
1501         return instance.session.session_authenticate(this.dbname, this.login, this.key);
1502     },
1503
1504     bind_credentials: function(dbname, login, key) {
1505         this.dbname = dbname;
1506         this.login = login;
1507         this.key = key;
1508     },
1509
1510 });
1511
1512 instance.web.embed = function (origin, dbname, login, key, action, options) {
1513     $('head').append($('<link>', {
1514         'rel': 'stylesheet',
1515         'type': 'text/css',
1516         'href': origin +'/web/css/web.assets_webclient'
1517     }));
1518     var currentScript = document.currentScript;
1519     if (!currentScript) {
1520         var sc = document.getElementsByTagName('script');
1521         currentScript = sc[sc.length-1];
1522     }
1523     var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1524     client.insertAfter(currentScript);
1525 };
1526
1527 })();
1528
1529 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: