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