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 if (!_.isFunction(fn)) {
131 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
132 $customButons.append($but);
133 $but.on('click', function(ev) {
134 fn.call(self.$el, ev);
139 Initializes the popup.
141 @return The result returned by start().
143 init_dialog: function() {
145 var options = _.extend({}, this.dialog_options);
146 options.title = options.title || this.dialog_title;
147 if (options.buttons) {
148 this._add_buttons(options.buttons);
149 delete(options.buttons);
151 this.renderElement();
153 this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
158 if (options.size !== 'large'){
159 var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
160 if (options.size === 'small'){
161 dialog_class_size.addClass('modal-sm');
165 this.$el.appendTo(this.$dialog_box.find(".modal-body"));
166 var $dialog_content = this.$dialog_box.find('.modal-content');
167 if (options.dialogClass){
168 $dialog_content.find(".modal-body").addClass(options.dialogClass);
170 $dialog_content.openerpClass();
172 this.$dialog_box.on('hidden.bs.modal', this, function() {
175 this.$dialog_box.modal('show');
177 this.dialog_inited = true;
178 var res = this.start();
182 Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
184 close: function(reason) {
185 if (this.dialog_inited && !this.__tmp_dialog_hiding) {
186 $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
187 this.trigger("closing", reason);
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;
195 _closing: function() {
196 if (this.__tmp_dialog_destroying)
198 if (this.dialog_options.destroy_on_close) {
199 this.__tmp_dialog_closing = true;
201 this.__tmp_dialog_closing = undefined;
205 Destroys the popup, also closes it.
207 destroy: function (reason) {
208 this.$buttons.remove();
210 _.each(this.getChildren(), function(el) {
213 if (! this.__tmp_dialog_closing) {
214 this.__tmp_dialog_destroying = true;
216 this.__tmp_dialog_destroying = undefined;
218 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
219 //we need this to put the instruction to remove modal from DOM at the end
220 //of the queue, otherwise it might already have been removed before the modal-backdrop
221 //is removed when pressing escape key
222 var $element = this.$dialog_box;
223 setTimeout(function () {
224 //remove modal from list of opened modal since we just destroy it
225 var modal_list_index = $.inArray($element, opened_modal);
226 if (modal_list_index > -1){
227 opened_modal.splice(modal_list_index,1)[0].remove();
229 if (opened_modal.length > 0){
230 //we still have other opened modal so we should focus it
231 opened_modal[opened_modal.length-1].focus();
239 instance.web.CrashManager = instance.web.Class.extend({
244 rpc_error: function(error) {
248 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
250 new (handler)(this, error).display();
253 if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
254 this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
257 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") {
258 this.show_warning(error);
260 this.show_error(error);
263 show_warning: function(error) {
267 if (error.data.exception_type === "except_osv") {
268 error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
270 new instance.web.Dialog(this, {
272 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
274 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
276 }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
278 show_error: function(error) {
283 buttons[_t("Ok")] = function() {
284 this.parents('.modal').modal('hide');
286 new instance.web.Dialog(this, {
287 title: "OpenERP " + _.str.capitalize(error.type),
289 }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
291 show_message: function(exception) {
293 type: _t("Client Error"),
301 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
303 instance.web.ExceptionHandler = {
305 @param parent The parent.
306 @param error The error object as returned by the JSON-RPC implementation.
308 init: function(parent, error) {},
310 Called to inform to display the widget, if necessary. A typical way would be to implement
311 this interface in a class extending instance.web.Dialog and simply display the dialog in this
314 display: function() {},
318 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
319 instance.web.ExceptionHandler.
321 instance.web.crash_manager_registry = new instance.web.Registry();
324 * Handle redirection warnings, which behave more or less like a regular
325 * warning, with an additional redirection button.
327 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
328 init: function(parent, error) {
332 display: function() {
334 error.data.message = error.data.arguments[0];
336 new instance.web.Dialog(this, {
338 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
340 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }},
341 {text: error.data.arguments[2], click: function() {
342 window.location.href='#action='+error.data.arguments[1];
343 this.$el.parents('.modal').modal('hide');
346 }, QWeb.render('CrashManager.warning', {error: error})).open();
350 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
352 instance.web.Loading = instance.web.Widget.extend({
353 template: _t("Loading"),
354 init: function(parent) {
357 this.blocked_ui = false;
358 this.session.on("request", this, this.request_call);
359 this.session.on("response", this, this.response_call);
360 this.session.on("response_failed", this, this.response_call);
362 destroy: function() {
363 this.on_rpc_event(-this.count);
366 request_call: function() {
367 this.on_rpc_event(1);
369 response_call: function() {
370 this.on_rpc_event(-1);
372 on_rpc_event : function(increment) {
374 if (!this.count && increment === 1) {
376 this.long_running_timer = setTimeout(function () {
377 self.blocked_ui = true;
378 instance.web.blockUI();
382 this.count += increment;
383 if (this.count > 0) {
384 if (instance.session.debug) {
385 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
387 this.$el.text(_t("Loading"));
390 this.getParent().$el.addClass('oe_wait');
393 clearTimeout(this.long_running_timer);
394 // Don't unblock if blocked by somebody else
395 if (self.blocked_ui) {
396 this.blocked_ui = false;
397 instance.web.unblockUI();
400 this.getParent().$el.removeClass('oe_wait');
405 instance.web.DatabaseManager = instance.web.Widget.extend({
406 init: function(parent) {
408 this.unblockUIFunction = instance.web.unblockUI;
409 $.validator.addMethod('matches', function (s, _, re) {
410 return new RegExp(re).test(s);
411 }, _t("Invalid database name"));
415 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
416 var fetch_db = this.rpc("/web/database/get_list", {}).then(
418 self.db_list = result;
424 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
425 self.lang_list = result;
427 return $.when(fetch_db, fetch_langs).always(self.do_render);
429 do_render: function() {
431 instance.webclient.toggle_bars(true);
432 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
433 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
434 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
435 $('ul.oe_secondary_submenu > li:first').addClass('active');
436 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
437 var menuitem = $(this);
438 menuitem.addClass('active').siblings().removeClass('active');
439 var form_id =menuitem.find('a').attr('href');
440 $(form_id).show().siblings().hide();
441 event.preventDefault();
443 $('#back-to-login').click(self.do_exit);
444 self.$el.find("td").addClass("oe_form_group_cell");
445 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
446 self.$el.find("label").addClass("oe_form_label");
447 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
448 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
449 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
450 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
451 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
452 self.$el.find("form[name=change_pwd_form]").validate({
454 old_pwd: _t("Please enter your previous password"),
455 new_pwd: _t("Please enter your new password"),
457 required: _t("Please confirm your new password"),
458 equalTo: _t("The confirmation does not match the password")
461 submitHandler: self.do_change_password
464 destroy: function () {
465 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
469 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
470 * from unblocking the UI
472 blockUI: function () {
473 instance.web.blockUI();
474 instance.web.unblockUI = function () {};
477 * Reinstates $.unblockUI so third parties can play with blockUI, and
480 unblockUI: function () {
481 instance.web.unblockUI = this.unblockUIFunction;
482 instance.web.unblockUI();
485 * Displays an error dialog resulting from the various RPC communications
486 * failing over themselves
488 * @param {Object} error error description
489 * @param {String} error.title title of the error dialog
490 * @param {String} error.error message of the error dialog
492 display_error: function (error) {
493 return new instance.web.Dialog(this, {
497 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
499 }, $('<div>').html(error.error)).open();
501 do_create: function(form) {
503 var fields = $(form).serializeArray();
504 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
506 instance.web.redirect('/web');
508 alert("Failed to create database");
512 do_duplicate: function(form) {
514 var fields = $(form).serializeArray();
515 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
517 self.display_error(result);
520 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
524 do_drop: function(form) {
527 fields = $form.serializeArray(),
528 $db_list = $form.find('[name=drop_db]'),
530 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
533 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
535 self.display_error(result);
538 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
542 do_backup: function(form) {
545 self.session.get_file({
547 success: function () {
548 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
550 error: function(error){
553 title: _t("Backup Database"),
554 error: 'AccessDenied'
558 complete: function() {
563 do_restore: function(form) {
567 url: '/web/database/restore',
570 success: function (body) {
571 // If empty body, everything went fine
572 if (!body) { return; }
574 if (body.indexOf('403 Forbidden') !== -1) {
576 title: _t("Access Denied"),
577 error: _t("Incorrect super-administrator password")
581 title: _t("Restore Database"),
582 error: _t("Could not restore the database")
586 complete: function() {
588 self.do_notify(_t("Restored"), _t("Database restored successfully"));
592 do_change_password: function(form) {
594 self.rpc("/web/database/change_password", {
595 'fields': $(form).serializeArray()
596 }).done(function(result) {
598 self.display_error(result);
602 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
605 do_exit: function () {
607 instance.web.redirect('/web');
610 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
612 instance.web.login = function() {
613 instance.web.redirect('/web/login');
615 instance.web.client_actions.add("login", "instance.web.login");
617 instance.web.logout = function() {
618 instance.web.redirect('/web/session/logout');
620 instance.web.client_actions.add("logout", "instance.web.logout");
624 * Redirect to url by replacing window.location
625 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
627 instance.web.redirect = function(url, wait) {
628 // Dont display a dialog if some xmlhttprequest are in progress
629 if (instance.client && instance.client.crashmanager) {
630 instance.client.crashmanager.active = false;
633 var load = function() {
634 var old = "" + window.location;
635 var old_no_hash = old.split("#")[0];
636 var url_no_hash = url.split("#")[0];
637 location.assign(url);
638 if (old_no_hash === url_no_hash) {
639 location.reload(true);
643 var wait_server = function() {
644 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
645 setTimeout(wait_server, 250);
650 setTimeout(wait_server, 1000);
657 * Client action to reload the whole interface.
658 * If params.menu_id, it opens the given menu entry.
659 * If params.wait, reload will wait the openerp server to be reachable before reloading
661 instance.web.Reload = function(parent, action) {
662 var params = action.params || {};
663 var menu_id = params.menu_id || false;
664 var l = window.location;
666 var sobj = $.deparam(l.search.substr(1));
667 if (params.url_search) {
668 sobj = _.extend(sobj, params.url_search);
670 var search = '?' + $.param(sobj);
674 hash = "#menu_id=" + menu_id;
676 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
678 instance.web.redirect(url, params.wait);
680 instance.web.client_actions.add("reload", "instance.web.Reload");
683 * Client action to refresh the session context (making sure
684 * HTTP requests will have the right one) then reload the
687 instance.web.ReloadContext = function(parent, action) {
688 // side-effect of get_session_info is to refresh the session context
689 instance.session.rpc("/web/session/get_session_info", {}).then(function() {
690 instance.web.Reload(parent, action);
693 instance.web.client_actions.add("reload_context", "instance.web.ReloadContext");
696 * Client action to go back in breadcrumb history.
697 * If can't go back in history stack, will go back to home.
699 instance.web.HistoryBack = function(parent) {
700 if (!parent.history_back()) {
701 instance.web.Home(parent);
704 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
707 * Client action to go back home.
709 instance.web.Home = function(parent, action) {
710 var url = '/' + (window.location.search || '');
711 instance.web.redirect(url, action && action.params && action.params.wait);
713 instance.web.client_actions.add("home", "instance.web.Home");
715 instance.web.ChangePassword = instance.web.Widget.extend({
716 template: "ChangePassword",
719 this.getParent().dialog_title = _t("Change Password");
720 var $button = self.$el.find('.oe_form_button');
721 $button.appendTo(this.getParent().$buttons);
722 $button.eq(2).click(function(){
723 self.$el.parents('.modal').modal('hide');
725 $button.eq(0).click(function(){
726 self.rpc("/web/session/change_password",{
727 'fields': $("form[name=change_password_form]").serializeArray()
728 }).done(function(result) {
730 self.display_error(result);
733 instance.webclient.on_logout();
738 display_error: function (error) {
739 return new instance.web.Dialog(this, {
743 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
745 }, $('<div>').html(error.error)).open();
748 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
750 instance.web.Menu = instance.web.Widget.extend({
753 this._super.apply(this, arguments);
754 this.is_bound = $.Deferred();
755 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
756 this.data = {data:{children:[]}};
757 this.on("menu_bound", this, function() {
758 // launch the fetch of needaction counters, asynchronous
759 var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
760 var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
761 if (!_.isEmpty(all_menu_ids)) {
762 this.do_load_needaction(all_menu_ids);
767 this._super.apply(this, arguments);
768 return this.bind_menu();
770 do_reload: function() {
774 bind_menu: function() {
776 this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container')
777 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
778 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
779 // Hide second level submenus
780 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
781 if (self.current_menu) {
782 self.open_menu(self.current_menu);
784 this.trigger('menu_bound');
786 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
787 instance.web.bus.on('resize', this, function() {
788 if (parseInt(self.$el.parent().css('width')) <= 768 ) {
789 lazyreflow('all_outside');
794 instance.web.bus.trigger('resize');
796 this.is_bound.resolve();
798 do_load_needaction: function (menu_ids) {
800 menu_ids = _.compact(menu_ids);
801 if (_.isEmpty(menu_ids)) {
804 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
805 self.on_needaction_loaded(r);
808 on_needaction_loaded: function(data) {
810 this.needaction_data = data;
811 _.each(this.needaction_data, function (item, menu_id) {
812 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
813 $item.find('.badge').remove();
814 if (item.needaction_counter && item.needaction_counter > 0) {
815 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
820 * Reflow the menu items and dock overflowing items into a "More" menu item.
821 * Automatically called when 'menu_bound' event is triggered and on window resizing.
823 * @param {string} behavior If set to 'all_outside', all the items are displayed. If set to
824 * 'all_inside', all the items are hidden under the more item. If not set, only the
825 * overflowing items are hidden.
827 reflow: function(behavior) {
829 var $more_container = this.$('#menu_more_container').hide();
830 var $more = this.$('#menu_more');
831 var $systray = this.$el.parents().find('.oe_systray');
833 $more.children('li').insertBefore($more_container); // Pull all the items out of the more menu
835 // 'all_outside' beahavior should display all the items, so hide the more menu and exit
836 if (behavior === 'all_outside') {
837 this.$el.find('li').show();
838 $more_container.hide();
842 var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
843 $toplevel_items.each(function() {
844 // In all inside mode, we do not compute to know if we must hide the items, we hide them all
845 if (behavior === 'all_inside') {
848 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
849 self.$el.parent().children(':visible').each(function() {
850 remaining_space -= $(this).outerWidth();
853 if ($(this).width() > remaining_space) {
858 $more.append($toplevel_items.filter(':hidden').show());
859 $more_container.toggle(!!$more.children().length || behavior === 'all_inside');
860 // Hide toplevel item if there is only one
861 var $toplevel = this.$el.children("li:visible");
862 if ($toplevel.length === 1 && behavior != 'all_inside') {
867 * Opens a given menu by id, as if a user had browsed to that menu by hand
868 * except does not trigger any event on the way
870 * @param {Number} id database id of the terminal menu to select
872 open_menu: function (id) {
873 this.current_menu = id;
874 this.session.active_id = id;
875 var $clicked_menu, $sub_menu, $main_menu;
876 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
877 this.trigger('open_menu', id, $clicked_menu);
879 if (this.$secondary_menus.has($clicked_menu).length) {
880 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
881 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
883 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
884 $main_menu = $clicked_menu;
887 // Activate current main menu
888 this.$el.find('.active').removeClass('active');
889 $main_menu.parent().addClass('active');
891 // Show current sub menu
892 this.$secondary_menus.find('.oe_secondary_menu').hide();
895 // Hide/Show the leftbar menu depending of the presence of sub-items
896 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
898 // Activate current menu item and show parents
899 this.$secondary_menus.find('.active').removeClass('active');
900 if ($main_menu !== $clicked_menu) {
901 $clicked_menu.parents().show();
902 if ($clicked_menu.is('.oe_menu_toggler')) {
903 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
905 $clicked_menu.parent().addClass('active');
908 // add a tooltip to cropped menu items
909 this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
910 $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'auto right'} :'destroy');
914 * Call open_menu with the first menu_item matching an action_id
916 * @param {Number} id the action_id to match
918 open_action: function (id) {
919 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
920 var menu_id = $menu.data('menu');
922 this.open_menu(menu_id);
926 * Process a click on a menu item
928 * @param {Number} id the menu_id
929 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
931 menu_click: function(id, needaction) {
934 // find back the menuitem in dom to get the action
935 var $item = this.$el.find('a[data-menu=' + id + ']');
937 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
939 var action_id = $item.data('action-id');
940 // If first level menu doesnt have action trigger first leaf
942 if(this.$el.has($item).length) {
943 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
944 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
946 action_id = $items.data('action-id');
947 id = $items.data('menu');
952 this.trigger('menu_click', {
953 action_id: action_id,
954 needaction: needaction,
956 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
961 do_reload_needaction: function () {
963 if (self.current_menu) {
964 self.do_load_needaction([self.current_menu]).then(function () {
965 self.trigger("need_action_reloaded");
970 * Jquery event handler for menu click
972 * @param {Event} ev the jquery event
974 on_top_menu_click: function(ev) {
977 var id = $(ev.currentTarget).data('menu');
979 // Fetch the menu leaves ids in order to check if they need a 'needaction'
980 var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
981 var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
982 var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
984 self.do_load_needaction(menu_ids).then(function () {
985 self.trigger("need_action_reloaded");
988 this.on_menu_click(ev);
990 on_menu_click: function(ev) {
992 var needaction = $(ev.target).is('div#menu_counter');
993 this.menu_click($(ev.currentTarget).data('menu'), needaction);
997 instance.web.UserMenu = instance.web.Widget.extend({
998 template: "UserMenu",
999 init: function(parent) {
1000 this._super(parent);
1001 this.update_promise = $.Deferred().resolve();
1005 this._super.apply(this, arguments);
1006 this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
1007 ev.preventDefault();
1008 var f = self['on_menu_' + $(this).data('menu')];
1013 this.$el.parent().show()
1015 do_update: function () {
1017 var fct = function() {
1018 var $avatar = self.$el.find('.oe_topbar_avatar');
1019 $avatar.attr('src', $avatar.data('default-src'));
1020 if (!self.session.uid)
1022 var func = new instance.web.Model("res.users").get_func("read");
1023 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1024 var topbar_name = res.name;
1025 if(instance.session.debug)
1026 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1027 if(res.company_id[0] > 1)
1028 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1029 self.$el.find('.oe_topbar_name').text(topbar_name);
1030 if (!instance.session.debug) {
1031 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1033 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1034 $avatar.attr('src', avatar_src);
1036 openerp.web.bus.trigger('resize'); // Re-trigger the reflow logic
1039 this.update_promise = this.update_promise.then(fct, fct);
1041 on_menu_help: function() {
1042 window.open('http://help.openerp.com', '_blank');
1044 on_menu_logout: function() {
1045 this.trigger('user_logout');
1047 on_menu_settings: function() {
1049 if (!this.getParent().has_uncommitted_changes()) {
1050 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1051 result.res_id = instance.session.uid;
1052 self.getParent().action_manager.do_action(result);
1056 on_menu_account: function() {
1058 if (!this.getParent().has_uncommitted_changes()) {
1059 var P = new instance.web.Model('ir.config_parameter');
1060 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1062 'd': instance.session.db,
1063 'u': window.location.protocol + '//' + window.location.host,
1066 response_type: 'token',
1067 client_id: dbuuid || '',
1068 state: JSON.stringify(state),
1071 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1075 on_menu_about: function() {
1077 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1078 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1079 $help.find('a.oe_activate_debug_mode').click(function (e) {
1081 window.location = $.param.querystring( window.location.href, 'debug');
1083 new instance.web.Dialog(this, {
1085 dialogClass: 'oe_act_window',
1092 instance.web.FullscreenWidget = instance.web.Widget.extend({
1094 * Widgets extending the FullscreenWidget will be displayed fullscreen,
1095 * and will have a fixed 1:1 zoom level on mobile devices.
1098 if(!$('#oe-fullscreenwidget-viewport').length){
1099 $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1101 instance.webclient.set_content_full_screen(true);
1102 return this._super();
1104 destroy: function(){
1105 instance.webclient.set_content_full_screen(false);
1106 $('#oe-fullscreenwidget-viewport').remove();
1107 return this._super();
1112 instance.web.Client = instance.web.Widget.extend({
1113 init: function(parent, origin) {
1114 instance.client = instance.webclient = this;
1115 this.client_options = {};
1116 this._super(parent);
1117 this.origin = origin;
1121 return instance.session.session_bind(this.origin).then(function() {
1123 return self.show_common();
1126 bind_events: function() {
1128 $('.oe_systray').show();
1129 this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1130 $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1132 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1133 ev.preventDefault();
1134 var $toggle = $(this);
1135 var doc_width = $(document).width();
1136 var $menu = $toggle.siblings('.oe_dropdown_menu');
1137 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1138 var state = $menu.is('.oe_opened');
1139 setTimeout(function() {
1140 // Do not alter propagation
1141 $toggle.add($menu).toggleClass('oe_opened', !state);
1143 // Move $menu if outside window's edge
1144 var offset = $menu.offset();
1145 var menu_width = $menu.width();
1146 var x = doc_width - offset.left - menu_width - 2;
1148 $menu.offset({ left: offset.left + x }).width(menu_width);
1153 instance.web.bus.on('click', this, function(ev) {
1154 $.fn.tooltip('destroy');
1155 if (!$(ev.target).is('input[type=file]')) {
1156 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1160 show_common: function() {
1162 this.crashmanager = new instance.web.CrashManager();
1163 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1164 self.notification = new instance.web.Notification(this);
1165 self.notification.appendTo(self.$el);
1166 self.loading = new instance.web.Loading(self);
1167 self.loading.appendTo(self.$('.openerp_webclient_container'));
1168 self.action_manager = new instance.web.ActionManager(self);
1169 self.action_manager.appendTo(self.$('.oe_application'));
1171 toggle_bars: function(value) {
1172 this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1174 has_uncommitted_changes: function() {
1179 instance.web.WebClient = instance.web.Client.extend({
1180 init: function(parent, client_options) {
1181 this._super(parent);
1182 if (client_options) {
1183 _.extend(this.client_options, client_options);
1185 this._current_state = null;
1186 this.menu_dm = new instance.web.DropMisordered();
1187 this.action_mutex = new $.Mutex();
1188 this.set('title_part', {"zopenerp": "OpenERP"});
1192 this.on("change:title_part", this, this._title_changed);
1193 this._title_changed();
1195 return $.when(this._super()).then(function() {
1196 if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1199 if (self.session.session_is_valid()) {
1200 self.show_application();
1202 if (self.client_options.action) {
1203 self.action_manager.do_action(self.client_options.action);
1204 delete(self.client_options.action);
1208 to_kitten: function() {
1210 $("body").addClass("kitten-mode-activated");
1211 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1213 var imgkit = Math.floor(Math.random() * 2 + 1);
1214 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1218 Sets the first part of the title of the window, dedicated to the current action.
1220 set_title: function(title) {
1221 this.set_title_part("action", title);
1224 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1225 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1226 title of the window separated by '-'.
1228 set_title_part: function(part, title) {
1229 var tmp = _.clone(this.get("title_part"));
1231 this.set("title_part", tmp);
1233 _title_changed: function() {
1234 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1236 _.each(parts, function(part) {
1237 var str = this.get("title_part")[part];
1239 tmp = tmp ? tmp + " - " + str : str;
1242 document.title = tmp;
1244 show_common: function() {
1247 window.onerror = function (message, file, line) {
1248 self.crashmanager.show_error({
1249 type: _t("Client Error"),
1251 data: {debug: file + ':' + line}
1255 show_application: function() {
1257 self.toggle_bars(true);
1260 this.$('.oe_logo_edit_admin').click(function(ev) {
1264 // Menu is rendered server-side thus we don't want the widget to create any dom
1265 self.menu = new instance.web.Menu(self);
1266 self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
1268 self.menu.on('menu_click', this, this.on_menu_action);
1269 self.user_menu = new instance.web.UserMenu(self);
1270 self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
1271 self.user_menu.on('user_logout', self, self.on_logout);
1272 self.user_menu.do_update();
1273 self.bind_hashchange();
1275 self.check_timezone();
1276 if (self.client_options.action_post_login) {
1277 self.action_manager.do_action(self.client_options.action_post_login);
1278 delete(self.client_options.action_post_login);
1281 update_logo: function() {
1282 var img = this.session.url('/web/binary/company_logo');
1283 this.$('.oe_logo img').attr('src', '').attr('src', img);
1284 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1286 logo_edit: function(ev) {
1288 ev.preventDefault();
1289 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1290 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1291 result.res_id = res['company_id'][0];
1292 result.target = "new";
1293 result.views = [[false, 'form']];
1295 action_buttons: true,
1297 self.action_manager.do_action(result);
1298 var form = self.action_manager.dialog_widget.views.form.controller;
1299 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1300 form.on('record_saved', self, function() {
1301 self.action_manager.dialog_stop();
1308 check_timezone: function() {
1310 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1311 var user_offset = result[0]['tz_offset'];
1312 var offset = -(new Date().getTimezoneOffset());
1313 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1314 var browser_offset = (offset < 0) ? "-" : "+";
1315 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1316 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1317 if (browser_offset !== user_offset) {
1318 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1319 $icon.on('click', function() {
1320 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1321 user_timezone: instance.session.user_context.tz || 'UTC',
1322 user_offset: user_offset,
1323 browser_offset: browser_offset,
1325 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1326 notification.close();
1327 }).find('a').on('click', function() {
1328 notification.close();
1329 self.user_menu.on_menu_settings();
1333 $icon.prependTo(window.$('.oe_systray'));
1337 destroy_content: function() {
1338 _.each(_.clone(this.getChildren()), function(el) {
1341 this.$el.children().remove();
1343 do_reload: function() {
1345 return this.session.session_reload().then(function () {
1346 instance.session.load_modules(true).then(
1347 self.menu.proxy('do_reload')); });
1349 do_notify: function() {
1350 var n = this.notification;
1351 return n.notify.apply(n, arguments);
1353 do_warn: function() {
1354 var n = this.notification;
1355 return n.warn.apply(n, arguments);
1357 on_logout: function() {
1359 if (!this.has_uncommitted_changes()) {
1360 self.action_manager.do_action('logout');
1363 bind_hashchange: function() {
1365 $(window).bind('hashchange', this.on_hashchange);
1367 var state = $.bbq.getState(true);
1368 if (_.isEmpty(state) || state.action == "login") {
1369 self.menu.is_bound.done(function() {
1370 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1371 if(data.action_id) {
1372 self.action_manager.do_action(data.action_id[0]);
1373 self.menu.open_action(data.action_id[0]);
1375 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1377 self.menu.menu_click(first_menu_id);
1383 $(window).trigger('hashchange');
1386 on_hashchange: function(event) {
1388 var stringstate = event.getState(false);
1389 if (!_.isEqual(this._current_state, stringstate)) {
1390 var state = event.getState(true);
1391 if(!state.action && state.menu_id) {
1392 self.menu.is_bound.done(function() {
1393 self.menu.menu_click(state.menu_id);
1396 state._push_me = false; // no need to push state back...
1397 this.action_manager.do_load_state(state, !!this._current_state);
1400 this._current_state = stringstate;
1402 do_push_state: function(state) {
1403 this.set_title(state.title);
1405 var url = '#' + $.param(state);
1406 this._current_state = $.deparam($.param(state), false); // stringify all values
1407 $.bbq.pushState(url);
1408 this.trigger('state_pushed', state);
1410 on_menu_action: function(options) {
1412 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1413 .then(function (result) {
1414 return self.action_mutex.exec(function() {
1415 if (options.needaction) {
1416 result.context = new instance.web.CompoundContext(result.context, {
1417 search_default_message_unread: true,
1418 search_disable_custom_filters: true,
1421 var completed = $.Deferred();
1422 $.when(self.action_manager.do_action(result, {
1423 clear_breadcrumbs: true,
1424 action_menu_id: self.menu.current_menu,
1425 })).fail(function() {
1426 self.menu.open_menu(options.previous_menu_id);
1427 }).always(function() {
1428 completed.resolve();
1430 setTimeout(function() {
1431 completed.resolve();
1433 // We block the menu when clicking on an element until the action has correctly finished
1434 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1439 set_content_full_screen: function(fullscreen) {
1440 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1441 this.$('.oe_webclient').toggleClass(
1442 'oe_content_full_screen', fullscreen);
1444 has_uncommitted_changes: function() {
1445 var $e = $.Event('clear_uncommitted_changes');
1446 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1447 if ($e.isDefaultPrevented()) {
1450 return this._super.apply(this, arguments);
1455 instance.web.EmbeddedClient = instance.web.Client.extend({
1456 _template: 'EmbedClient',
1457 init: function(parent, origin, dbname, login, key, action_id, options) {
1458 this._super(parent, origin);
1459 this.bind_credentials(dbname, login, key);
1460 this.action_id = action_id;
1461 this.options = options || {};
1465 return $.when(this._super()).then(function() {
1466 return self.authenticate().then(function() {
1467 if (!self.action_id) {
1470 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1471 var action = result;
1472 action.flags = _.extend({
1473 //views_switcher : false,
1474 search_view : false,
1475 action_buttons : false,
1478 }, self.options, action.flags || {});
1480 self.do_action(action);
1486 do_action: function(/*...*/) {
1487 var am = this.action_manager;
1488 return am.do_action.apply(am, arguments);
1491 authenticate: function() {
1492 var s = instance.session;
1493 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1496 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1499 bind_credentials: function(dbname, login, key) {
1500 this.dbname = dbname;
1507 instance.web.embed = function (origin, dbname, login, key, action, options) {
1508 $('head').append($('<link>', {
1509 'rel': 'stylesheet',
1511 'href': origin +'/web/css/web.assets_webclient'
1513 var currentScript = document.currentScript;
1514 if (!currentScript) {
1515 var sc = document.getElementsByTagName('script');
1516 currentScript = sc[sc.length-1];
1518 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1519 client.insertAfter(currentScript);
1524 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: