1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
6 var instance = openerp;
7 openerp.web.chrome = {};
9 var QWeb = instance.web.qweb,
12 instance.web.Notification = instance.web.Widget.extend({
13 template: 'Notification',
15 this._super.apply(this, arguments);
16 instance.web.notification = this;
19 this._super.apply(this, arguments);
25 notify: function(title, text, sticky) {
31 return this.$el.notify('create', {
36 warn: function(title, text, sticky) {
42 return this.$el.notify('create', 'oe_notification_alert', {
49 var opened_modal = [];
51 instance.web.action_notify = function(element, action) {
52 element.do_notify(action.params.title, action.params.text, action.params.sticky);
54 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
56 instance.web.action_warn = function(element, action) {
57 element.do_warn(action.params.title, action.params.text, action.params.sticky);
59 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
62 A useful class to handle dialogs.
65 - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
66 during the lifecycle of the dialog.
68 instance.web.Dialog = instance.web.Widget.extend({
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 .
86 init: function (parent, options, content) {
89 this.content_to_set = content;
90 this.dialog_options = {
91 destroy_on_close: true,
92 size: 'large', //'medium', 'small'
96 _.extend(this.dialog_options, options);
98 this.on("closing", this, this._closing);
99 this.$buttons = $('<div class="modal-footer"><span class="oe_dialog_custom_buttons"/></div>');
101 renderElement: function() {
102 if (this.content_to_set) {
103 this.setElement(this.content_to_set);
104 } else if (this.template) {
109 Opens the popup. Inits the dialog if it is not already inited.
114 if (!this.dialog_inited) {
117 this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
118 //add to list of currently opened modal
119 opened_modal.push(this.$dialog_box);
122 _add_buttons: function(buttons) {
124 var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
125 _.each(buttons, function(fn, text) {
126 // buttons can be object or array
127 var oe_link_class = fn.oe_link_class;
128 if (!_.isFunction(fn)) {
132 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {'class': oe_link_class} }}}));
133 $customButons.append($but);
134 $but.on('click', function(ev) {
135 fn.call(self.$el, ev);
140 Initializes the popup.
142 @return The result returned by start().
144 init_dialog: function() {
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);
152 this.renderElement();
154 this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
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');
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);
171 $dialog_content.openerpClass();
173 this.$dialog_box.on('hidden.bs.modal', this, function() {
176 this.$dialog_box.modal('show');
178 this.dialog_inited = true;
179 var res = this.start();
183 Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
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;
193 this.trigger("closing", reason);
196 _closing: function() {
197 if (this.__tmp_dialog_destroying)
199 if (this.dialog_options.destroy_on_close) {
200 this.__tmp_dialog_closing = true;
202 this.__tmp_dialog_closing = undefined;
206 Destroys the popup, also closes it.
208 destroy: function (reason) {
209 this.$buttons.remove();
211 _.each(this.getChildren(), function(el) {
214 if (! this.__tmp_dialog_closing) {
215 this.__tmp_dialog_destroying = true;
217 this.__tmp_dialog_destroying = undefined;
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();
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();
240 instance.web.CrashManager = instance.web.Class.extend({
245 rpc_error: function(error) {
249 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
251 new (handler)(this, error).display();
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 Odoo session expired. Please refresh the current web page.") }});
258 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") {
259 this.show_warning(error);
261 this.show_error(error);
264 show_warning: function(error) {
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]})});
271 new instance.web.Dialog(this, {
273 title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
275 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
277 }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
279 show_error: function(error) {
284 buttons[_t("Ok")] = function() {
285 this.parents('.modal').modal('hide');
287 new instance.web.Dialog(this, {
288 title: "Odoo " + _.str.capitalize(error.type),
290 }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
292 show_message: function(exception) {
294 type: _t("Client Error"),
302 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
304 instance.web.ExceptionHandler = {
306 @param parent The parent.
307 @param error The error object as returned by the JSON-RPC implementation.
309 init: function(parent, error) {},
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
315 display: function() {},
319 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
320 instance.web.ExceptionHandler.
322 instance.web.crash_manager_registry = new instance.web.Registry();
325 * Handle redirection warnings, which behave more or less like a regular
326 * warning, with an additional redirection button.
328 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
329 init: function(parent, error) {
333 display: function() {
336 error.data.message = error.data.arguments[0];
338 new instance.web.Dialog(this, {
340 title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
342 {text: _t("Ok"), click: function() { self.$el.parents('.modal').modal('hide'); self.destroy();}},
343 {text: error.data.arguments[2],
344 oe_link_class: 'oe_link',
346 window.location.href='#action='+error.data.arguments[1];
350 }, QWeb.render('CrashManager.warning', {error: error})).open();
353 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
355 instance.web.Loading = instance.web.Widget.extend({
356 template: _t("Loading"),
357 init: function(parent) {
360 this.blocked_ui = false;
361 this.session.on("request", this, this.request_call);
362 this.session.on("response", this, this.response_call);
363 this.session.on("response_failed", this, this.response_call);
365 destroy: function() {
366 this.on_rpc_event(-this.count);
369 request_call: function() {
370 this.on_rpc_event(1);
372 response_call: function() {
373 this.on_rpc_event(-1);
375 on_rpc_event : function(increment) {
377 if (!this.count && increment === 1) {
379 this.long_running_timer = setTimeout(function () {
380 self.blocked_ui = true;
381 instance.web.blockUI();
385 this.count += increment;
386 if (this.count > 0) {
387 if (instance.session.debug) {
388 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
390 this.$el.text(_t("Loading"));
393 this.getParent().$el.addClass('oe_wait');
396 clearTimeout(this.long_running_timer);
397 // Don't unblock if blocked by somebody else
398 if (self.blocked_ui) {
399 this.blocked_ui = false;
400 instance.web.unblockUI();
403 this.getParent().$el.removeClass('oe_wait');
408 instance.web.DatabaseManager = instance.web.Widget.extend({
409 init: function(parent) {
411 this.unblockUIFunction = instance.web.unblockUI;
412 $.validator.addMethod('matches', function (s, _, re) {
413 return new RegExp(re).test(s);
414 }, _t("Invalid database name"));
418 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
419 var fetch_db = this.rpc("/web/database/get_list", {}).then(
421 self.db_list = result;
427 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
428 self.lang_list = result;
430 return $.when(fetch_db, fetch_langs).always(self.do_render);
432 do_render: function() {
434 instance.webclient.toggle_bars(true);
435 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
436 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
437 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
438 $('ul.oe_secondary_submenu > li:first').addClass('active');
439 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
440 var menuitem = $(this);
441 menuitem.addClass('active').siblings().removeClass('active');
442 var form_id =menuitem.find('a').attr('href');
443 $(form_id).show().siblings().hide();
444 event.preventDefault();
446 $('#back-to-login').click(self.do_exit);
447 self.$el.find("td").addClass("oe_form_group_cell");
448 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
449 self.$el.find("label").addClass("oe_form_label");
450 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
451 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
452 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
453 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
454 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
455 self.$el.find("form[name=change_pwd_form]").validate({
457 old_pwd: _t("Please enter your previous password"),
458 new_pwd: _t("Please enter your new password"),
460 required: _t("Please confirm your new password"),
461 equalTo: _t("The confirmation does not match the password")
464 submitHandler: self.do_change_password
467 destroy: function () {
468 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
472 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
473 * from unblocking the UI
475 blockUI: function () {
476 instance.web.blockUI();
477 instance.web.unblockUI = function () {};
480 * Reinstates $.unblockUI so third parties can play with blockUI, and
483 unblockUI: function () {
484 instance.web.unblockUI = this.unblockUIFunction;
485 instance.web.unblockUI();
488 * Displays an error dialog resulting from the various RPC communications
489 * failing over themselves
491 * @param {Object} error error description
492 * @param {String} error.title title of the error dialog
493 * @param {String} error.error message of the error dialog
495 display_error: function (error) {
496 return new instance.web.Dialog(this, {
500 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
502 }, $('<div>').html(error.error)).open();
504 do_create: function(form) {
506 var fields = $(form).serializeArray();
507 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
509 instance.web.redirect('/web');
511 alert("Failed to create database");
515 do_duplicate: function(form) {
517 var fields = $(form).serializeArray();
518 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
520 self.display_error(result);
523 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
527 do_drop: function(form) {
530 fields = $form.serializeArray(),
531 $db_list = $form.find('[name=drop_db]'),
533 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
536 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
538 self.display_error(result);
541 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
545 do_backup: function(form) {
548 self.session.get_file({
550 success: function () {
551 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
553 error: function(error){
556 title: _t("Backup Database"),
557 error: 'AccessDenied'
561 complete: function() {
566 do_restore: function(form) {
570 url: '/web/database/restore',
573 success: function (body) {
574 // If empty body, everything went fine
575 if (!body) { return; }
577 if (body.indexOf('403 Forbidden') !== -1) {
579 title: _t("Access Denied"),
580 error: _t("Incorrect super-administrator password")
584 title: _t("Restore Database"),
585 error: _t("Could not restore the database")
589 complete: function() {
591 self.do_notify(_t("Restored"), _t("Database restored successfully"));
595 do_change_password: function(form) {
597 self.rpc("/web/database/change_password", {
598 'fields': $(form).serializeArray()
599 }).done(function(result) {
601 self.display_error(result);
605 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
608 do_exit: function () {
610 instance.web.redirect('/web');
613 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
615 instance.web.login = function() {
616 instance.web.redirect('/web/login');
618 instance.web.client_actions.add("login", "instance.web.login");
620 instance.web.logout = function() {
621 instance.web.redirect('/web/session/logout');
623 instance.web.client_actions.add("logout", "instance.web.logout");
627 * Redirect to url by replacing window.location
628 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
630 instance.web.redirect = function(url, wait) {
631 // Dont display a dialog if some xmlhttprequest are in progress
632 if (instance.client && instance.client.crashmanager) {
633 instance.client.crashmanager.active = false;
636 var load = function() {
637 var old = "" + window.location;
638 var old_no_hash = old.split("#")[0];
639 var url_no_hash = url.split("#")[0];
640 location.assign(url);
641 if (old_no_hash === url_no_hash) {
642 location.reload(true);
646 var wait_server = function() {
647 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
648 setTimeout(wait_server, 250);
653 setTimeout(wait_server, 1000);
660 * Client action to reload the whole interface.
661 * If params.menu_id, it opens the given menu entry.
662 * If params.wait, reload will wait the openerp server to be reachable before reloading
664 instance.web.Reload = function(parent, action) {
665 var params = action.params || {};
666 var menu_id = params.menu_id || false;
667 var l = window.location;
669 var sobj = $.deparam(l.search.substr(1));
670 if (params.url_search) {
671 sobj = _.extend(sobj, params.url_search);
673 var search = '?' + $.param(sobj);
677 hash = "#menu_id=" + menu_id;
679 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
681 instance.web.redirect(url, params.wait);
683 instance.web.client_actions.add("reload", "instance.web.Reload");
686 * Client action to refresh the session context (making sure
687 * HTTP requests will have the right one) then reload the
690 instance.web.ReloadContext = function(parent, action) {
691 // side-effect of get_session_info is to refresh the session context
692 instance.session.rpc("/web/session/get_session_info", {}).then(function() {
693 instance.web.Reload(parent, action);
696 instance.web.client_actions.add("reload_context", "instance.web.ReloadContext");
699 * Client action to go back in breadcrumb history.
700 * If can't go back in history stack, will go back to home.
702 instance.web.HistoryBack = function(parent) {
703 if (!parent.history_back()) {
704 instance.web.Home(parent);
707 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
710 * Client action to go back home.
712 instance.web.Home = function(parent, action) {
713 var url = '/' + (window.location.search || '');
714 instance.web.redirect(url, action && action.params && action.params.wait);
716 instance.web.client_actions.add("home", "instance.web.Home");
718 instance.web.ChangePassword = instance.web.Widget.extend({
719 template: "ChangePassword",
722 this.getParent().dialog_title = _t("Change Password");
723 var $button = self.$el.find('.oe_form_button');
724 $button.appendTo(this.getParent().$buttons);
725 $button.eq(2).click(function(){
726 self.$el.parents('.modal').modal('hide');
728 $button.eq(0).click(function(){
729 self.rpc("/web/session/change_password",{
730 'fields': $("form[name=change_password_form]").serializeArray()
731 }).done(function(result) {
733 self.display_error(result);
736 instance.webclient.on_logout();
741 display_error: function (error) {
742 return new instance.web.Dialog(this, {
746 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
748 }, $('<div>').html(error.error)).open();
751 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
753 instance.web.Menu = instance.web.Widget.extend({
756 this._super.apply(this, arguments);
757 this.is_bound = $.Deferred();
758 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
759 this.data = {data:{children:[]}};
760 this.on("menu_bound", this, function() {
761 // launch the fetch of needaction counters, asynchronous
762 var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
763 var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
764 if (!_.isEmpty(all_menu_ids)) {
765 this.do_load_needaction(all_menu_ids);
770 this._super.apply(this, arguments);
771 return this.bind_menu();
773 do_reload: function() {
777 bind_menu: function() {
779 this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container')
780 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
781 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
782 // Hide second level submenus
783 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
784 if (self.current_menu) {
785 self.open_menu(self.current_menu);
787 this.trigger('menu_bound');
789 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
790 instance.web.bus.on('resize', this, function() {
791 if (parseInt(self.$el.parent().css('width')) <= 768 ) {
792 lazyreflow('all_outside');
797 instance.web.bus.trigger('resize');
799 this.is_bound.resolve();
801 do_load_needaction: function (menu_ids) {
803 menu_ids = _.compact(menu_ids);
804 if (_.isEmpty(menu_ids)) {
807 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
808 self.on_needaction_loaded(r);
811 on_needaction_loaded: function(data) {
813 this.needaction_data = data;
814 _.each(this.needaction_data, function (item, menu_id) {
815 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
816 $item.find('.badge').remove();
817 if (item.needaction_counter && item.needaction_counter > 0) {
818 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
823 * Reflow the menu items and dock overflowing items into a "More" menu item.
824 * Automatically called when 'menu_bound' event is triggered and on window resizing.
826 * @param {string} behavior If set to 'all_outside', all the items are displayed. If set to
827 * 'all_inside', all the items are hidden under the more item. If not set, only the
828 * overflowing items are hidden.
830 reflow: function(behavior) {
832 var $more_container = this.$('#menu_more_container').hide();
833 var $more = this.$('#menu_more');
834 var $systray = this.$el.parents().find('.oe_systray');
836 $more.children('li').insertBefore($more_container); // Pull all the items out of the more menu
838 // 'all_outside' beahavior should display all the items, so hide the more menu and exit
839 if (behavior === 'all_outside') {
840 this.$el.find('li').show();
841 $more_container.hide();
845 var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
846 $toplevel_items.each(function() {
847 // In all inside mode, we do not compute to know if we must hide the items, we hide them all
848 if (behavior === 'all_inside') {
851 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
852 self.$el.parent().children(':visible').each(function() {
853 remaining_space -= $(this).outerWidth();
856 if ($(this).width() > remaining_space) {
861 $more.append($toplevel_items.filter(':hidden').show());
862 $more_container.toggle(!!$more.children().length || behavior === 'all_inside');
863 // Hide toplevel item if there is only one
864 var $toplevel = this.$el.children("li:visible");
865 if ($toplevel.length === 1 && behavior != 'all_inside') {
870 * Opens a given menu by id, as if a user had browsed to that menu by hand
871 * except does not trigger any event on the way
873 * @param {Number} id database id of the terminal menu to select
875 open_menu: function (id) {
876 this.current_menu = id;
877 this.session.active_id = id;
878 var $clicked_menu, $sub_menu, $main_menu;
879 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
880 this.trigger('open_menu', id, $clicked_menu);
882 if (this.$secondary_menus.has($clicked_menu).length) {
883 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
884 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
886 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
887 $main_menu = $clicked_menu;
890 // Activate current main menu
891 this.$el.find('.active').removeClass('active');
892 $main_menu.parent().addClass('active');
894 // Show current sub menu
895 this.$secondary_menus.find('.oe_secondary_menu').hide();
898 // Hide/Show the leftbar menu depending of the presence of sub-items
899 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
901 // Activate current menu item and show parents
902 this.$secondary_menus.find('.active').removeClass('active');
903 if ($main_menu !== $clicked_menu) {
904 $clicked_menu.parents().show();
905 if ($clicked_menu.is('.oe_menu_toggler')) {
906 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
908 $clicked_menu.parent().addClass('active');
911 // add a tooltip to cropped menu items
912 this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
913 $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
917 * Call open_menu with the first menu_item matching an action_id
919 * @param {Number} id the action_id to match
921 open_action: function (id) {
922 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
923 var menu_id = $menu.data('menu');
925 this.open_menu(menu_id);
929 * Process a click on a menu item
931 * @param {Number} id the menu_id
932 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
934 menu_click: function(id, needaction) {
937 // find back the menuitem in dom to get the action
938 var $item = this.$el.find('a[data-menu=' + id + ']');
940 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
942 var action_id = $item.data('action-id');
943 // If first level menu doesnt have action trigger first leaf
945 if(this.$el.has($item).length) {
946 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
947 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
949 action_id = $items.data('action-id');
950 id = $items.data('menu');
955 this.trigger('menu_click', {
956 action_id: action_id,
957 needaction: needaction,
959 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
962 console.log('Menu no action found web test 04 will fail');
966 do_reload_needaction: function () {
968 if (self.current_menu) {
969 self.do_load_needaction([self.current_menu]).then(function () {
970 self.trigger("need_action_reloaded");
975 * Jquery event handler for menu click
977 * @param {Event} ev the jquery event
979 on_top_menu_click: function(ev) {
982 var id = $(ev.currentTarget).data('menu');
984 // Fetch the menu leaves ids in order to check if they need a 'needaction'
985 var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
986 var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
987 var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
989 self.do_load_needaction(menu_ids).then(function () {
990 self.trigger("need_action_reloaded");
993 this.on_menu_click(ev);
995 on_menu_click: function(ev) {
997 var needaction = $(ev.target).is('div#menu_counter');
998 this.menu_click($(ev.currentTarget).data('menu'), needaction);
1002 instance.web.UserMenu = instance.web.Widget.extend({
1003 template: "UserMenu",
1004 init: function(parent) {
1005 this._super(parent);
1006 this.update_promise = $.Deferred().resolve();
1010 this._super.apply(this, arguments);
1011 this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
1012 ev.preventDefault();
1013 var f = self['on_menu_' + $(this).data('menu')];
1018 this.$el.parent().show()
1020 do_update: function () {
1022 var fct = function() {
1023 var $avatar = self.$el.find('.oe_topbar_avatar');
1024 $avatar.attr('src', $avatar.data('default-src'));
1025 if (!self.session.uid)
1027 var func = new instance.web.Model("res.users").get_func("read");
1028 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1029 var topbar_name = res.name;
1030 if(instance.session.debug)
1031 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1032 if(res.company_id[0] > 1)
1033 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1034 self.$el.find('.oe_topbar_name').text(topbar_name);
1035 if (!instance.session.debug) {
1036 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1038 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1039 $avatar.attr('src', avatar_src);
1041 openerp.web.bus.trigger('resize'); // Re-trigger the reflow logic
1044 this.update_promise = this.update_promise.then(fct, fct);
1046 on_menu_help: function() {
1047 window.open('http://help.openerp.com', '_blank');
1049 on_menu_logout: function() {
1050 this.trigger('user_logout');
1052 on_menu_settings: function() {
1054 if (!this.getParent().has_uncommitted_changes()) {
1055 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1056 result.res_id = instance.session.uid;
1057 self.getParent().action_manager.do_action(result);
1061 on_menu_account: function() {
1063 if (!this.getParent().has_uncommitted_changes()) {
1064 var P = new instance.web.Model('ir.config_parameter');
1065 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1067 'd': instance.session.db,
1068 'u': window.location.protocol + '//' + window.location.host,
1071 response_type: 'token',
1072 client_id: dbuuid || '',
1073 state: JSON.stringify(state),
1076 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1080 on_menu_about: function() {
1082 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1083 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1084 $help.find('a.oe_activate_debug_mode').click(function (e) {
1086 window.location = $.param.querystring( window.location.href, 'debug');
1088 new instance.web.Dialog(this, {
1090 dialogClass: 'oe_act_window',
1097 instance.web.FullscreenWidget = instance.web.Widget.extend({
1099 * Widgets extending the FullscreenWidget will be displayed fullscreen,
1100 * and will have a fixed 1:1 zoom level on mobile devices.
1103 if(!$('#oe-fullscreenwidget-viewport').length){
1104 $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1106 instance.webclient.set_content_full_screen(true);
1107 return this._super();
1109 destroy: function(){
1110 instance.webclient.set_content_full_screen(false);
1111 $('#oe-fullscreenwidget-viewport').remove();
1112 return this._super();
1117 instance.web.Client = instance.web.Widget.extend({
1118 init: function(parent, origin) {
1119 instance.client = instance.webclient = this;
1120 this.client_options = {};
1121 this._super(parent);
1122 this.origin = origin;
1126 return instance.session.session_bind(this.origin).then(function() {
1128 return self.show_common();
1131 bind_events: function() {
1133 $('.oe_systray').show();
1134 this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1135 $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1137 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1138 ev.preventDefault();
1139 var $toggle = $(this);
1140 var doc_width = $(document).width();
1141 var $menu = $toggle.siblings('.oe_dropdown_menu');
1142 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1143 var state = $menu.is('.oe_opened');
1144 setTimeout(function() {
1145 // Do not alter propagation
1146 $toggle.add($menu).toggleClass('oe_opened', !state);
1148 // Move $menu if outside window's edge
1149 var offset = $menu.offset();
1150 var menu_width = $menu.width();
1151 var x = doc_width - offset.left - menu_width - 2;
1153 $menu.offset({ left: offset.left + x }).width(menu_width);
1158 instance.web.bus.on('click', this, function(ev) {
1159 $.fn.tooltip('destroy');
1160 if (!$(ev.target).is('input[type=file]')) {
1161 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1165 show_common: function() {
1167 this.crashmanager = new instance.web.CrashManager();
1168 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1169 self.notification = new instance.web.Notification(this);
1170 self.notification.appendTo(self.$el);
1171 self.loading = new instance.web.Loading(self);
1172 self.loading.appendTo(self.$('.openerp_webclient_container'));
1173 self.action_manager = new instance.web.ActionManager(self);
1174 self.action_manager.appendTo(self.$('.oe_application'));
1176 toggle_bars: function(value) {
1177 this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1179 has_uncommitted_changes: function() {
1184 instance.web.WebClient = instance.web.Client.extend({
1185 init: function(parent, client_options) {
1186 this._super(parent);
1187 if (client_options) {
1188 _.extend(this.client_options, client_options);
1190 this._current_state = null;
1191 this.menu_dm = new instance.web.DropMisordered();
1192 this.action_mutex = new $.Mutex();
1193 this.set('title_part', {"zopenerp": "Odoo"});
1197 this.on("change:title_part", this, this._title_changed);
1198 this._title_changed();
1200 return $.when(this._super()).then(function() {
1201 if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1204 if (self.session.session_is_valid()) {
1205 self.show_application();
1207 if (self.client_options.action) {
1208 self.action_manager.do_action(self.client_options.action);
1209 delete(self.client_options.action);
1213 to_kitten: function() {
1215 $("body").addClass("kitten-mode-activated");
1216 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1218 var imgkit = Math.floor(Math.random() * 2 + 1);
1219 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1223 Sets the first part of the title of the window, dedicated to the current action.
1225 set_title: function(title) {
1226 this.set_title_part("action", title);
1229 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1230 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1231 title of the window separated by '-'.
1233 set_title_part: function(part, title) {
1234 var tmp = _.clone(this.get("title_part"));
1236 this.set("title_part", tmp);
1238 _title_changed: function() {
1239 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1241 _.each(parts, function(part) {
1242 var str = this.get("title_part")[part];
1244 tmp = tmp ? tmp + " - " + str : str;
1247 document.title = tmp;
1249 show_common: function() {
1252 window.onerror = function (message, file, line) {
1253 self.crashmanager.show_error({
1254 type: _t("Client Error"),
1256 data: {debug: file + ':' + line}
1260 show_application: function() {
1262 self.toggle_bars(true);
1265 this.$('.oe_logo_edit_admin').click(function(ev) {
1269 // Menu is rendered server-side thus we don't want the widget to create any dom
1270 self.menu = new instance.web.Menu(self);
1271 self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
1273 self.menu.on('menu_click', this, this.on_menu_action);
1274 self.user_menu = new instance.web.UserMenu(self);
1275 self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
1276 self.user_menu.on('user_logout', self, self.on_logout);
1277 self.user_menu.do_update();
1278 self.bind_hashchange();
1280 self.check_timezone();
1281 if (self.client_options.action_post_login) {
1282 self.action_manager.do_action(self.client_options.action_post_login);
1283 delete(self.client_options.action_post_login);
1286 update_logo: function() {
1287 var img = this.session.url('/web/binary/company_logo');
1288 this.$('.oe_logo img').attr('src', '').attr('src', img);
1289 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1291 logo_edit: function(ev) {
1293 ev.preventDefault();
1294 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1295 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1296 result.res_id = res['company_id'][0];
1297 result.target = "new";
1298 result.views = [[false, 'form']];
1300 action_buttons: true,
1302 self.action_manager.do_action(result);
1303 var form = self.action_manager.dialog_widget.views.form.controller;
1304 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1305 form.on('record_saved', self, function() {
1306 self.action_manager.dialog_stop();
1313 check_timezone: function() {
1315 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1316 var user_offset = result[0]['tz_offset'];
1317 var offset = -(new Date().getTimezoneOffset());
1318 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1319 var browser_offset = (offset < 0) ? "-" : "+";
1320 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1321 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1322 if (browser_offset !== user_offset) {
1323 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1324 $icon.on('click', function() {
1325 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1326 user_timezone: instance.session.user_context.tz || 'UTC',
1327 user_offset: user_offset,
1328 browser_offset: browser_offset,
1330 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1331 notification.close();
1332 }).find('a').on('click', function() {
1333 notification.close();
1334 self.user_menu.on_menu_settings();
1338 $icon.prependTo(window.$('.oe_systray'));
1342 destroy_content: function() {
1343 _.each(_.clone(this.getChildren()), function(el) {
1346 this.$el.children().remove();
1348 do_reload: function() {
1350 return this.session.session_reload().then(function () {
1351 instance.session.load_modules(true).then(
1352 self.menu.proxy('do_reload')); });
1354 do_notify: function() {
1355 var n = this.notification;
1356 return n.notify.apply(n, arguments);
1358 do_warn: function() {
1359 var n = this.notification;
1360 return n.warn.apply(n, arguments);
1362 on_logout: function() {
1364 if (!this.has_uncommitted_changes()) {
1365 self.action_manager.do_action('logout');
1368 bind_hashchange: function() {
1370 $(window).bind('hashchange', this.on_hashchange);
1372 var state = $.bbq.getState(true);
1373 if (_.isEmpty(state) || state.action == "login") {
1374 self.menu.is_bound.done(function() {
1375 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1376 if(data.action_id) {
1377 self.action_manager.do_action(data.action_id[0]);
1378 self.menu.open_action(data.action_id[0]);
1380 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1382 self.menu.menu_click(first_menu_id);
1387 $(window).trigger('hashchange');
1390 on_hashchange: function(event) {
1392 var stringstate = event.getState(false);
1393 if (!_.isEqual(this._current_state, stringstate)) {
1394 var state = event.getState(true);
1395 if(!state.action && state.menu_id) {
1396 self.menu.is_bound.done(function() {
1397 self.menu.menu_click(state.menu_id);
1400 state._push_me = false; // no need to push state back...
1401 this.action_manager.do_load_state(state, !!this._current_state);
1404 this._current_state = stringstate;
1406 do_push_state: function(state) {
1407 this.set_title(state.title);
1409 var url = '#' + $.param(state);
1410 this._current_state = $.deparam($.param(state), false); // stringify all values
1411 $.bbq.pushState(url);
1412 this.trigger('state_pushed', state);
1414 on_menu_action: function(options) {
1416 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1417 .then(function (result) {
1418 return self.action_mutex.exec(function() {
1419 if (options.needaction) {
1420 result.context = new instance.web.CompoundContext(result.context, {
1421 search_default_message_unread: true,
1422 search_disable_custom_filters: true,
1425 var completed = $.Deferred();
1426 $.when(self.action_manager.do_action(result, {
1427 clear_breadcrumbs: true,
1428 action_menu_id: self.menu.current_menu,
1429 })).fail(function() {
1430 self.menu.open_menu(options.previous_menu_id);
1431 }).always(function() {
1432 completed.resolve();
1434 setTimeout(function() {
1435 completed.resolve();
1437 // We block the menu when clicking on an element until the action has correctly finished
1438 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1443 set_content_full_screen: function(fullscreen) {
1444 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1445 this.$('.oe_webclient').toggleClass(
1446 'oe_content_full_screen', fullscreen);
1448 has_uncommitted_changes: function() {
1449 var $e = $.Event('clear_uncommitted_changes');
1450 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1451 if ($e.isDefaultPrevented()) {
1454 return this._super.apply(this, arguments);
1459 instance.web.EmbeddedClient = instance.web.Client.extend({
1460 _template: 'EmbedClient',
1461 init: function(parent, origin, dbname, login, key, action_id, options) {
1462 this._super(parent, origin);
1463 this.bind_credentials(dbname, login, key);
1464 this.action_id = action_id;
1465 this.options = options || {};
1469 return $.when(this._super()).then(function() {
1470 return self.authenticate().then(function() {
1471 if (!self.action_id) {
1474 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1475 var action = result;
1476 action.flags = _.extend({
1477 //views_switcher : false,
1478 search_view : false,
1479 action_buttons : false,
1482 }, self.options, action.flags || {});
1484 self.do_action(action);
1490 do_action: function(/*...*/) {
1491 var am = this.action_manager;
1492 return am.do_action.apply(am, arguments);
1495 authenticate: function() {
1496 var s = instance.session;
1497 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1500 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1503 bind_credentials: function(dbname, login, key) {
1504 this.dbname = dbname;
1511 instance.web.embed = function (origin, dbname, login, key, action, options) {
1512 $('head').append($('<link>', {
1513 'rel': 'stylesheet',
1515 'href': origin +'/web/css/web.assets_webclient'
1517 var currentScript = document.currentScript;
1518 if (!currentScript) {
1519 var sc = document.getElementsByTagName('script');
1520 currentScript = sc[sc.length-1];
1522 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1523 client.insertAfter(currentScript);
1528 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: