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 $('.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);
123 _add_buttons: function(buttons) {
125 var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
126 _.each(buttons, function(fn, text) {
127 // buttons can be object or array
128 var oe_link_class = fn.oe_link_class;
129 if (!_.isFunction(fn)) {
133 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {'class': oe_link_class} }}}));
134 $customButons.append($but);
135 $but.on('click', function(ev) {
136 fn.call(self.$el, ev);
141 Initializes the popup.
143 @return The result returned by start().
145 init_dialog: function() {
147 var options = _.extend({}, this.dialog_options);
148 options.title = options.title || this.dialog_title;
149 if (options.buttons) {
150 this._add_buttons(options.buttons);
151 delete(options.buttons);
153 this.renderElement();
155 this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
160 if (options.size !== 'large'){
161 var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg');
162 if (options.size === 'small'){
163 dialog_class_size.addClass('modal-sm');
167 this.$el.appendTo(this.$dialog_box.find(".modal-body"));
168 var $dialog_content = this.$dialog_box.find('.modal-content');
169 if (options.dialogClass){
170 $dialog_content.find(".modal-body").addClass(options.dialogClass);
172 $dialog_content.openerpClass();
174 this.$dialog_box.on('hidden.bs.modal', this, function() {
177 this.$dialog_box.modal('show');
179 this.dialog_inited = true;
180 var res = this.start();
184 Closes (hide) the popup, if destroy_on_close was passed to the constructor, it will be destroyed instead.
186 close: function(reason) {
187 if (this.dialog_inited && !this.__tmp_dialog_hiding) {
188 $('.tooltip').remove(); //remove open tooltip if any to prevent them staying when modal has disappeared
189 if (this.$el.is(":data(bs.modal)")) { // may have been destroyed by closing signal
190 this.__tmp_dialog_hiding = true;
191 this.$dialog_box.modal('hide');
192 this.__tmp_dialog_hiding = undefined;
194 this.trigger("closing", reason);
197 _closing: function() {
198 if (this.__tmp_dialog_destroying)
200 if (this.dialog_options.destroy_on_close) {
201 this.__tmp_dialog_closing = true;
203 this.__tmp_dialog_closing = undefined;
207 Destroys the popup, also closes it.
209 destroy: function (reason) {
210 this.$buttons.remove();
212 _.each(this.getChildren(), function(el) {
215 if (! this.__tmp_dialog_closing) {
216 this.__tmp_dialog_destroying = true;
218 this.__tmp_dialog_destroying = undefined;
220 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
221 //we need this to put the instruction to remove modal from DOM at the end
222 //of the queue, otherwise it might already have been removed before the modal-backdrop
223 //is removed when pressing escape key
224 var $element = this.$dialog_box;
225 setTimeout(function () {
226 //remove modal from list of opened modal since we just destroy it
227 var modal_list_index = $.inArray($element, opened_modal);
228 if (modal_list_index > -1){
229 opened_modal.splice(modal_list_index,1)[0].remove();
231 if (opened_modal.length > 0){
232 //we still have other opened modal so we should focus it
233 opened_modal[opened_modal.length-1].focus();
234 //keep class modal-open (deleted by bootstrap hide fnct) on body
235 //to allow scrolling inside the modal
236 $('body').addClass('modal-open');
244 instance.web.CrashManager = instance.web.Class.extend({
249 rpc_error: function(error) {
253 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
255 new (handler)(this, error).display();
258 if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
259 this.show_warning({type: "Session Expired", data: { message: _t("Your Odoo session expired. Please refresh the current web page.") }});
262 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning" || error.data.exception_type === "access_error") {
263 this.show_warning(error);
265 this.show_error(error);
268 show_warning: function(error) {
272 if (error.data.exception_type === "except_osv") {
273 error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
275 new instance.web.Dialog(this, {
277 title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
279 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
281 }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
283 show_error: function(error) {
288 buttons[_t("Ok")] = function() {
289 this.parents('.modal').modal('hide');
291 new instance.web.Dialog(this, {
292 title: "Odoo " + _.str.capitalize(error.type),
294 }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
296 show_message: function(exception) {
298 type: _t("Client Error"),
306 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
308 instance.web.ExceptionHandler = {
310 @param parent The parent.
311 @param error The error object as returned by the JSON-RPC implementation.
313 init: function(parent, error) {},
315 Called to inform to display the widget, if necessary. A typical way would be to implement
316 this interface in a class extending instance.web.Dialog and simply display the dialog in this
319 display: function() {},
323 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
324 instance.web.ExceptionHandler.
326 instance.web.crash_manager_registry = new instance.web.Registry();
329 * Handle redirection warnings, which behave more or less like a regular
330 * warning, with an additional redirection button.
332 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
333 init: function(parent, error) {
337 display: function() {
340 error.data.message = error.data.arguments[0];
342 new instance.web.Dialog(this, {
344 title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
346 {text: _t("Ok"), click: function() { self.$el.parents('.modal').modal('hide'); self.destroy();}},
347 {text: error.data.arguments[2],
348 oe_link_class: 'oe_link',
350 window.location.href='#action='+error.data.arguments[1];
354 }, QWeb.render('CrashManager.warning', {error: error})).open();
357 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
359 instance.web.Loading = instance.web.Widget.extend({
360 template: _t("Loading"),
361 init: function(parent) {
364 this.blocked_ui = false;
365 this.session.on("request", this, this.request_call);
366 this.session.on("response", this, this.response_call);
367 this.session.on("response_failed", this, this.response_call);
369 destroy: function() {
370 this.on_rpc_event(-this.count);
373 request_call: function() {
374 this.on_rpc_event(1);
376 response_call: function() {
377 this.on_rpc_event(-1);
379 on_rpc_event : function(increment) {
381 if (!this.count && increment === 1) {
383 this.long_running_timer = setTimeout(function () {
384 self.blocked_ui = true;
385 instance.web.blockUI();
389 this.count += increment;
390 if (this.count > 0) {
391 if (instance.session.debug) {
392 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
394 this.$el.text(_t("Loading"));
397 this.getParent().$el.addClass('oe_wait');
400 clearTimeout(this.long_running_timer);
401 // Don't unblock if blocked by somebody else
402 if (self.blocked_ui) {
403 this.blocked_ui = false;
404 instance.web.unblockUI();
407 this.getParent().$el.removeClass('oe_wait');
412 instance.web.DatabaseManager = instance.web.Widget.extend({
413 init: function(parent) {
415 this.unblockUIFunction = instance.web.unblockUI;
416 $.validator.addMethod('matches', function (s, _, re) {
417 return new RegExp(re).test(s);
418 }, _t("Invalid database name"));
422 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
423 var fetch_db = this.rpc("/web/database/get_list", {}).then(
425 self.db_list = result;
431 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
432 self.lang_list = result;
434 return $.when(fetch_db, fetch_langs).always(self.do_render);
436 do_render: function() {
438 instance.webclient.toggle_bars(true);
439 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
440 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
441 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
442 $('ul.oe_secondary_submenu > li:first').addClass('active');
443 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
444 var menuitem = $(this);
445 menuitem.addClass('active').siblings().removeClass('active');
446 var form_id =menuitem.find('a').attr('href');
447 $(form_id).show().siblings().hide();
448 event.preventDefault();
450 $('#back-to-login').click(self.do_exit);
451 self.$el.find("td").addClass("oe_form_group_cell");
452 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
453 self.$el.find("label").addClass("oe_form_label");
454 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
455 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
456 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
457 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
458 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
459 self.$el.find("form[name=change_pwd_form]").validate({
461 old_pwd: _t("Please enter your previous password"),
462 new_pwd: _t("Please enter your new password"),
464 required: _t("Please confirm your new password"),
465 equalTo: _t("The confirmation does not match the password")
468 submitHandler: self.do_change_password
471 destroy: function () {
472 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
476 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
477 * from unblocking the UI
479 blockUI: function () {
480 instance.web.blockUI();
481 instance.web.unblockUI = function () {};
484 * Reinstates $.unblockUI so third parties can play with blockUI, and
487 unblockUI: function () {
488 instance.web.unblockUI = this.unblockUIFunction;
489 instance.web.unblockUI();
492 * Displays an error dialog resulting from the various RPC communications
493 * failing over themselves
495 * @param {Object} error error description
496 * @param {String} error.title title of the error dialog
497 * @param {String} error.error message of the error dialog
499 display_error: function (error) {
500 return new instance.web.Dialog(this, {
504 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
506 }, $('<div>').html(error.error)).open();
508 do_create: function(form) {
510 var fields = $(form).serializeArray();
511 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
513 instance.web.redirect('/web');
515 alert("Failed to create database");
519 do_duplicate: function(form) {
521 var fields = $(form).serializeArray();
522 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
524 self.display_error(result);
527 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
531 do_drop: function(form) {
534 fields = $form.serializeArray(),
535 $db_list = $form.find('[name=drop_db]'),
537 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
540 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
542 self.display_error(result);
545 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
549 do_backup: function(form) {
552 self.session.get_file({
554 success: function () {
555 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
557 error: function(error){
560 title: _t("Backup Database"),
561 error: 'AccessDenied'
565 complete: function() {
570 do_restore: function(form) {
574 url: '/web/database/restore',
577 success: function (body) {
578 // If empty body, everything went fine
579 if (!body) { return; }
581 if (body.indexOf('403 Forbidden') !== -1) {
583 title: _t("Access Denied"),
584 error: _t("Incorrect super-administrator password")
588 title: _t("Restore Database"),
589 error: _t("Could not restore the database")
593 complete: function() {
595 self.do_notify(_t("Restored"), _t("Database restored successfully"));
599 do_change_password: function(form) {
601 self.rpc("/web/database/change_password", {
602 'fields': $(form).serializeArray()
603 }).done(function(result) {
605 self.display_error(result);
609 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
612 do_exit: function () {
614 instance.web.redirect('/web');
617 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
619 instance.web.login = function() {
620 instance.web.redirect('/web/login');
622 instance.web.client_actions.add("login", "instance.web.login");
624 instance.web.logout = function() {
625 instance.web.redirect('/web/session/logout');
627 instance.web.client_actions.add("logout", "instance.web.logout");
631 * Redirect to url by replacing window.location
632 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
634 instance.web.redirect = function(url, wait) {
635 // Dont display a dialog if some xmlhttprequest are in progress
636 if (instance.client && instance.client.crashmanager) {
637 instance.client.crashmanager.active = false;
640 var load = function() {
641 var old = "" + window.location;
642 var old_no_hash = old.split("#")[0];
643 var url_no_hash = url.split("#")[0];
644 location.assign(url);
645 if (old_no_hash === url_no_hash) {
646 location.reload(true);
650 var wait_server = function() {
651 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
652 setTimeout(wait_server, 250);
657 setTimeout(wait_server, 1000);
664 * Client action to reload the whole interface.
665 * If params.menu_id, it opens the given menu entry.
666 * If params.wait, reload will wait the openerp server to be reachable before reloading
668 instance.web.Reload = function(parent, action) {
669 var params = action.params || {};
670 var menu_id = params.menu_id || false;
671 var l = window.location;
673 var sobj = $.deparam(l.search.substr(1));
674 if (params.url_search) {
675 sobj = _.extend(sobj, params.url_search);
677 var search = '?' + $.param(sobj);
681 hash = "#menu_id=" + menu_id;
683 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
685 instance.web.redirect(url, params.wait);
687 instance.web.client_actions.add("reload", "instance.web.Reload");
690 * Client action to refresh the session context (making sure
691 * HTTP requests will have the right one) then reload the
694 instance.web.ReloadContext = function(parent, action) {
695 // side-effect of get_session_info is to refresh the session context
696 instance.session.rpc("/web/session/get_session_info", {}).then(function() {
697 instance.web.Reload(parent, action);
700 instance.web.client_actions.add("reload_context", "instance.web.ReloadContext");
703 * Client action to go back in breadcrumb history.
704 * If can't go back in history stack, will go back to home.
706 instance.web.HistoryBack = function(parent) {
707 if (!parent.history_back()) {
708 instance.web.Home(parent);
711 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
714 * Client action to go back home.
716 instance.web.Home = function(parent, action) {
717 var url = '/' + (window.location.search || '');
718 instance.web.redirect(url, action && action.params && action.params.wait);
720 instance.web.client_actions.add("home", "instance.web.Home");
722 instance.web.ChangePassword = instance.web.Widget.extend({
723 template: "ChangePassword",
726 this.getParent().dialog_title = _t("Change Password");
727 var $button = self.$el.find('.oe_form_button');
728 $button.appendTo(this.getParent().$buttons);
729 $button.eq(2).click(function(){
730 self.$el.parents('.modal').modal('hide');
732 $button.eq(0).click(function(){
733 self.rpc("/web/session/change_password",{
734 'fields': $("form[name=change_password_form]").serializeArray()
735 }).done(function(result) {
737 self.display_error(result);
740 instance.webclient.on_logout();
745 display_error: function (error) {
746 return new instance.web.Dialog(this, {
750 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
752 }, $('<div>').html(error.error)).open();
755 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
757 instance.web.Menu = instance.web.Widget.extend({
760 this._super.apply(this, arguments);
761 this.is_bound = $.Deferred();
762 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
763 this.data = {data:{children:[]}};
764 this.on("menu_bound", this, function() {
765 // launch the fetch of needaction counters, asynchronous
766 var $all_menus = self.$el.parents('body').find('.oe_webclient').find('[data-menu]');
767 var all_menu_ids = _.map($all_menus, function (menu) {return parseInt($(menu).attr('data-menu'), 10);});
768 if (!_.isEmpty(all_menu_ids)) {
769 this.do_load_needaction(all_menu_ids);
774 this._super.apply(this, arguments);
775 return this.bind_menu();
777 do_reload: function() {
781 bind_menu: function() {
783 this.$secondary_menus = this.$el.parents().find('.oe_secondary_menus_container')
784 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
785 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
786 // Hide second level submenus
787 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
788 if (self.current_menu) {
789 self.open_menu(self.current_menu);
791 this.trigger('menu_bound');
793 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
794 instance.web.bus.on('resize', this, function() {
795 if (parseInt(self.$el.parent().css('width')) <= 768 ) {
796 lazyreflow('all_outside');
801 instance.web.bus.trigger('resize');
803 this.is_bound.resolve();
805 do_load_needaction: function (menu_ids) {
807 menu_ids = _.compact(menu_ids);
808 if (_.isEmpty(menu_ids)) {
811 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
812 self.on_needaction_loaded(r);
815 on_needaction_loaded: function(data) {
817 this.needaction_data = data;
818 _.each(this.needaction_data, function (item, menu_id) {
819 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
820 $item.find('.badge').remove();
821 if (item.needaction_counter && item.needaction_counter > 0) {
822 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
827 * Reflow the menu items and dock overflowing items into a "More" menu item.
828 * Automatically called when 'menu_bound' event is triggered and on window resizing.
830 * @param {string} behavior If set to 'all_outside', all the items are displayed. If set to
831 * 'all_inside', all the items are hidden under the more item. If not set, only the
832 * overflowing items are hidden.
834 reflow: function(behavior) {
836 var $more_container = this.$('#menu_more_container').hide();
837 var $more = this.$('#menu_more');
838 var $systray = this.$el.parents().find('.oe_systray');
840 $more.children('li').insertBefore($more_container); // Pull all the items out of the more menu
842 // 'all_outside' beahavior should display all the items, so hide the more menu and exit
843 if (behavior === 'all_outside') {
844 this.$el.find('li').show();
845 $more_container.hide();
849 var $toplevel_items = this.$el.find('li').not($more_container).not($systray.find('li')).hide();
850 $toplevel_items.each(function() {
851 // In all inside mode, we do not compute to know if we must hide the items, we hide them all
852 if (behavior === 'all_inside') {
855 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
856 self.$el.parent().children(':visible').each(function() {
857 remaining_space -= $(this).outerWidth();
860 if ($(this).width() > remaining_space) {
865 $more.append($toplevel_items.filter(':hidden').show());
866 $more_container.toggle(!!$more.children().length || behavior === 'all_inside');
867 // Hide toplevel item if there is only one
868 var $toplevel = this.$el.children("li:visible");
869 if ($toplevel.length === 1 && behavior != 'all_inside') {
874 * Opens a given menu by id, as if a user had browsed to that menu by hand
875 * except does not trigger any event on the way
877 * @param {Number} id database id of the terminal menu to select
879 open_menu: function (id) {
880 this.current_menu = id;
881 this.session.active_id = id;
882 var $clicked_menu, $sub_menu, $main_menu;
883 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
884 this.trigger('open_menu', id, $clicked_menu);
886 if (this.$secondary_menus.has($clicked_menu).length) {
887 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
888 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
890 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
891 $main_menu = $clicked_menu;
894 // Activate current main menu
895 this.$el.find('.active').removeClass('active');
896 $main_menu.parent().addClass('active');
898 // Show current sub menu
899 this.$secondary_menus.find('.oe_secondary_menu').hide();
902 // Hide/Show the leftbar menu depending of the presence of sub-items
903 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
905 // Activate current menu item and show parents
906 this.$secondary_menus.find('.active').removeClass('active');
907 if ($main_menu !== $clicked_menu) {
908 $clicked_menu.parents().show();
909 if ($clicked_menu.is('.oe_menu_toggler')) {
910 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
912 $clicked_menu.parent().addClass('active');
915 // add a tooltip to cropped menu items
916 this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
917 $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
921 * Call open_menu with the first menu_item matching an action_id
923 * @param {Number} id the action_id to match
925 open_action: function (id) {
926 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
927 var menu_id = $menu.data('menu');
929 this.open_menu(menu_id);
933 * Process a click on a menu item
935 * @param {Number} id the menu_id
936 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
938 menu_click: function(id, needaction) {
941 // find back the menuitem in dom to get the action
942 var $item = this.$el.find('a[data-menu=' + id + ']');
944 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
946 var action_id = $item.data('action-id');
947 // If first level menu doesnt have action trigger first leaf
949 if(this.$el.has($item).length) {
950 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
951 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
953 action_id = $items.data('action-id');
954 id = $items.data('menu');
959 this.trigger('menu_click', {
960 action_id: action_id,
961 needaction: needaction,
963 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
966 console.log('Menu no action found web test 04 will fail');
970 do_reload_needaction: function () {
972 if (self.current_menu) {
973 self.do_load_needaction([self.current_menu]).then(function () {
974 self.trigger("need_action_reloaded");
979 * Jquery event handler for menu click
981 * @param {Event} ev the jquery event
983 on_top_menu_click: function(ev) {
986 var id = $(ev.currentTarget).data('menu');
988 // Fetch the menu leaves ids in order to check if they need a 'needaction'
989 var $secondary_menu = this.$el.parents().find('.oe_secondary_menu[data-menu-parent=' + id + ']');
990 var $menu_leaves = $secondary_menu.children().find('.oe_menu_leaf');
991 var menu_ids = _.map($menu_leaves, function (leave) {return parseInt($(leave).attr('data-menu'), 10);});
993 self.do_load_needaction(menu_ids).then(function () {
994 self.trigger("need_action_reloaded");
996 this.$el.parents().find(".oe_secondary_menus_container").scrollTop(0,0);
998 this.on_menu_click(ev);
1000 on_menu_click: function(ev) {
1001 ev.preventDefault();
1002 var needaction = $(ev.target).is('div#menu_counter');
1003 this.menu_click($(ev.currentTarget).data('menu'), needaction);
1007 instance.web.UserMenu = instance.web.Widget.extend({
1008 template: "UserMenu",
1009 init: function(parent) {
1010 this._super(parent);
1011 this.update_promise = $.Deferred().resolve();
1015 this._super.apply(this, arguments);
1016 this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
1017 ev.preventDefault();
1018 var f = self['on_menu_' + $(this).data('menu')];
1023 this.$el.parent().show()
1025 do_update: function () {
1027 var fct = function() {
1028 var $avatar = self.$el.find('.oe_topbar_avatar');
1029 $avatar.attr('src', $avatar.data('default-src'));
1030 if (!self.session.uid)
1032 var func = new instance.web.Model("res.users").get_func("read");
1033 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1034 var topbar_name = res.name;
1035 if(instance.session.debug)
1036 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1037 if(res.company_id[0] > 1)
1038 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1039 self.$el.find('.oe_topbar_name').text(topbar_name);
1040 if (!instance.session.debug) {
1041 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1043 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1044 $avatar.attr('src', avatar_src);
1046 openerp.web.bus.trigger('resize'); // Re-trigger the reflow logic
1049 this.update_promise = this.update_promise.then(fct, fct);
1051 on_menu_help: function() {
1052 window.open('http://help.odoo.com', '_blank');
1054 on_menu_logout: function() {
1055 this.trigger('user_logout');
1057 on_menu_settings: function() {
1059 if (!this.getParent().has_uncommitted_changes()) {
1060 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1061 result.res_id = instance.session.uid;
1062 self.getParent().action_manager.do_action(result);
1066 on_menu_account: function() {
1068 if (!this.getParent().has_uncommitted_changes()) {
1069 var P = new instance.web.Model('ir.config_parameter');
1070 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1072 'd': instance.session.db,
1073 'u': window.location.protocol + '//' + window.location.host,
1076 response_type: 'token',
1077 client_id: dbuuid || '',
1078 state: JSON.stringify(state),
1081 instance.web.redirect('https://accounts.odoo.com/oauth2/auth?'+$.param(params));
1082 }).fail(function(result, ev){
1083 ev.preventDefault();
1084 instance.web.redirect('https://accounts.odoo.com/web');
1088 on_menu_about: function() {
1090 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1091 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1092 $help.find('a.oe_activate_debug_mode').click(function (e) {
1094 window.location = $.param.querystring( window.location.href, 'debug');
1096 new instance.web.Dialog(this, {
1098 dialogClass: 'oe_act_window',
1105 instance.web.FullscreenWidget = instance.web.Widget.extend({
1107 * Widgets extending the FullscreenWidget will be displayed fullscreen,
1108 * and will have a fixed 1:1 zoom level on mobile devices.
1111 if(!$('#oe-fullscreenwidget-viewport').length){
1112 $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1114 instance.webclient.set_content_full_screen(true);
1115 return this._super();
1117 destroy: function(){
1118 instance.webclient.set_content_full_screen(false);
1119 $('#oe-fullscreenwidget-viewport').remove();
1120 return this._super();
1125 instance.web.Client = instance.web.Widget.extend({
1126 init: function(parent, origin) {
1127 instance.client = instance.webclient = this;
1128 this.client_options = {};
1129 this._super(parent);
1130 this.origin = origin;
1134 return instance.session.session_bind(this.origin).then(function() {
1136 return self.show_common();
1139 bind_events: function() {
1141 $('.oe_systray').show();
1142 this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1143 $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1145 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1146 ev.preventDefault();
1147 var $toggle = $(this);
1148 var doc_width = $(document).width();
1149 var $menu = $toggle.siblings('.oe_dropdown_menu');
1150 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1151 var state = $menu.is('.oe_opened');
1152 setTimeout(function() {
1153 // Do not alter propagation
1154 $toggle.add($menu).toggleClass('oe_opened', !state);
1156 // Move $menu if outside window's edge
1157 var offset = $menu.offset();
1158 var menu_width = $menu.width();
1159 var x = doc_width - offset.left - menu_width - 2;
1161 $menu.offset({ left: offset.left + x }).width(menu_width);
1166 instance.web.bus.on('click', this, function(ev) {
1167 $('.tooltip').remove();
1168 if (!$(ev.target).is('input[type=file]')) {
1169 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1173 show_common: function() {
1175 this.crashmanager = new instance.web.CrashManager();
1176 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1177 self.notification = new instance.web.Notification(this);
1178 self.notification.appendTo(self.$el);
1179 self.loading = new instance.web.Loading(self);
1180 self.loading.appendTo(self.$('.openerp_webclient_container'));
1181 self.action_manager = new instance.web.ActionManager(self);
1182 self.action_manager.appendTo(self.$('.oe_application'));
1184 toggle_bars: function(value) {
1185 this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1187 has_uncommitted_changes: function() {
1192 instance.web.WebClient = instance.web.Client.extend({
1193 init: function(parent, client_options) {
1194 this._super(parent);
1195 if (client_options) {
1196 _.extend(this.client_options, client_options);
1198 this._current_state = null;
1199 this.menu_dm = new instance.web.DropMisordered();
1200 this.action_mutex = new $.Mutex();
1201 this.set('title_part', {"zopenerp": "Odoo"});
1205 this.on("change:title_part", this, this._title_changed);
1206 this._title_changed();
1208 return $.when(this._super()).then(function() {
1209 if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1212 if (self.session.session_is_valid()) {
1213 self.show_application();
1215 if (self.client_options.action) {
1216 self.action_manager.do_action(self.client_options.action);
1217 delete(self.client_options.action);
1221 to_kitten: function() {
1223 $("body").addClass("kitten-mode-activated");
1224 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1226 var imgkit = Math.floor(Math.random() * 2 + 1);
1227 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1231 Sets the first part of the title of the window, dedicated to the current action.
1233 set_title: function(title) {
1234 this.set_title_part("action", title);
1237 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1238 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1239 title of the window separated by '-'.
1241 set_title_part: function(part, title) {
1242 var tmp = _.clone(this.get("title_part"));
1244 this.set("title_part", tmp);
1246 _title_changed: function() {
1247 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1249 _.each(parts, function(part) {
1250 var str = this.get("title_part")[part];
1252 tmp = tmp ? tmp + " - " + str : str;
1255 document.title = tmp;
1257 show_common: function() {
1260 window.onerror = function (message, file, line) {
1261 self.crashmanager.show_error({
1262 type: _t("Client Error"),
1264 data: {debug: file + ':' + line}
1268 show_application: function() {
1270 self.toggle_bars(true);
1273 this.$('.oe_logo_edit_admin').click(function(ev) {
1277 // Menu is rendered server-side thus we don't want the widget to create any dom
1278 self.menu = new instance.web.Menu(self);
1279 self.menu.setElement(this.$el.parents().find('.oe_application_menu_placeholder'));
1281 self.menu.on('menu_click', this, this.on_menu_action);
1282 self.user_menu = new instance.web.UserMenu(self);
1283 self.user_menu.appendTo(this.$el.parents().find('.oe_user_menu_placeholder'));
1284 self.user_menu.on('user_logout', self, self.on_logout);
1285 self.user_menu.do_update();
1286 self.bind_hashchange();
1288 self.check_timezone();
1289 if (self.client_options.action_post_login) {
1290 self.action_manager.do_action(self.client_options.action_post_login);
1291 delete(self.client_options.action_post_login);
1294 update_logo: function() {
1295 var company = this.session.company_id;
1296 var img = this.session.url('/web/binary/company_logo' + (company ? '?company=' + company : ''));
1297 this.$('.oe_logo img').attr('src', '').attr('src', img);
1298 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1300 logo_edit: function(ev) {
1302 ev.preventDefault();
1303 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1304 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1305 result.res_id = res['company_id'][0];
1306 result.target = "new";
1307 result.views = [[false, 'form']];
1309 action_buttons: true,
1311 self.action_manager.do_action(result);
1312 var form = self.action_manager.dialog_widget.views.form.controller;
1313 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1314 form.on('record_saved', self, function() {
1315 self.action_manager.dialog_stop();
1322 check_timezone: function() {
1324 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1325 var user_offset = result[0]['tz_offset'];
1326 var offset = -(new Date().getTimezoneOffset());
1327 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1328 var browser_offset = (offset < 0) ? "-" : "+";
1329 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1330 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1331 if (browser_offset !== user_offset) {
1332 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1333 $icon.on('click', function() {
1334 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1335 user_timezone: instance.session.user_context.tz || 'UTC',
1336 user_offset: user_offset,
1337 browser_offset: browser_offset,
1339 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1340 notification.close();
1341 }).find('a').on('click', function() {
1342 notification.close();
1343 self.user_menu.on_menu_settings();
1347 $icon.prependTo(window.$('.oe_systray'));
1351 destroy_content: function() {
1352 _.each(_.clone(this.getChildren()), function(el) {
1355 this.$el.children().remove();
1357 do_reload: function() {
1359 return this.session.session_reload().then(function () {
1360 instance.session.load_modules(true).then(
1361 self.menu.proxy('do_reload')); });
1363 do_notify: function() {
1364 var n = this.notification;
1365 return n.notify.apply(n, arguments);
1367 do_warn: function() {
1368 var n = this.notification;
1369 return n.warn.apply(n, arguments);
1371 on_logout: function() {
1373 if (!this.has_uncommitted_changes()) {
1374 self.action_manager.do_action('logout');
1377 bind_hashchange: function() {
1379 $(window).bind('hashchange', this.on_hashchange);
1381 var state = $.bbq.getState(true);
1382 if (_.isEmpty(state) || state.action == "login") {
1383 self.menu.is_bound.done(function() {
1384 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1385 if(data.action_id) {
1386 self.action_manager.do_action(data.action_id[0]);
1387 self.menu.open_action(data.action_id[0]);
1389 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1391 self.menu.menu_click(first_menu_id);
1396 $(window).trigger('hashchange');
1399 on_hashchange: function(event) {
1401 var stringstate = event.getState(false);
1402 if (!_.isEqual(this._current_state, stringstate)) {
1403 var state = event.getState(true);
1404 if(!state.action && state.menu_id) {
1405 self.menu.is_bound.done(function() {
1406 self.menu.menu_click(state.menu_id);
1409 state._push_me = false; // no need to push state back...
1410 this.action_manager.do_load_state(state, !!this._current_state);
1413 this._current_state = stringstate;
1415 do_push_state: function(state) {
1416 this.set_title(state.title);
1418 var url = '#' + $.param(state);
1419 this._current_state = $.deparam($.param(state), false); // stringify all values
1420 $.bbq.pushState(url);
1421 this.trigger('state_pushed', state);
1423 on_menu_action: function(options) {
1425 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1426 .then(function (result) {
1427 return self.action_mutex.exec(function() {
1428 if (options.needaction) {
1429 result.context = new instance.web.CompoundContext(result.context, {
1430 search_default_message_unread: true,
1431 search_disable_custom_filters: true,
1434 var completed = $.Deferred();
1435 $.when(self.action_manager.do_action(result, {
1436 clear_breadcrumbs: true,
1437 action_menu_id: self.menu.current_menu,
1438 })).fail(function() {
1439 self.menu.open_menu(options.previous_menu_id);
1440 }).always(function() {
1441 completed.resolve();
1443 setTimeout(function() {
1444 completed.resolve();
1446 // We block the menu when clicking on an element until the action has correctly finished
1447 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1452 set_content_full_screen: function(fullscreen) {
1453 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1454 this.$('.oe_webclient').toggleClass(
1455 'oe_content_full_screen', fullscreen);
1457 has_uncommitted_changes: function() {
1458 var $e = $.Event('clear_uncommitted_changes');
1459 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1460 if ($e.isDefaultPrevented()) {
1463 return this._super.apply(this, arguments);
1468 instance.web.EmbeddedClient = instance.web.Client.extend({
1469 _template: 'EmbedClient',
1470 init: function(parent, origin, dbname, login, key, action_id, options) {
1471 this._super(parent, origin);
1472 this.bind_credentials(dbname, login, key);
1473 this.action_id = action_id;
1474 this.options = options || {};
1478 return $.when(this._super()).then(function() {
1479 return self.authenticate().then(function() {
1480 if (!self.action_id) {
1483 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1484 var action = result;
1485 action.flags = _.extend({
1486 //views_switcher : false,
1487 search_view : false,
1488 action_buttons : false,
1491 }, self.options, action.flags || {});
1493 self.do_action(action);
1499 do_action: function(/*...*/) {
1500 var am = this.action_manager;
1501 return am.do_action.apply(am, arguments);
1504 authenticate: function() {
1505 var s = instance.session;
1506 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1509 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1512 bind_credentials: function(dbname, login, key) {
1513 this.dbname = dbname;
1520 instance.web.embed = function (origin, dbname, login, key, action, options) {
1521 $('head').append($('<link>', {
1522 'rel': 'stylesheet',
1524 'href': origin +'/web/css/web.assets_webclient'
1526 var currentScript = document.currentScript;
1527 if (!currentScript) {
1528 var sc = document.getElementsByTagName('script');
1529 currentScript = sc[sc.length-1];
1531 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1532 client.insertAfter(currentScript);
1537 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: