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 img = this.session.url('/web/binary/company_logo');
1296 this.$('.oe_logo img').attr('src', '').attr('src', img);
1297 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1299 logo_edit: function(ev) {
1301 ev.preventDefault();
1302 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1303 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1304 result.res_id = res['company_id'][0];
1305 result.target = "new";
1306 result.views = [[false, 'form']];
1308 action_buttons: true,
1310 self.action_manager.do_action(result);
1311 var form = self.action_manager.dialog_widget.views.form.controller;
1312 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1313 form.on('record_saved', self, function() {
1314 self.action_manager.dialog_stop();
1321 check_timezone: function() {
1323 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1324 var user_offset = result[0]['tz_offset'];
1325 var offset = -(new Date().getTimezoneOffset());
1326 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1327 var browser_offset = (offset < 0) ? "-" : "+";
1328 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1329 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1330 if (browser_offset !== user_offset) {
1331 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1332 $icon.on('click', function() {
1333 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1334 user_timezone: instance.session.user_context.tz || 'UTC',
1335 user_offset: user_offset,
1336 browser_offset: browser_offset,
1338 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1339 notification.close();
1340 }).find('a').on('click', function() {
1341 notification.close();
1342 self.user_menu.on_menu_settings();
1346 $icon.prependTo(window.$('.oe_systray'));
1350 destroy_content: function() {
1351 _.each(_.clone(this.getChildren()), function(el) {
1354 this.$el.children().remove();
1356 do_reload: function() {
1358 return this.session.session_reload().then(function () {
1359 instance.session.load_modules(true).then(
1360 self.menu.proxy('do_reload')); });
1362 do_notify: function() {
1363 var n = this.notification;
1364 return n.notify.apply(n, arguments);
1366 do_warn: function() {
1367 var n = this.notification;
1368 return n.warn.apply(n, arguments);
1370 on_logout: function() {
1372 if (!this.has_uncommitted_changes()) {
1373 self.action_manager.do_action('logout');
1376 bind_hashchange: function() {
1378 $(window).bind('hashchange', this.on_hashchange);
1380 var state = $.bbq.getState(true);
1381 if (_.isEmpty(state) || state.action == "login") {
1382 self.menu.is_bound.done(function() {
1383 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1384 if(data.action_id) {
1385 self.action_manager.do_action(data.action_id[0]);
1386 self.menu.open_action(data.action_id[0]);
1388 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1390 self.menu.menu_click(first_menu_id);
1395 $(window).trigger('hashchange');
1398 on_hashchange: function(event) {
1400 var stringstate = event.getState(false);
1401 if (!_.isEqual(this._current_state, stringstate)) {
1402 var state = event.getState(true);
1403 if(!state.action && state.menu_id) {
1404 self.menu.is_bound.done(function() {
1405 self.menu.menu_click(state.menu_id);
1408 state._push_me = false; // no need to push state back...
1409 this.action_manager.do_load_state(state, !!this._current_state);
1412 this._current_state = stringstate;
1414 do_push_state: function(state) {
1415 this.set_title(state.title);
1417 var url = '#' + $.param(state);
1418 this._current_state = $.deparam($.param(state), false); // stringify all values
1419 $.bbq.pushState(url);
1420 this.trigger('state_pushed', state);
1422 on_menu_action: function(options) {
1424 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1425 .then(function (result) {
1426 return self.action_mutex.exec(function() {
1427 if (options.needaction) {
1428 result.context = new instance.web.CompoundContext(result.context, {
1429 search_default_message_unread: true,
1430 search_disable_custom_filters: true,
1433 var completed = $.Deferred();
1434 $.when(self.action_manager.do_action(result, {
1435 clear_breadcrumbs: true,
1436 action_menu_id: self.menu.current_menu,
1437 })).fail(function() {
1438 self.menu.open_menu(options.previous_menu_id);
1439 }).always(function() {
1440 completed.resolve();
1442 setTimeout(function() {
1443 completed.resolve();
1445 // We block the menu when clicking on an element until the action has correctly finished
1446 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1451 set_content_full_screen: function(fullscreen) {
1452 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1453 this.$('.oe_webclient').toggleClass(
1454 'oe_content_full_screen', fullscreen);
1456 has_uncommitted_changes: function() {
1457 var $e = $.Event('clear_uncommitted_changes');
1458 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1459 if ($e.isDefaultPrevented()) {
1462 return this._super.apply(this, arguments);
1467 instance.web.EmbeddedClient = instance.web.Client.extend({
1468 _template: 'EmbedClient',
1469 init: function(parent, origin, dbname, login, key, action_id, options) {
1470 this._super(parent, origin);
1471 this.bind_credentials(dbname, login, key);
1472 this.action_id = action_id;
1473 this.options = options || {};
1477 return $.when(this._super()).then(function() {
1478 return self.authenticate().then(function() {
1479 if (!self.action_id) {
1482 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1483 var action = result;
1484 action.flags = _.extend({
1485 //views_switcher : false,
1486 search_view : false,
1487 action_buttons : false,
1490 }, self.options, action.flags || {});
1492 self.do_action(action);
1498 do_action: function(/*...*/) {
1499 var am = this.action_manager;
1500 return am.do_action.apply(am, arguments);
1503 authenticate: function() {
1504 var s = instance.session;
1505 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1508 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1511 bind_credentials: function(dbname, login, key) {
1512 this.dbname = dbname;
1519 instance.web.embed = function (origin, dbname, login, key, action, options) {
1520 $('head').append($('<link>', {
1521 'rel': 'stylesheet',
1523 'href': origin +'/web/css/web.assets_webclient'
1525 var currentScript = document.currentScript;
1526 if (!currentScript) {
1527 var sc = document.getElementsByTagName('script');
1528 currentScript = sc[sc.length-1];
1530 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1531 client.insertAfter(currentScript);
1536 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: